diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 570f9e96e3..41bba10c2e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,13 +22,13 @@ configurations { dependencies { implementation(files("libs/walletconnect-1.5.6.aar")) - implementation(project(":domain:legacy")) - implementation(project(":domain:models")) - implementation(project(":domain:core")) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.core) implementation(projects.domain.card) implementation(projects.domain.demo) - implementation(project(":domain:wallets")) - implementation(project(":domain:wallets:models")) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) @@ -37,21 +37,24 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) - implementation(project(":common")) - implementation(project(":core:analytics")) + implementation(projects.common) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(project(":core:navigation")) - implementation(project(":core:featuretoggles")) - implementation(project(":core:res")) - implementation(project(":core:ui")) - implementation(project(":core:datasource")) - implementation(project(":core:utils")) - implementation(project(":libs:crypto")) - implementation(project(":libs:auth")) + implementation(projects.core.navigation) + implementation(projects.core.featuretoggles) + implementation(projects.core.res) + implementation(projects.core.ui) + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.libs.crypto) + implementation(projects.libs.auth) implementation(projects.data.appCurrency) implementation(projects.data.appTheme) + implementation(projects.data.balanceHiding) implementation(projects.data.card) implementation(projects.data.common) implementation(projects.data.settings) @@ -61,20 +64,20 @@ dependencies { implementation(projects.data.wallets) /** Features */ - implementation(project(":features:onboarding")) - implementation(project(":features:learn2earn:api")) - implementation(project(":features:learn2earn:impl")) - implementation(project(":features:referral:presentation")) - implementation(project(":features:referral:domain")) - implementation(project(":features:referral:data")) - implementation(project(":features:swap:api")) - implementation(project(":features:swap:presentation")) - implementation(project(":features:swap:domain")) - implementation(project(":features:swap:data")) - implementation(project(":features:tester:api")) - implementation(project(":features:tester:impl")) - implementation(project(":features:wallet:api")) - implementation(project(":features:wallet:impl")) + implementation(projects.features.onboarding) + implementation(projects.features.learn2earn.api) + implementation(projects.features.learn2earn.impl) + implementation(projects.features.referral.presentation) + implementation(projects.features.referral.domain) + implementation(projects.features.referral.data) + implementation(projects.features.swap.api) + implementation(projects.features.swap.presentation) + implementation(projects.features.swap.domain) + implementation(projects.features.swap.data) + implementation(projects.features.tester.api) + implementation(projects.features.tester.impl) + implementation(projects.features.wallet.api) + implementation(projects.features.wallet.impl) implementation(projects.features.tokendetails.api) implementation(projects.features.tokendetails.impl) @@ -95,6 +98,7 @@ dependencies { /** Compose libraries */ implementation(deps.compose.constraintLayout) implementation(deps.compose.material) + implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index efa00b067f..23547755c9 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -1,11 +1,14 @@ package com.tangem.tap +import android.app.Application import android.content.Intent import android.content.pm.ActivityInfo import android.os.Bundle import android.view.View import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope @@ -14,6 +17,7 @@ import com.google.android.material.snackbar.Snackbar import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.data.card.sdk.CardSdkLifecycleObserver +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager @@ -127,11 +131,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() + if (!isDarkThemeFeatureEnabled(application)) { + setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + } + super.onCreate(savedInstanceState) cardSdkLifecycleObserver.onCreate(context = this) - bootstrapMainStateUpdates() + bootstrapMainStateUpdates(application) splashScreen.setKeepOnScreenCondition { isInitializing } @@ -165,6 +173,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac ) } + private fun isDarkThemeFeatureEnabled(application: Application): Boolean { + val featureToggle = (application as TapApplication).darkThemeFeatureToggle + return featureToggle.isDarkThemeEnabled + } + override fun onStart() { super.onStart() dialogManager.onStart(this) @@ -216,14 +229,25 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager)) } - private fun bootstrapMainStateUpdates() { + private fun bootstrapMainStateUpdates(application: Application) { viewModel.state .onEach { state -> isInitializing = state is GlobalSettingsState.Loading when (state) { is GlobalSettingsState.Content -> { - MutableAppThemeModeHolder.value = state.appThemeMode + if (isDarkThemeFeatureEnabled(application)) { + MutableAppThemeModeHolder.value = state.appThemeMode + + val mode = when (state.appThemeMode) { + AppThemeMode.FORCE_DARK -> AppCompatDelegate.MODE_NIGHT_YES + AppThemeMode.FORCE_LIGHT -> AppCompatDelegate.MODE_NIGHT_NO + AppThemeMode.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + setDefaultNightMode(mode) + } else { + MutableAppThemeModeHolder.value = AppThemeMode.FORCE_LIGHT + } } is GlobalSettingsState.Loading -> Unit } diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index cfd38560ca..b02a66920f 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -22,10 +22,12 @@ import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.domain.DomainLayer import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +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.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles @@ -61,6 +63,8 @@ import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementa import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles +import com.tangem.tap.features.details.DarkThemeFeatureToggle +import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.BuildConfig @@ -169,6 +173,21 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var walletManagersFacade: WalletManagersFacade + @Inject + lateinit var currenciesRepository: CurrenciesRepository + + @Inject + lateinit var appThemeModeRepository: AppThemeModeRepository + + @Inject + lateinit var balanceHidingRepository: BalanceHidingRepository + + @Inject + lateinit var detailsFeatureToggles: DetailsFeatureToggles + + @Inject + lateinit var darkThemeFeatureToggle: DarkThemeFeatureToggle + override fun onCreate() { super.onCreate() @@ -189,6 +208,11 @@ class TapApplication : Application(), ImageLoaderFactory { scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, + appStateHolder = appStateHolder, + currenciesRepository = currenciesRepository, + appThemeModeRepository = appThemeModeRepository, + balanceHidingRepository = balanceHidingRepository, + detailsFeatureToggles = detailsFeatureToggles, ), ), ) @@ -208,7 +232,6 @@ class TapApplication : Application(), ImageLoaderFactory { activityResultCaller = foregroundActivityObserver registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) - DomainLayer.init() preferencesStorage = preferencesDataSource walletConnectRepository = WalletConnectRepository(this) diff --git a/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt b/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt deleted file mode 100644 index b09112d39f..0000000000 --- a/app/src/main/java/com/tangem/tap/common/CompositionCounter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.tap.common - -import timber.log.Timber - -class CompositionCounter( - val id: String, - count: Int = 0, -) { - var count: Int = count - private set - - fun increase(id: String): CompositionCounter { - if (this.id != id) return this - - count += 1 - return CompositionCounter(id, count) - } -} - -class CompositionLogger( - private val recomposeViewId: String, - private val tag: String = recomposeViewId, - private var turnOnForIds: List = listOf(recomposeViewId), -) { - val count: Int - get() = compositionCounter.count - - private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId) - - fun nextComposition() { - compositionCounter = compositionCounter.increase(recomposeViewId) - log("") - } - - fun log(message: String) { - if (!turnOnForIds.contains(recomposeViewId)) return - - Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index e5e4c697a6..d1dd0f06bb 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -8,10 +8,7 @@ sealed class AnalyticsParam { class Currency(currency: com.tangem.tap.features.wallet.models.Currency) : CurrencyType(currency.currencySymbol) class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency) class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol) - class FiatCurrency( - fiatCurrency: com.tangem.tap.common.entities.FiatCurrency, - ) : CurrencyType(fiatCurrency.code) - + class FiatCurrency(fiatCurrency: com.tangem.tap.common.entities.FiatCurrency) : CurrencyType(fiatCurrency.code) class Amount(amount: com.tangem.blockchain.common.Amount) : CurrencyType(amount.currencySymbol) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt index 3e961883b7..8270df641c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt @@ -12,6 +12,10 @@ sealed class Portfolio( ) : AnalyticsEvent("Portfolio", event, params) { class Refreshed : Portfolio("Refreshed") + class ButtonManageTokens : Portfolio("Button - Manage Tokens") + class TokenTapped : Portfolio("Token is Tapped") + + class OrganizeTokens : Portfolio("Button - Organize Tokens") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt deleted file mode 100644 index 59cf01d88c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ /dev/null @@ -1,277 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.LinearProgressIndicator -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldColors -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.tangem.common.module.ModuleError -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.common.form.Field -import com.tangem.tap.common.CompositionLogger -import com.tangem.tap.common.compose.extensions.stringResourceDefault -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun OutlinedTextFieldWidget( - fieldData: Field.Data, - labelId: Int? = null, - label: String = "", - placeholderId: Int? = null, - placeholder: String = "", - trailingIcon: @Composable (() -> Unit)? = null, - isEnabled: Boolean = true, - isVisible: Boolean = true, - isLoading: Boolean = false, - error: ModuleError? = null, - errorConverter: ModuleMessageConverter? = null, - debounceTextChanges: Long = 400, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - onTextChange: (String) -> Unit, -) { - if (!isVisible) return - - Column(modifier = Modifier.animateContentSize()) { - OutlinedProgressTextField( - fieldData = fieldData, - label = stringResourceDefault(labelId, label), - placeholder = stringResourceDefault(placeholderId, placeholder), - trailingIcon = trailingIcon, - isEnabled = isEnabled, - isLoading = isLoading, - error = error, - debounce = debounceTextChanges, - visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, - onTextChange = onTextChange, - ) - errorConverter?.let { AnimatedErrorView(errorConverter = it, error = error) } - } -} - -@Suppress("LongMethod", "NestedBlockDepth", "MagicNumber", "MaxLineLength") -@Composable -private fun OutlinedProgressTextField( - fieldData: Field.Data, - label: String = "", - placeholder: String = "", - isEnabled: Boolean = true, - isLoading: Boolean = false, - error: ModuleError? = null, - debounce: Long = 400, - visualTransformation: VisualTransformation = VisualTransformation.None, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - colors: TextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - trailingIcon: @Composable (() -> Unit)? = null, - onTextChange: (String) -> Unit, -) { - val logger = remember { - CompositionLogger(label, "OutlinedProgressTextField", listOf("Символ токена")) - } - logger.nextComposition() - - val textValueState = remember { mutableStateOf(fieldData.value) } - val textDebouncer = valueDebouncerAsState( - initialValue = fieldData.value, - debounce = debounce, - onEmitValueReceive = { - logger.log("DEBOUNCER: onEmitValueReceived: [$it]") - logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]") - textValueState.value = it - }, - onValueChange = { - logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])") - onTextChange(it) - }, - ) - - logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]") - logger.log("RECOMPOSE --data: fieldData.value: [$fieldData]") - logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]") - logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]") - logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]") - - if (!fieldData.isUserInput) { - // initial value is not from an user - val isNotUserInput = "-- IS NOT USER INPUT" - logger.log("recompose $isNotUserInput") - if (textValueState.value == fieldData.value) { - logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле") - } else { - logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле") - if (textDebouncer.emittedValue != textDebouncer.debounced || textDebouncer.emitsCountBeforeDebounce > 0) { - logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE") - } else { - logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные") - if (textValueState.value != textDebouncer.emittedValue || - textValueState.value != textDebouncer.debounced - ) { - logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer") - if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) { - logger.log( - "$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } else { - logger.log( - "$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } - } else { - logger.log( - "$isNotUserInput: в пустое поле вставляются данные -> start RECOMPOSE новые данные для " + - "textValueState.value = [${fieldData.value}]", - ) - textValueState.value = fieldData.value - } - } - } - } - logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]") - - Box { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth(), - value = textValueState.value, - onValueChange = { - logger.log("WIDGET: textDebouncer.emmit([$it])") - textDebouncer.emmit(it) - }, - keyboardOptions = keyboardOptions, - label = { - Text( - text = label, - style = TangemTheme.typography.caption, - color = colors.labelColor( - enabled = isEnabled, - error = error != null, - interactionSource = interactionSource, - ).value, - ) - }, - placeholder = { - Text( - text = placeholder, - style = TangemTheme.typography.body1, - color = colors.placeholderColor(enabled = isEnabled).value, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - trailingIcon = trailingIcon, - singleLine = true, - enabled = isEnabled, - isError = error != null, - visualTransformation = visualTransformation, - colors = colors, - interactionSource = interactionSource, - ) - AnimatedVisibility( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter) - .padding(start = 6.dp, top = 0.dp, end = 6.dp, bottom = 6.dp), - visible = isLoading, - ) { - LinearProgressIndicator( - color = TangemTheme.colors.icon.primary1, - ) - } - } -} - -@Composable -private fun AnimatedErrorView(errorConverter: ModuleMessageConverter, error: ModuleError? = null) { - AnimatedVisibility( - visible = error != null, - enter = fadeIn() + slideInVertically(), - exit = slideOutVertically() + fadeOut(), - ) { - error?.let { - ErrorView( - text = errorConverter.convert(it).message, - style = TextStyle(fontSize = 14.sp), - ) - } - } -} - -@Preview -@Composable -private fun OutlinedTextFieldWithErrorTest() { - val context = LocalContext.current - val converter = remember { ModuleMessageConverter(context) } - - class SimpleError( - override val code: Int = 1, - override val message: String = "Error message", - override val data: Any? = null, - ) : ModuleError() - - val modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - Column { - OutlinedTextFieldWidget( - fieldData = Field.Data("", false), - label = "First label", - placeholder = "1 placeholder", - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - isLoading = true, - error = null, - errorConverter = converter, - ) {} - OutlinedTextFieldWidget( - fieldData = Field.Data("First", false), - label = "First label", - placeholder = "1 placeholder", - error = SimpleError(), - errorConverter = converter, - ) {} - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt b/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt deleted file mode 100644 index d5e5168be3..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.foundation.layout.Column -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.sp - -/** -[REDACTED_AUTHOR] - * Compose views are not typically used as a main or base view. - */ - -@Composable -fun TitleSubtitle(title: String, subtitle: String) { - Column { - Text(text = title) - Text( - text = subtitle, - fontSize = 12.sp, - color = Color.Gray, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt deleted file mode 100644 index 5b8da92e24..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.common.compose.extensions - -import android.content.res.Resources -import androidx.annotation.StringRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun stringResourceDefault(@StringRes id: Int?, default: String = ""): String { - val resources = LocalContext.current.resources - return try { - resources.getString(requireNotNull(id)) - } catch (ex: Resources.NotFoundException) { - default - } catch (ex: IllegalArgumentException) { - default - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 9119734bfd..5f4abc182a 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -6,7 +6,8 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.FragmentShareTransition import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.swap.presentation.SwapFragment -import com.tangem.tap.features.customtoken.legacy.AddCustomTokenFragment +import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment @@ -33,7 +34,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.wallet.R import timber.log.Timber -import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment as RedesignedAddCustomTokenFragment fun FragmentActivity.openFragment( screen: AppScreen, @@ -155,18 +155,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment() AppScreen.Disclaimer -> DisclaimerFragment() AppScreen.AddTokens -> TokensListFragment() - - AppScreen.AddCustomToken -> { - val featureToggles = store.state.daggerGraphState.get( - getDependency = DaggerGraphState::customTokenFeatureToggles, - ) - if (featureToggles.isRedesignedScreenEnabled) { - RedesignedAddCustomTokenFragment() - } else { - AddCustomTokenFragment() - } - } - + AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> { val featureToggles = store.state.daggerGraphState.get( getDependency = DaggerGraphState::tokenDetailsFeatureToggles, @@ -186,5 +175,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.Welcome -> WelcomeFragment() AppScreen.SaveWallet -> SaveWalletBottomSheetFragment() AppScreen.WalletSelector -> WalletSelectorBottomSheetFragment() + AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt b/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt deleted file mode 100644 index 3dd4357bba..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/ValueCallback.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.tap.common.extensions - -/** -[REDACTED_AUTHOR] - */ -typealias ValueCallback = (T) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 2d75cf1723..5e159ae358 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.models.ChatConfig +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.analytics.topup.TopUpController @@ -103,4 +104,5 @@ sealed class GlobalAction : Action { } data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction() + data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index 287b5d35f1..0f2f76f8d0 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -66,7 +66,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di } } is GlobalAction.RestoreAppCurrency -> { - if (store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles).isRedesignedScreenEnabled) { + val daggerGraphState = store.state.daggerGraphState + val walletFeatureToggles = daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + val detailsFeatureToggles = daggerGraphState.get(DaggerGraphState::detailsFeatureToggles) + + if (walletFeatureToggles.isRedesignedScreenEnabled || + detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled + ) { restoreAppCurrencyNew() } else { restoreAppCurrencyLegacy() @@ -131,6 +137,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di sellService = makeSellExchangeService(config), primaryRules = CardExchangeRules(cardProvider), ) + // TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance) + store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager)) store.dispatchOnMain(GlobalAction.ExchangeManager.Update) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 0c90110fcb..2b282844d5 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -97,6 +97,9 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde userWalletsListManager = action.manager, ) } + is GlobalAction.ChangeAppThemeMode -> globalState.copy( + appThemeMode = action.appThemeMode, + ) else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 1e5a3cfce0..f5a30ba673 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.redux.global import com.tangem.datasource.config.ConfigManager +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.common.analytics.topup.TopUpController @@ -29,6 +30,7 @@ data class GlobalState( val userCountryCode: String? = null, val userWalletsListManager: UserWalletsListManager? = null, val topUpController: TopUpController? = null, + val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT, ) : StateType typealias CryptoCurrencyName = String diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index a894d7c4fe..58073accdf 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -3,8 +3,11 @@ package com.tangem.tap.di import android.content.Context import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.exchange.RampStateManager import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository +import com.tangem.tap.network.exchangeServices.DefaultRampManager +import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.userTokensRepository import dagger.Module import dagger.Provides @@ -40,4 +43,10 @@ internal object ActivityModule { ), ) } + + @Provides + @Singleton + fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager { + return DefaultRampManager(appStateHolder.exchangeService) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt index c69bca18d1..ab7bc24782 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AppCurrencyDomainModule.kt @@ -1,22 +1,34 @@ package com.tangem.tap.di.domain +import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase import com.tangem.domain.appcurrency.repository.AppCurrencyRepository 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 AppCurrencyDomainModule { @Provides - @ViewModelScoped fun provideGetSelectedAppCurrencyUseCase( appCurrencyRepository: AppCurrencyRepository, ): GetSelectedAppCurrencyUseCase { return GetSelectedAppCurrencyUseCase(appCurrencyRepository) } + + @Provides + fun provideSelectAppCurrencyUseCase(appCurrencyRepository: AppCurrencyRepository): SelectAppCurrencyUseCase { + return SelectAppCurrencyUseCase(appCurrencyRepository) + } + + @Provides + fun provideGetAvailableCurrenciesUseCase( + appCurrencyRepository: AppCurrencyRepository, + ): GetAvailableCurrenciesUseCase { + return GetAvailableCurrenciesUseCase(appCurrencyRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index eaf0da329c..a25d200f68 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -1,8 +1,10 @@ package com.tangem.tap.di.domain -import com.tangem.domain.settings.CanUseBiometryUseCase -import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase -import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.balancehiding.DeviceFlipDetector +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository +import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository @@ -37,4 +39,24 @@ internal object SettingsDomainModule { legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager), ) } + + @Provides + @ViewModelScoped + fun providesIsBalanceHiddenUseCase(balanceHidingRepository: BalanceHidingRepository): IsBalanceHiddenUseCase { + return IsBalanceHiddenUseCase( + balanceHidingRepository = balanceHidingRepository, + ) + } + + @Provides + @ViewModelScoped + fun providesListenUseCase( + flipDetector: DeviceFlipDetector, + balanceHidingRepository: BalanceHidingRepository, + ): ListenToFlipsUseCase { + return ListenToFlipsUseCase( + flipDetector = flipDetector, + balanceHidingRepository = balanceHidingRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 1a77f50c7b..352c3c3353 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -1,9 +1,12 @@ package com.tangem.tap.di.domain +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -56,6 +59,24 @@ internal object TokensDomainModule { return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) } + @Provides + @ViewModelScoped + fun provideGetCurrencyWarningsUseCase( + walletManagersFacade: WalletManagersFacade, + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetCurrencyWarningsUseCase { + return GetCurrencyWarningsUseCase( + walletManagersFacade = walletManagersFacade, + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + dispatchers = dispatchers, + ) + } + @Provides @ViewModelScoped fun provideGetPrimaryCurrencyUseCase( @@ -108,8 +129,32 @@ internal object TokensDomainModule { @Provides @ViewModelScoped fun provideGetCryptoCurrencyActionsUseCase( + rampStateManager: RampStateManager, + marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { - return GetCryptoCurrencyActionsUseCase(dispatchers) + return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideGetCurrencyStatusByNetworkUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetNetworkCoinStatusUseCase { + return GetNetworkCoinStatusUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + dispatchers = dispatchers, + ) + } + + @Provides + @ViewModelScoped + fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { + return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 6d23a8b78a..c10b88280a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -19,6 +19,7 @@ import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.setContext +import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletStores.WalletStoresError import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -28,11 +29,13 @@ import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import org.rekotlin.Store import timber.log.Timber class TapWalletManager( @@ -75,17 +78,33 @@ class TapWalletManager( withMainContext { // Order is important store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer())) - store.dispatch(WalletAction.UserWalletChanged(userWallet)) - store.dispatch(WalletAction.UpdateCanSaveUserWallets(preferencesStorage.shouldSaveUserWallets)) + store.dispatchWalletAction(action = WalletAction.UserWalletChanged(userWallet)) + store.dispatchWalletAction( + action = WalletAction.UpdateCanSaveUserWallets( + canSaveUserWallets = preferencesStorage.shouldSaveUserWallets, + ), + ) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) store.dispatch(WalletConnectAction.ResetState) store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) store.dispatch(WalletConnectAction.RestoreSessions(scanResponse)) store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed)) - store.dispatch(WalletAction.Warnings.CheckIfNeeded) + store.dispatchWalletAction(action = WalletAction.Warnings.CheckIfNeeded) } setupWalletConnectV2(userWallet) - loadData(userWallet, refresh) + + val walletFeatureToggles = store.state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + loadData(userWallet = userWallet, refresh = refresh) + } + } + + private fun Store.dispatchWalletAction(action: WalletAction) { + val walletFeatureToggles = state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles) + + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + dispatch(action = action) + } } private fun setupWalletConnectV2(userWallet: UserWallet) { diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt deleted file mode 100644 index 39e845f7b2..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.tangem.tap.domain.tokens - -import com.squareup.moshi.JsonAdapter -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.services.Result -import com.tangem.datasource.api.common.MoshiConverter -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.datasource.asset.AssetReader -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class LoadAvailableCoinsService( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val assetReader: AssetReader, -) { - private val currenciesAdapter: JsonAdapter = - MoshiConverter.networkMoshi.adapter(CurrenciesFromJson::class.java) - - suspend fun getSupportedTokens( - isTestNet: Boolean, - supportedBlockchains: List, - page: Int, - searchInput: String?, - ): Result { - if (isTestNet) { - return Result.Success( - LoadedCoins( - currencies = getTestnetCoins().filter(searchInput), - moreAvailable = false, - ), - ) - } - - val offset = page * LOAD_PER_PAGE - return when (val result = loadCoins(supportedBlockchains, offset, searchInput)) { - is Result.Success -> { - val data = result.data - - Result.Success( - LoadedCoins( - currencies = data.coins.map { - Currency.fromCoinResponse(currency = it, imageHost = data.imageHost) - }, - moreAvailable = data.total > offset + LOAD_PER_PAGE, - ), - ) - } - is Result.Failure -> { - Result.Failure(result.error) - } - } - } - - private suspend fun loadCoins( - supportedBlockchains: List, - offset: Int, - searchInput: String?, - ): Result { - return withContext(dispatchers.io) { - runCatching { - tangemTechApi.getCoins( - networkIds = supportedBlockchains.joinToString( - separator = ",", - transform = Blockchain::toNetworkId, - ), - active = true, - searchText = searchInput, - offset = offset, - limit = LOAD_PER_PAGE, - ) - }.fold( - onSuccess = { Result.Success(it) }, - onFailure = { Result.Failure(it) }, - ) - } - } - - private fun getTestnetCoins(): List { - val json = assetReader.readJson(FILE_NAME_TESTNET_COINS) - return currenciesAdapter.fromJson(json)!!.coins - .map { Currency.fromJsonObject(it) } - } - - private fun List.filter(searchInput: String?): List { - if (searchInput.isNullOrBlank()) return this - - return filter { currency -> - currency.symbol.contains(searchInput, ignoreCase = true) || - currency.name.contains(searchInput, ignoreCase = true) - } - } - - private companion object { - const val LOAD_PER_PAGE = 100 - const val FILE_NAME_TESTNET_COINS = "testnet_tokens" - } -} - -data class LoadedCoins( - val currencies: List, - val moreAvailable: Boolean, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt index b64f9efd14..a6894e04b1 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt @@ -7,8 +7,5 @@ package com.tangem.tap.features.customtoken.api.featuretoggles */ interface CustomTokenFeatureToggles { - /** Availability of redesigned screen (internal feature) */ - val isRedesignedScreenEnabled: Boolean - val isNewCardScanningEnabled: Boolean } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt index a19dde5403..cd06665b00 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt @@ -14,9 +14,6 @@ internal class DefaultCustomTokenFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : CustomTokenFeatureToggles { - override val isRedesignedScreenEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED") - override val isNewCardScanningEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt deleted file mode 100644 index 204f95ec25..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy - -import android.os.Bundle -import android.view.View -import android.view.WindowManager -import androidx.appcompat.widget.Toolbar -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import com.tangem.core.analytics.Analytics -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.analytics.events.ManageTokens -import com.tangem.tap.features.BaseStoreFragment -import com.tangem.tap.features.FragmentOnBackPressedHandler -import com.tangem.tap.features.addBackPressHandler -import com.tangem.tap.features.customtoken.legacy.compose.AddCustomTokenScreen -import com.tangem.tap.features.customtoken.legacy.compose.ClosePopupTrigger -import com.tangem.wallet.R -import org.rekotlin.StoreSubscriber - -/** -[REDACTED_AUTHOR] - */ -class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber { - - private var state: MutableState = mutableStateOf(domainStore.state.addCustomTokensState) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - Analytics.send(ManageTokens.CustomToken.ScreenOpened) - } - - override fun subscribeToStore() { - domainStore.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.addCustomTokensState == newState.addCustomTokensState - }.select { it.addCustomTokensState } - } - } - - override fun newState(state: AddCustomTokenState) { - if (activity == null || view == null) return - - this.state.value = state - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) - view.findViewById(R.id.toolbar)?.setTitle(R.string.add_custom_token_title) - - val closePopupTrigger = initClosingPopupTriggerEvent() - view.findViewById(R.id.view_compose)?.setContent { - TangemTheme { - Box( - modifier = Modifier - .fillMaxSize(), - ) { - AddCustomTokenScreen(state, closePopupTrigger) - } - } - } - } - - private fun initClosingPopupTriggerEvent(): ClosePopupTrigger = ClosePopupTrigger().apply { - onCloseComplete = ::handleOnBackPressed - addBackPressHandler( - object : FragmentOnBackPressedHandler { - override fun handleOnBackPressed() = close() - }, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt deleted file mode 100644 index baa487cec7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt +++ /dev/null @@ -1,208 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.common.form.DataField -import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.features.addCustomToken.redux.ScreenState -import com.tangem.domain.features.addCustomToken.redux.ViewStates -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.AddCustomTokenWarning -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter -import com.tangem.tap.features.customtoken.legacy.compose.test.TestCase -import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList -import com.tangem.wallet.R -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun AddCustomTokenScreen(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) } - - val bottomSheetScaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed), - ) - val coroutineScope = rememberCoroutineScope() - val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } } - - BottomSheetScaffold( - scaffoldState = bottomSheetScaffoldState, - sheetContent = { - Surface(color = colorResource(id = R.color.lightGray5)) { - selectedTestCase.value.content(toggleBottomSheet) - } - }, - sheetPeekHeight = 0.dp, - ) { - Column { - TestCasesList( - onItemClick = { - selectedTestCase.value = it - toggleBottomSheet() - }, - ) - ScreenContent(state, closePopupTrigger) - } - } - - ComposeDialogManager() - LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) }) - DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } }) -} - -@OptIn(ExperimentalMaterialApi::class) -private suspend fun BottomSheetScaffoldState.toggle() { - if (bottomSheetState.isCollapsed) { - bottomSheetState.expand() - } else { - bottomSheetState.collapse() - } -} - -@Composable -private fun ScreenContent(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val scaffoldState = rememberScaffoldState() - - Scaffold( - scaffoldState = scaffoldState, - backgroundColor = colorResource(id = R.color.backgroundLightGray), - floatingActionButton = { - HangingOverKeyboardView(keyboardState = keyboardAsState()) { - AddButton(state) - } - }, - floatingActionButtonPosition = FabPosition.Center, - ) { paddings -> - Box( - modifier = Modifier - .padding(paddings) - .fillMaxSize(), - ) { - LazyColumn( - contentPadding = PaddingValues(bottom = 90.dp), - ) { - item { - Surface( - modifier = Modifier.padding(16.dp), - shape = MaterialTheme.shapes.small, - elevation = 4.dp, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - FormFields(state, closePopupTrigger) - } - } - } - item { Warnings(state.value.warnings.toList()) } - } - } - } -} - -@Composable -private fun FormFields(state: MutableState, closePopupTrigger: ClosePopupTrigger) { - val context = LocalContext.current - val errorConverter = remember { ModuleMessageConverter(context) } - - val stateValue = state.value - stateValue.form.fieldList.forEach { field -> - val data = ScreenFieldData.fromState(field, stateValue, errorConverter) - when (field.id) { - ContractAddress -> TokenContractAddressView(data) - Network -> TokenNetworkView(data, stateValue, closePopupTrigger) - Name -> TokenNameView(data) - Symbol -> TokenSymbolView(data) - Decimals -> TokenDecimalsView(data) - DerivationPath -> TokenDerivationPathView(data, stateValue, closePopupTrigger) - } - } -} - -@Composable -fun Warnings(warnings: List) { - if (warnings.isEmpty()) return - - val context = LocalContext.current - val warningConverter = remember { ModuleMessageConverter(context) } - - Column { - warnings.forEachIndexed { index, item -> - val modifier = when (index) { - 0 -> Modifier.padding(vertical = 0.dp) - warnings.lastIndex -> Modifier.padding(top = 8.dp, bottom = 16.dp) - else -> Modifier.padding(top = 8.dp, bottom = 0.dp) - } - AddCustomTokenWarning( - modifier = modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - warning = item, - converter = warningConverter, - ) - } - } -} - -@Composable -private fun AddButton(state: MutableState) { - PrimaryButtonIconStart( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResource(id = R.string.common_add), - iconResId = R.drawable.ic_plus_24, - enabled = state.value.screenState.addButton.isEnabled, - onClick = { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) }, - ) -} - -data class ScreenFieldData( - val field: DataField<*>, - val error: AddCustomTokenError?, - val errorConverter: ModuleMessageConverter, - val viewState: ViewStates.TokenField, -) { - companion object { - fun fromState( - field: DataField<*>, - state: AddCustomTokenState, - errorConverter: ModuleMessageConverter, - ): ScreenFieldData { - return ScreenFieldData( - field = field, - error = state.getError(field.id), - errorConverter = errorConverter, - viewState = selectField(field.id, state.screenState), - ) - } - - private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField { - return when (id) { - ContractAddress -> screenState.contractAddressField - Network -> screenState.network - Name -> screenState.name - Symbol -> screenState.symbol - Decimals -> screenState.decimals - DerivationPath -> screenState.derivationPath - else -> throw UnsupportedOperationException() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt deleted file mode 100644 index d6c60c2b26..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/BlockchainSpinner.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.form.Field - -@Composable -fun BlockchainSpinner( - @StringRes title: Int, - itemList: List, - selectedItem: Field.Data, - isEnabled: Boolean = true, - textFieldConverter: (Blockchain) -> String, - dropdownItemView: @Composable ((Blockchain) -> Unit)? = null, - closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(), - onItemSelect: (Blockchain) -> Unit, -) { - OutlinedSpinner( - modifier = Modifier.fillMaxWidth(), - label = stringResource(id = title), - itemList = itemList, - selectedItem = selectedItem, - textFieldConverter = textFieldConverter, - dropdownItemView = dropdownItemView, - isEnabled = isEnabled, - onItemSelected = onItemSelect, - closePopupTrigger = closePopupTrigger, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt deleted file mode 100644 index 453ace8c9e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/ComposeDialogManager.kt +++ /dev/null @@ -1,157 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.AlertDialog -import androidx.compose.material.Button -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.res.TangemTheme -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.DomainDialog -import com.tangem.domain.redux.domainStore -import com.tangem.domain.redux.global.DomainGlobalAction -import com.tangem.domain.redux.global.DomainGlobalState -import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter -import com.tangem.wallet.R -import org.rekotlin.StoreSubscriber - -@Composable -internal fun ComposeDialogManager() { - val dialogSate = remember { mutableStateOf(null) } - val subscriber = remember { - object : StoreSubscriber { - override fun newState(state: DomainGlobalState) { - dialogSate.value = state.dialog - } - } - } - - ShowTheDialog(dialogSate) - - LaunchedEffect( - key1 = Unit, - block = { - domainStore.subscribe(subscriber) { state -> - state.skipRepeats { oldState, newState -> - oldState.globalState == newState.globalState - }.select { it.globalState } - } - }, - ) - DisposableEffect( - key1 = Unit, - effect = { - onDispose { domainStore.unsubscribe(subscriber) } - }, - ) -} - -@Composable -private fun ShowTheDialog(dialogState: MutableState) { - if (dialogState.value == null) return - - val context = LocalContext.current - val errorConverter = remember { ModuleMessageConverter(context) } - val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) } - - when (val dialog = dialogState.value) { - is DomainDialog.DialogError -> ErrorDialog( - title = stringResource(id = R.string.common_error), - body = errorConverter.convert(dialog.error).message, - onDismissRequest, - ) - is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest) - else -> {} - } -} - -/** - * Dialog with single item selection - */ -@Composable -fun SimpleDialog( - title: String, - items: List, - onSelect: (CoinsResponse.Coin.Network) -> Unit, - onDismissRequest: () -> Unit, - itemContent: @Composable (CoinsResponse.Coin.Network) -> Unit, -) { - Dialog( - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), - onDismissRequest = { }, - ) { - Surface(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) { - Column(modifier = Modifier.padding(TangemTheme.dimens.spacing22)) { - DialogTitle(title = title) - LazyColumn { - items(items = items, key = CoinsResponse.Coin.Network::networkId) { item -> - Row( - modifier = Modifier - .fillMaxWidth() - .height(56.dp) - .clickable { - onSelect(item) - onDismissRequest() - }, - verticalAlignment = Alignment.CenterVertically, - ) { itemContent(item) } - } - } - } - } - } -} - -@Composable -private fun DialogTitle(title: String) { - Text( - text = title, - style = LocalTextStyle.provides( - TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - ), - ).value, - ) - SpacerH16() -} - -@Composable -fun ErrorDialog(title: String, body: String, onDismissRequest: () -> Unit) { - AlertDialog( - title = { DialogTitle(title) }, - text = { Text(body) }, - onDismissRequest = onDismissRequest, - confirmButton = { - Button(onClick = onDismissRequest) { - Text(text = stringResource(id = R.string.common_ok)) - } - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt deleted file mode 100644 index a640e91575..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.components.SpacerH8 -import com.tangem.domain.common.form.Field -import com.tangem.domain.features.addCustomToken.TokenBlockchainField -import com.tangem.domain.features.addCustomToken.TokenDerivationPathField -import com.tangem.domain.features.addCustomToken.TokenField -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.OutlinedTextFieldWidget -import com.tangem.tap.common.compose.TitleSubtitle -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun TokenContractAddressView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_contract_address_input_title, - placeholder = "0x0000000000000000000000000000000000000000", - isEnabled = screenFieldData.viewState.isEnabled, - isLoading = screenFieldData.viewState.isLoading, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, -// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) } - ) { - domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true))) - } - SpacerH8() -} - -@Composable -fun TokenNameView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_name_input_title, - placeholderId = R.string.custom_token_name_input_placeholder, - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { - domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true))) - } - SpacerH8() -} - -@Composable -fun TokenNetworkView( - screenFieldData: ScreenFieldData, - state: AddCustomTokenState, - closePopupTrigger: ClosePopupTrigger, -) { - if (!screenFieldData.viewState.isVisible) return - - val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) - val networkField = screenFieldData.field as TokenBlockchainField - - BlockchainSpinner( - title = R.string.custom_token_network_input_title, - itemList = networkField.itemList, - selectedItem = networkField.data, - isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.blockchainToName(it) ?: notSelected }, - closePopupTrigger = closePopupTrigger, - ) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenSymbolView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_token_symbol_input_title, - placeholderId = R.string.custom_token_token_symbol_input_placeholder, - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenDecimalsView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - fieldData = tokenField.data, - labelId = R.string.custom_token_decimals_input_title, - placeholder = "8", - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) } - SpacerH8() -} - -@Composable -fun TokenDerivationPathView( - screenFieldData: ScreenFieldData, - state: AddCustomTokenState, - closePopupTrigger: ClosePopupTrigger, -) { - if (!screenFieldData.viewState.isVisible) return - - val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) - val networkField = screenFieldData.field as TokenDerivationPathField - - BlockchainSpinner( - title = R.string.custom_token_derivation_path_input_title, - itemList = networkField.itemList, - selectedItem = networkField.data, - isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.blockchainToName(it) ?: notSelected }, - dropdownItemView = { blockchain -> - val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected - val blockchainName = state.blockchainToName(blockchain) ?: notSelected - TitleSubtitle(derivationPathName, blockchainName) - }, - closePopupTrigger = closePopupTrigger, - ) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) } - SpacerH8() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt deleted file mode 100644 index 6aa2994210..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.Keyboard - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun HangingOverKeyboardView( - modifier: Modifier = Modifier, - keyboardState: State, - spaceBetweenKeyboard: Dp = 0.dp, - content: @Composable (BoxScope.() -> Unit), -) { - val padding = remember(keyboardState) { - when (val state = keyboardState.value) { - is Keyboard.Closed -> 0.dp - is Keyboard.Opened -> state.height + spaceBetweenKeyboard - } - } - - Box(modifier.padding(bottom = padding)) { content() } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt deleted file mode 100644 index a3ddaedf7e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/OutlinedSpinner.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import android.os.Handler -import android.os.Looper -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.core.os.postDelayed -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.common.form.Field -import com.tangem.tap.common.compose.TangemTextFieldsDefault -import com.tangem.tap.common.extensions.ValueCallback - -/** -[REDACTED_AUTHOR] - */ -@Suppress("MagicNumber") -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun OutlinedSpinner( - label: String, - itemList: List, - selectedItem: Field.Data, - onItemSelected: ValueCallback, - modifier: Modifier = Modifier, - textFieldConverter: (T) -> String = { it.toString() }, - dropdownItemView: @Composable ((T) -> Unit)? = null, - isEnabled: Boolean = true, - onClose: VoidCallback = {}, - closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(), -) { - val rIsExpanded = remember { mutableStateOf(false) } - val stateSelectedItem = remember { mutableStateOf(selectedItem.value) } - if (!selectedItem.isUserInput) { - stateSelectedItem.value = selectedItem.value - } - - val onDropDownItemSelectedInternal: (T) -> Unit = { - stateSelectedItem.value = it - rIsExpanded.value = false - onItemSelected(it) - } - val onDismissRequest = { - rIsExpanded.value = false - onClose() - } - - closePopupTrigger.close = { - onDismissRequest() - Handler(Looper.getMainLooper()).postDelayed(100) { - closePopupTrigger.onCloseComplete() - } - } - - ExposedDropdownMenuBox( - expanded = rIsExpanded.value, - onExpandedChange = { rIsExpanded.value = !rIsExpanded.value }, - ) { - OutlinedTextField( - modifier = modifier, - readOnly = true, - enabled = isEnabled, - value = textFieldConverter(stateSelectedItem.value), - onValueChange = {}, - label = { Text(label) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, - colors = TangemTextFieldsDefault.defaultTextFieldColors, - ) - - if (isEnabled) { - ExposedDropdownMenu(expanded = rIsExpanded.value, onDismissRequest = onDismissRequest) { - itemList.forEach { item -> - key(item) { - DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) { - if (dropdownItemView == null) Text(textFieldConverter(item)) else dropdownItemView(item) - } - } - } - } - } - } -} - -class ClosePopupTrigger { - var close: () -> Unit = {} - var onCloseComplete: () -> Unit = {} -} - -@Preview -@Composable -private fun TestSpinnerPreview() { - OutlinedSpinner( - label = "Blockchain name", - itemList = listOf(Blockchain.values()), - selectedItem = Field.Data(Blockchain.Avalanche, false), - onItemSelected = {}, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt deleted file mode 100644 index 441969ad5e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.domain.DomainDialog -import com.tangem.tap.common.compose.TitleSubtitle -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) { - SimpleDialog( - title = stringResource(id = R.string.custom_token_network_input_title), - items = dialog.items, - onSelect = dialog.onSelect, - onDismissRequest = onDismissRequest, - ) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt deleted file mode 100644 index 3ba171f427..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose.test - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Button -import androidx.compose.material.Divider -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.common.form.Field -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.redux.domainStore - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun ContractAddressTests(onItemClick: VoidCallback) { - val casesInfo = listOf( - "USDC on ETH" to "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - "BUSD on ETH" to "0x4fabb145d64652a948d72533023f6e7a623c7c53", - "ETH on AVALANCHE" to "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15", - "USDC on ETH (invalid - cut address)" to "0xa0b86991c6218b36c1d1", - "Custom EVM" to "0x1111111111111111112111111111111111111113", - "Supported by several networks" to "0xa1faa113cbe53436df28ff0aee54275c13b40975", - "Invalid" to "!@#_ _-%%^&&*((){P P2iOWsdfFQLA", - ) - CasesListContent(casesInfo, onItemClick) -} - -@Composable -fun SolanaAddressTests(onItemClick: VoidCallback) { - val casesInfo = listOf( - "USDT (full)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", - "USDT (valid - 2/3 of address)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben", - "USDT (invalid - 1/3 of address)" to "Es9vMFrzaCERmJ", - "ETH (full)" to "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk", - ) - CasesListContent(casesInfo, onItemClick) -} - -@Composable -private fun CasesListContent(casesList: List>, onItemClick: VoidCallback) { - LazyColumn( - content = { - item { - Row { - ResetContractAddressButton(onItemClick) - Text("", modifier = Modifier.weight(1f)) - ResetAllFieldsButton(onItemClick) - } - Divider() - } - items(casesList.size) { - val (info, address) = casesList[it] - ContractAddressButton(info, address, onItemClick) - } - }, - ) -} - -@Composable -fun ResetAllFieldsButton(onItemClick: VoidCallback) { - ActionButton( - name = "Reset", - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false))) - domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false))) - domainStore.dispatch( - AddCustomTokenAction.OnTokenDerivationPathChanged( - Field.Data( - Blockchain.Unknown, - false, - ), - ), - ) - }, - ) -} - -@Composable -fun ResetContractAddressButton(onItemClick: VoidCallback) { - ActionButton( - name = "Set empty address", - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false))) - }, - ) -} - -@Composable -private fun ContractAddressButton(name: String, address: String, onItemClick: VoidCallback) { - ActionButton( - modifier = Modifier.fillMaxWidth(), - name = name, - onClick = { - onItemClick() - domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false))) - }, - ) -} - -@Composable -fun ActionButton(name: String, onClick: () -> Unit, modifier: Modifier = Modifier) { - Button( - modifier = modifier.padding(horizontal = 8.dp), - onClick = onClick, - ) { Text(name, fontSize = 12.sp) } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt deleted file mode 100644 index 3bae26e193..0000000000 --- a/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.tap.features.customtoken.legacy.compose.test - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.unit.dp -import com.tangem.common.extensions.VoidCallback -import com.tangem.wallet.BuildConfig -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun TestCasesList(onItemClick: (TestCase) -> Unit) { - if (!BuildConfig.TEST_ACTION_ENABLED) return - - Surface(color = colorResource(id = R.color.lightGray5)) { - Column(Modifier.padding(horizontal = 16.dp)) { - listOf(TestCase.ContractAddress, TestCase.SolanaTokens) - .map { case -> TestCaseListItem(testCase = case, onItemClick = { onItemClick(case) }) } - } - } -} - -@Composable -fun TestCaseListItem(testCase: TestCase, onItemClick: () -> Unit) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = testCase.description, - ) - Button( - onClick = onItemClick, - ) { Text("Start") } - } -} - -enum class TestCase(val description: String, val content: @Composable (VoidCallback) -> Unit) { - ContractAddress("Test contract address field", { ContractAddressTests(it) }), - Auto("Test contract address field", { ContractAddressTests(it) }), - SolanaTokens("Test Solana contract addresses", { SolanaAddressTests(it) }), - ; -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/DarkThemeFeatureToggle.kt b/app/src/main/java/com/tangem/tap/features/details/DarkThemeFeatureToggle.kt new file mode 100644 index 0000000000..afb3010ad3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/DarkThemeFeatureToggle.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.details + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +class DarkThemeFeatureToggle( + private val featureTogglesManager: FeatureTogglesManager, +) { + val isDarkThemeEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "DARK_THEME_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DefaultDetailsFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DefaultDetailsFeatureToggles.kt new file mode 100644 index 0000000000..2ed04a4c94 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DefaultDetailsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.features.details.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultDetailsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : DetailsFeatureToggles { + + override val isRedesignedAppCurrencySelectorEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_APP_CURRENCY_SELECTOR_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureToggles.kt new file mode 100644 index 0000000000..dd1cd3bbdd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.features.details.featuretoggles + +interface DetailsFeatureToggles { + + val isRedesignedAppCurrencySelectorEnabled: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureTogglesModule.kt b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureTogglesModule.kt new file mode 100644 index 0000000000..2e6fd9874a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/featuretoggles/DetailsFeatureTogglesModule.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.features.details.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal object DetailsFeatureTogglesModule { + + @Provides + fun provideDetailsFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles { + return DefaultDetailsFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 1248e2537c..4bef087c68 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -1,7 +1,8 @@ package com.tangem.tap.features.details.redux import androidx.lifecycle.LifecycleCoroutineScope -import com.tangem.blockchain.common.Wallet +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -12,7 +13,7 @@ sealed class DetailsAction : Action { data class PrepareScreen( val scanResponse: ScanResponse, - val wallets: List, + val darkThemeSwitchEnabled: Boolean, ) : DetailsAction() object ReCreateTwinsWallet : DetailsAction() @@ -29,6 +30,14 @@ sealed class DetailsAction : Action { data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction() object ResetCardSettingsData : DetailsAction() + object ScanAndSaveUserWallet : DetailsAction() { + + object Success : DetailsAction() + + data class Error(val error: TextReference?) : DetailsAction() + } + + object DismissError : DetailsAction() sealed class AccessCodeRecovery : DetailsAction() { object Open : AccessCodeRecovery() @@ -72,6 +81,18 @@ sealed class DetailsAction : Action { data class BiometricsStatusChanged( val needEnrollBiometrics: Boolean, ) : AppSettings() + + data class ChangeAppThemeMode( + val appThemeMode: AppThemeMode, + ) : AppSettings() + + data class ChangeBalanceHiding( + val hideBalance: Boolean, + ) : AppSettings() + + data class ChangeAppCurrency( + val fiatCurrency: FiatCurrency, + ) : AppSettings() } data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index a672117ab2..1d0b13e4e3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux import androidx.lifecycle.LifecycleCoroutineScope import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess @@ -10,6 +11,11 @@ import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse @@ -19,6 +25,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.isLockedSync import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam +import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain @@ -33,6 +40,7 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.tangemSdkManager import com.tangem.wallet.R @@ -47,7 +55,7 @@ import timber.log.Timber class DetailsMiddleware { private val eraseWalletMiddleware = EraseWalletMiddleware() private val manageSecurityMiddleware = ManageSecurityMiddleware() - private val managePrivacyMiddleware = ManagePrivacyMiddleware() + private val appSettingsMiddleware = AppSettingsMiddleware() private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware() val detailsMiddleware: Middleware = { _, stateProvider -> { next -> @@ -67,43 +75,14 @@ class DetailsMiddleware { when (action) { is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state) - is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action) + is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action) is DetailsAction.ReCreateTwinsWallet -> { store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) } is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) - DetailsAction.ScanCard -> { - scope.launch { - store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) - .scan(allowsRequestAccessCodeFromRepository = true) - .doOnSuccess { scanResponse -> - // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards - store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) - val currentUserWalletId = state.scanResponse - ?.let { UserWalletIdBuilder.scanResponse(it).build() } - val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) - .build() - val isSameWallet = currentUserWalletId == scannedUserWalletId - - if (isSameWallet) { - store.dispatchOnMain( - DetailsAction.PrepareCardSettingsData( - scanResponse.card, - scanResponse.cardTypesResolver, - ), - ) - } else { - store.dispatchDialogShow( - AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.error_wrong_wallet_tapped, - ), - ) - } - } - } - } + is DetailsAction.ScanCard -> scanCard(state) + is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet() } } @@ -207,7 +186,7 @@ class DetailsMiddleware { } } - class ManagePrivacyMiddleware { + class AppSettingsMiddleware { fun handle(state: DetailsState, action: DetailsAction.AppSettings) { when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> { @@ -226,6 +205,17 @@ class DetailsMiddleware { is DetailsAction.AppSettings.EnrollBiometrics -> { enrollBiometrics() } + is DetailsAction.AppSettings.ChangeAppThemeMode -> { + changeAppThemeMode(action.appThemeMode) + } + is DetailsAction.AppSettings.ChangeBalanceHiding -> { + changeBalanceHiding(action.hideBalance) + } + is DetailsAction.AppSettings.ChangeAppCurrency -> { + store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency)) + store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency)) + store.dispatch(WalletSelectorAction.ChangeAppCurrency(action.fiatCurrency)) + } is DetailsAction.AppSettings.SwitchPrivacySetting.Success, is DetailsAction.AppSettings.SwitchPrivacySetting.Failure, is DetailsAction.AppSettings.BiometricsStatusChanged, @@ -261,6 +251,29 @@ class DetailsMiddleware { store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) } + private fun changeAppThemeMode(appThemeMode: AppThemeMode) { + val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository) + + scope.launch { + repository.changeAppThemeMode(appThemeMode) + + store.dispatchWithMain(GlobalAction.ChangeAppThemeMode(appThemeMode)) + } + } + + private fun changeBalanceHiding(hideBalance: Boolean) { + val repository = store.state.daggerGraphState.get(DaggerGraphState::balanceHidingRepository) + + scope.launch { + val newState = BalanceHidingSettings( + isHidingEnabledInSettings = hideBalance, + isBalanceHidden = false, + ) + + repository.storeBalanceHidingSettings(newState) + } + } + private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { // Nothing to change if (preferencesStorage.shouldSaveUserWallets == enable) { @@ -455,4 +468,97 @@ class DetailsMiddleware { } } } + + private fun scanCard(state: DetailsState) = scope.launch { + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + .scan(allowsRequestAccessCodeFromRepository = true) + .doOnSuccess { scanResponse -> + // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards + store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) + val currentUserWalletId = state.scanResponse + ?.let { UserWalletIdBuilder.scanResponse(it).build() } + val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse) + .build() + val isSameWallet = currentUserWalletId == scannedUserWalletId + + if (isSameWallet) { + store.dispatchOnMain( + DetailsAction.PrepareCardSettingsData( + scanResponse.card, + scanResponse.cardTypesResolver, + ), + ) + } else { + store.dispatchDialogShow( + AppDialog.SimpleOkDialogRes( + headerId = R.string.common_warning, + messageId = R.string.error_wrong_wallet_tapped, + ), + ) + } + } + } + + private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) { + val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + + val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() + + // Update access code policy for access code saving when a card was scanned + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, + ) + + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( + analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets), + onWalletNotCreated = { + // No need to rollback policy, continue with the policy set before the card scan + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + }, + disclaimerWillShow = { + store.dispatchOnMain(NavigationAction.PopBackTo()) + }, + onSuccess = { scanResponse -> + saveUserWalletAndPopBackToWalletScreen(scanResponse) + .doOnFailure { error -> + // Rollback policy if card saving was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + Timber.e(error, "Unable to save user wallet") + + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + } + }, + onFailure = { error -> + // Rollback policy if card scanning was failed + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + Timber.e(error, "Unable to scan card") + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference())) + }, + ) + } + + private suspend fun saveUserWalletAndPopBackToWalletScreen(scanResponse: ScanResponse): CompletionResult { + val userWallet = UserWalletBuilder(scanResponse).build() + ?: return CompletionResult.Failure(TangemSdkError.WalletIsNotCreated()) + + return userWalletsListManager.save(userWallet) + .doOnSuccess { + store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) + store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + + val walletFeatureToggles = store.state.daggerGraphState + .get(DaggerGraphState::walletFeatureToggles) + + if (!walletFeatureToggles.isRedesignedScreenEnabled) { + store.onUserWalletSelected(userWallet) + } + } + } + + private fun TangemError.toTextReference(): TextReference? { + if (silent) return null + + return messageResId?.let(::resourceReference) ?: stringReference(customMessage) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index e28b9cd398..6c464d6275 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -8,13 +8,15 @@ import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.preferencesStorage import com.tangem.tap.store import com.tangem.tap.tangemSdkManager +import kotlinx.coroutines.runBlocking import org.rekotlin.Action -import java.util.* +import java.util.EnumSet object DetailsReducer { fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state) } +@Suppress("CyclomaticComplexMethod") private fun internalReduce(action: Action, state: AppState): DetailsState { if (action !is DetailsAction) return state.detailsState val detailsState = state.detailsState @@ -39,9 +41,25 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { is DetailsAction.AppSettings -> { handlePrivacyAction(action, detailsState) } - is DetailsAction.ChangeAppCurrency -> - detailsState.copy(appCurrency = action.fiatCurrency) + is DetailsAction.ChangeAppCurrency -> detailsState.copy( + appSettingsState = detailsState.appSettingsState.copy( + selectedFiatCurrency = action.fiatCurrency, + ), + ) is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState) + is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy( + isScanningInProgress = true, + ) + is DetailsAction.ScanAndSaveUserWallet.Error -> detailsState.copy( + isScanningInProgress = false, + error = action.error, + ) + is DetailsAction.ScanAndSaveUserWallet.Success -> detailsState.copy( + isScanningInProgress = false, + ) + is DetailsAction.DismissError -> detailsState.copy( + error = null, + ) else -> detailsState } } @@ -49,13 +67,18 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState { return DetailsState( scanResponse = action.scanResponse, - wallets = action.wallets, createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, - appCurrency = store.state.globalState.appCurrency, appSettingsState = AppSettingsState( isBiometricsAvailable = tangemSdkManager.canUseBiometry, saveWallets = preferencesStorage.shouldSaveUserWallets, saveAccessCodes = preferencesStorage.shouldSaveAccessCodes, + selectedFiatCurrency = store.state.globalState.appCurrency, + selectedThemeMode = store.state.globalState.appThemeMode, + isHidingEnabled = runBlocking { + store.state.daggerGraphState + .get { balanceHidingRepository }.getBalanceHidingSettings().isHidingEnabledInSettings + }, + darkThemeSwitchEnabled = action.darkThemeSwitchEnabled, ), ) } @@ -194,6 +217,21 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail needEnrollBiometrics = action.needEnrollBiometrics, ), ) + is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy( + appSettingsState = state.appSettingsState.copy( + selectedThemeMode = action.appThemeMode, + ), + ) + is DetailsAction.AppSettings.ChangeAppCurrency -> state.copy( + appSettingsState = state.appSettingsState.copy( + selectedFiatCurrency = action.fiatCurrency, + ), + ) + is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy( + appSettingsState = state.appSettingsState.copy( + isHidingEnabled = action.hideBalance, + ), + ) is DetailsAction.AppSettings.EnrollBiometrics, is DetailsAction.AppSettings.CheckBiometricsStatus, -> state diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 6fd17a4119..68c663534e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -1,20 +1,21 @@ package com.tangem.tap.features.details.redux -import com.tangem.blockchain.common.Wallet +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.FiatCurrency import org.rekotlin.StateType -import java.util.* +import java.util.EnumSet data class DetailsState( val scanResponse: ScanResponse? = null, - val wallets: List = emptyList(), val cardSettingsState: CardSettingsState? = null, val privacyPolicyUrl: String? = null, val createBackupAllowed: Boolean = false, - val appCurrency: FiatCurrency = FiatCurrency.Default, + val isScanningInProgress: Boolean = false, + val error: TextReference? = null, val appSettingsState: AppSettingsState = AppSettingsState(), ) : StateType @@ -56,7 +57,11 @@ data class AppSettingsState( val saveAccessCodes: Boolean = false, val isBiometricsAvailable: Boolean = false, val needEnrollBiometrics: Boolean = false, + val isHidingEnabled: Boolean = false, val isInProgress: Boolean = false, + val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default, + val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT, + val darkThemeSwitchEnabled: Boolean = false, ) enum class SecurityOption { LongTap, PassCode, AccessCode } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt new file mode 100644 index 0000000000..b9273ef99b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class AppCurrencySelectorFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + private val viewModel: AppCurrencySelectorViewModel by viewModels() + + @Composable + override fun ScreenContent(modifier: Modifier) { + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + AppCurrencySelectorScreen( + modifier = modifier, + state = uiState, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorIntents.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorIntents.kt new file mode 100644 index 0000000000..b6d25d03a0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorIntents.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.details.ui.appcurrency + +internal interface AppCurrencySelectorIntents { + + fun onBackClick() + + fun onSearchClick() + + fun onSearchInputChange(input: String) + + fun onCurrencyClick(currency: AppCurrencySelectorState.Currency) + + fun onDismissSearchClick() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt new file mode 100644 index 0000000000..a38cac5c0a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -0,0 +1,363 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +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.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState.Currency +import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier: Modifier = Modifier) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val listState = rememberLazyListState() + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TopBar( + modifier = Modifier.fillMaxWidth(), + scrollBehavior = scrollBehavior, + state = state, + ) + }, + content = { paddingValues -> + val contentModifier = Modifier + .padding(paddingValues) + .fillMaxSize() + + when (state) { + is AppCurrencySelectorState.Loading -> LoadingList( + modifier = contentModifier, + ) + is AppCurrencySelectorState.Content -> CurrenciesList( + modifier = contentModifier, + listState = listState, + currencies = state.items, + selectedId = state.selectedId.orEmpty(), + onCurrencyClick = state.onCurrencyClick, + ) + } + }, + ) + + if (state is AppCurrencySelectorState.Content) { + EventEffect(event = state.scrollToSelected) { + listState.scrollToItem(it) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + scrollBehavior: TopAppBarScrollBehavior, + state: AppCurrencySelectorState, + modifier: Modifier = Modifier, +) { + TopAppBar( + modifier = modifier, + scrollBehavior = scrollBehavior, + colors = TopAppBarColors, + navigationIcon = { + IconButton( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing8) + .size(TangemTheme.dimens.size32), + onClick = state.onBackClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_back_24), + contentDescription = null, + ) + } + }, + title = { + when (state) { + is AppCurrencySelectorState.Search -> SearchBar( + modifier = Modifier.fillMaxWidth(), + onInputChange = state.onSearchInputChange, + ) + is AppCurrencySelectorState.Loading, + is AppCurrencySelectorState.Default, + -> Text( + text = stringResource(id = R.string.details_row_title_currency), + style = TangemTheme.typography.subtitle1, + ) + } + }, + actions = { + when (state) { + is AppCurrencySelectorState.Content -> { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = state.onTopBarActionClick, + ) { + val iconResId = when (state) { + is AppCurrencySelectorState.Default -> R.drawable.ic_search_24 + is AppCurrencySelectorState.Search -> R.drawable.ic_close_24 + } + val iconTint = when (state) { + is AppCurrencySelectorState.Default -> TangemTheme.colors.icon.primary1 + is AppCurrencySelectorState.Search -> TangemTheme.colors.icon.informative + } + + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = iconResId), + tint = iconTint, + contentDescription = null, + ) + } + } + is AppCurrencySelectorState.Loading -> Unit + } + SpacerW(width = TangemTheme.dimens.spacing8) + }, + ) +} + +@Composable +private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modifier) { + val focusRequester = remember { FocusRequester() } + var input by remember { mutableStateOf(value = "") } + + TextField( + modifier = modifier + .focusRequester(focusRequester), + value = input, + onValueChange = { input = it }, + singleLine = true, + textStyle = TangemTheme.typography.subtitle2, + placeholder = { + Text(text = stringResource(id = R.string.common_search)) + }, + colors = SearchBarColors, + ) + + LaunchedEffect(key1 = input) { + onInputChange(input) + } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } +} + +@Composable +private fun LoadingList(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + repeat(times = 10) { + Row( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing24, + ) + .height(TangemTheme.dimens.size56) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing16, + alignment = Alignment.Start, + ), + ) { + CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size24)) + RectangleShimmer( + modifier = Modifier + .height(TangemTheme.dimens.size24) + .fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun CurrenciesList( + listState: LazyListState, + currencies: ImmutableList, + selectedId: String, + onCurrencyClick: (Currency) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier, + state = listState, + ) { + items( + items = currencies, + key = Currency::id, + ) { currency -> + val onClick = remember(key1 = currency) { + { onCurrencyClick(currency) } + } + + CurrencyItem( + modifier = Modifier.fillMaxWidth(), + name = currency.name, + isSelected = currency.id == selectedId, + onClick = onClick, + ) + } + } +} + +@Composable +private fun CurrencyItem(name: String, isSelected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing24, + ) + .heightIn(min = TangemTheme.dimens.size56), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing16, + alignment = Alignment.Start, + ), + ) { + RadioButton( + modifier = Modifier.size(TangemTheme.dimens.size24), + selected = isSelected, + onClick = onClick, + interactionSource = interactionSource, + colors = RadioButtonColors, + ) + Text( + text = name, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +private val TopAppBarColors: TopAppBarColors + @Composable + get() = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + // Currently (08.09.23) it's not working when scrolling programmatically + scrolledContainerColor = TangemTheme.colors.background.secondary, + navigationIconContentColor = TangemTheme.colors.icon.primary1, + titleContentColor = TangemTheme.colors.text.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ) + +private val SearchBarColors: TextFieldColors + @Composable + get() = TextFieldDefaults.colors( + unfocusedContainerColor = TangemTheme.colors.background.secondary, + focusedContainerColor = TangemTheme.colors.background.secondary, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.secondary, + focusedPlaceholderColor = TangemTheme.colors.text.disabled, + unfocusedPlaceholderColor = TangemTheme.colors.text.disabled, + focusedIndicatorColor = TangemTheme.colors.background.secondary, + unfocusedIndicatorColor = TangemTheme.colors.background.secondary, + cursorColor = TangemTheme.colors.icon.primary1, + ) + +private val RadioButtonColors: RadioButtonColors + @Composable + get() = RadioButtonDefaults.colors( + selectedColor = TangemTheme.colors.icon.accent, + unselectedColor = TangemTheme.colors.icon.secondary, + ) + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +private fun AppCurrencySelectorScreenPreview_Light( + @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, +) { + TangemTheme(isDark = false) { + AppCurrencySelectorScreen(param) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Composable +private fun AppCurrencySelectorScreenPreview_Dark( + @PreviewParameter(AppCurrencySelectorStateProvider::class) param: AppCurrencySelectorState, +) { + TangemTheme(isDark = true) { + AppCurrencySelectorScreen(param) + } +} + +private class AppCurrencySelectorStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + val items = listOf( + "US Dollar (USD) – $", + "Inited Arab Emirates Dirham (AED) – DH", + "Argentine Peso (ARS) – $", + "Australian Dollar (AUD) – A$", + "Bangladeshi Taka (BDT) – ৳", + "Bahraini Dinar (BHD) – BD", + "Bermudian Dollar (BMD) – $", + "Brazil Real (BRL) – R$", + "Canadian Dollar (CAD) – CA$", + "Swiss Franc (CHF) – Fr", + "Chilean Peso (CLP) – CLP$", + "Chinese Yan (CNY)", + ) + .mapIndexed { index, s -> Currency(index.toString(), s) } + .toPersistentList() + + AppCurrencySelectorState.Loading(onBackClick = {}).let(::add) + AppCurrencySelectorState.Default( + selectedId = "0", + items = items, + scrollToSelected = consumedEvent(), + onCurrencyClick = {}, + onBackClick = {}, + onTopBarActionClick = {}, + ).let(::add) + AppCurrencySelectorState.Search( + selectedId = "0", + items = items, + scrollToSelected = consumedEvent(), + onCurrencyClick = {}, + onBackClick = {}, + onSearchInputChange = {}, + onTopBarActionClick = {}, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt new file mode 100644 index 0000000000..b3b606c396 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorState.kt @@ -0,0 +1,71 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.event.StateEvent +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal sealed class AppCurrencySelectorState { + + abstract val onBackClick: () -> Unit + + data class Loading( + override val onBackClick: () -> Unit, + ) : AppCurrencySelectorState() + + @Immutable + sealed class Content : AppCurrencySelectorState() { + abstract val selectedId: String + abstract val scrollToSelected: StateEvent + abstract val items: PersistentList + abstract val onCurrencyClick: (Currency) -> Unit + abstract val onTopBarActionClick: () -> Unit + + fun copySealed( + selectedId: String? = this.selectedId, + items: PersistentList = this.items, + scrollToSelected: StateEvent = this.scrollToSelected, + onCurrencyClick: (Currency) -> Unit = this.onCurrencyClick, + onTopBarActionClick: () -> Unit = this.onTopBarActionClick, + ): AppCurrencySelectorState = when (this) { + is Default -> this.copy( + selectedId = selectedId.orEmpty(), + items = items, + scrollToSelected = scrollToSelected, + onCurrencyClick = onCurrencyClick, + onTopBarActionClick = onTopBarActionClick, + ) + is Search -> this.copy( + selectedId = selectedId.orEmpty(), + items = items, + scrollToSelected = scrollToSelected, + onCurrencyClick = onCurrencyClick, + onTopBarActionClick = onTopBarActionClick, + ) + } + } + + data class Default( + override val selectedId: String, + override val items: PersistentList, + override val onCurrencyClick: (Currency) -> Unit, + override val onBackClick: () -> Unit, + override val onTopBarActionClick: () -> Unit, + override val scrollToSelected: StateEvent, + ) : Content() + + data class Search( + override val selectedId: String, + override val items: PersistentList, + override val scrollToSelected: StateEvent, + override val onCurrencyClick: (Currency) -> Unit, + override val onBackClick: () -> Unit, + override val onTopBarActionClick: () -> Unit, + val onSearchInputChange: (String) -> Unit, + ) : Content() + + data class Currency( + val id: String, + val name: String, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorStateHolder.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorStateHolder.kt new file mode 100644 index 0000000000..6f091f4376 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorStateHolder.kt @@ -0,0 +1,125 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.tap.features.details.ui.appcurrency.converter.CurrencyConverter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +internal class AppCurrencySelectorStateHolder( + private val intents: AppCurrencySelectorIntents, + private val onSubscription: () -> Unit, + stateFlowScope: CoroutineScope, +) { + + private var availableCurrencies: PersistentList = persistentListOf() + + private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) + + private val currencyConverter by lazy(mode = LazyThreadSafetyMode.NONE) { CurrencyConverter() } + + val stateFlow: StateFlow = stateFlowInternal + .onSubscription { onSubscription() } + .stateIn( + scope = stateFlowScope, + started = SharingStarted.WhileSubscribed(), + initialValue = getInitialState(), + ) + + fun updateStateWithAvailableCurrencies(currencies: List) { + availableCurrencies = currencyConverter.convertList(currencies).toPersistentList() + + updateState { + when (this) { + is AppCurrencySelectorState.Loading -> getContentState(availableCurrencies) + is AppCurrencySelectorState.Content -> copySealed(items = availableCurrencies) + } + } + } + + fun updateStateWithSelectedCurrency(selectedCurrency: AppCurrency, selectedCurrencyIndex: Int) { + val selectedCurrencyId = selectedCurrency.code + + updateState { + when (this) { + is AppCurrencySelectorState.Loading -> this + is AppCurrencySelectorState.Content -> copySealed( + selectedId = selectedCurrencyId, + scrollToSelected = triggeredEvent(selectedCurrencyIndex, ::consumeScrollToSelectedEvent), + ) + } + } + } + + fun updateStateWithSearch(input: String = "") { + val filteredItems = availableCurrencies.mutate { list -> + list.removeAll { input.lowercase() !in it.name.lowercase() } + } + + updateState { + when (this) { + is AppCurrencySelectorState.Loading -> this + is AppCurrencySelectorState.Search -> copy(items = filteredItems) + is AppCurrencySelectorState.Default -> getSearchState(filteredItems, selectedId) + } + } + } + + fun updateStateWithoutSearch() { + updateState { + when (this) { + is AppCurrencySelectorState.Search -> getContentState(availableCurrencies, selectedId) + is AppCurrencySelectorState.Default, + is AppCurrencySelectorState.Loading, + -> this + } + } + } + + private fun getInitialState() = AppCurrencySelectorState.Loading( + onBackClick = intents::onBackClick, + ) + + private fun getContentState( + items: PersistentList, + selectedCurrencyId: String = "", + ) = AppCurrencySelectorState.Default( + selectedId = selectedCurrencyId, + items = items, + onBackClick = intents::onBackClick, + onCurrencyClick = intents::onCurrencyClick, + onTopBarActionClick = intents::onSearchClick, + scrollToSelected = consumedEvent(), + ) + + private fun getSearchState( + filteredItems: PersistentList, + selectedCurrencyId: String, + ) = AppCurrencySelectorState.Search( + selectedId = selectedCurrencyId, + items = filteredItems, + scrollToSelected = consumedEvent(), + onBackClick = intents::onBackClick, + onCurrencyClick = intents::onCurrencyClick, + onSearchInputChange = intents::onSearchInputChange, + onTopBarActionClick = intents::onDismissSearchClick, + ) + + private inline fun updateState(block: AppCurrencySelectorState.() -> AppCurrencySelectorState) { + stateFlowInternal.update(block) + } + + private fun consumeScrollToSelectedEvent() { + updateState { + when (this) { + is AppCurrencySelectorState.Loading -> this + is AppCurrencySelectorState.Content -> copySealed(scrollToSelected = consumedEvent()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt new file mode 100644 index 0000000000..4013947c19 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt @@ -0,0 +1,74 @@ +package com.tangem.tap.features.details.ui.appcurrency + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.navigation.ReduxNavController +import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class AppCurrencySelectorViewModel @Inject constructor( + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase, + private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase, + private val reduxNavController: ReduxNavController, + private val dispatchers: CoroutineDispatcherProvider, +) : ViewModel(), AppCurrencySelectorIntents { + + private val stateController = AppCurrencySelectorStateHolder( + intents = this, + onSubscription = { fetchCurrencies() }, + stateFlowScope = viewModelScope, + ) + + val uiState: StateFlow = stateController.stateFlow + + override fun onBackClick() { + reduxNavController.popBackStack() + } + + override fun onSearchClick() { + stateController.updateStateWithSearch() + } + + override fun onSearchInputChange(input: String) { + stateController.updateStateWithSearch(input) + } + + override fun onCurrencyClick(currency: AppCurrencySelectorState.Currency) { + viewModelScope.launch(dispatchers.io) { + selectAppCurrencyUseCase(currency.id) + .onRight { reduxNavController.popBackStack() } + } + } + + override fun onDismissSearchClick() { + stateController.updateStateWithoutSearch() + } + + private fun fetchCurrencies() { + viewModelScope.launch(dispatchers.io) { + val availableCurrencies = getAvailableCurrenciesUseCase() + .onRight(stateController::updateStateWithAvailableCurrencies) + .getOrNull() + + getSelectedAppCurrencyUseCase().collectLatest { maybeSelectedCurrency -> + maybeSelectedCurrency + .onRight { selectedCurrency -> + val selectedCurrencyIndex = availableCurrencies?.indexOfFirst { it == selectedCurrency } + + if (selectedCurrencyIndex != null && selectedCurrencyIndex != -1) { + stateController.updateStateWithSelectedCurrency(selectedCurrency, selectedCurrencyIndex) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt new file mode 100644 index 0000000000..9e6df0ed22 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/converter/CurrencyConverter.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.details.ui.appcurrency.converter + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorState +import com.tangem.utils.converter.Converter + +internal class CurrencyConverter : Converter { + + override fun convert(value: AppCurrency): AppCurrencySelectorState.Currency { + val fullCurrencyName = with(value) { "$name ($code) - $symbol" } + + return AppCurrencySelectorState.Currency(value.code, fullCurrencyName) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt new file mode 100644 index 0000000000..43e66b065a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.features.details.ui.appsettings + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog +import com.tangem.wallet.R +import kotlinx.collections.immutable.toImmutableList + +internal class AppSettingsDialogsFactory { + + fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message), + confirmText = resourceReference(R.string.common_delete), + onConfirm = onDelete, + onDismiss = onDismiss, + ) + } + + fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { + return Dialog.Alert( + title = resourceReference(R.string.common_attention), + description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message), + confirmText = resourceReference(R.string.common_delete), + onConfirm = onDelete, + onDismiss = onDismiss, + ) + } + + fun createThemeModeSelectorDialog( + selectedModeIndex: Int, + onSelect: (AppThemeMode) -> Unit, + onDismiss: () -> Unit, + ): Dialog.Selector { + val modes = AppThemeMode.available + + return Dialog.Selector( + title = resourceReference(R.string.app_settings_theme_selector_title), + selectedItemIndex = selectedModeIndex, + items = modes.map { mode -> + resourceReference( + id = when (mode) { + AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark + AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light + AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system + }, + ) + }.toImmutableList(), + onSelect = { index -> + val mode = AppThemeMode.available[index] + + onSelect(mode) + }, + onDismiss = onDismiss, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 2be6f7b037..0e0911e303 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -1,74 +1,86 @@ package com.tangem.tap.features.details.ui.appsettings import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class AppSettingsFragment : Fragment(), StoreSubscriber { - private val viewModel = AppSettingsViewModel(store) - private var screenState: MutableState = - mutableStateOf(viewModel.updateState(store.state.detailsState)) +@AndroidEntryPoint +internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.fade) - exitTransition = inflater.inflateTransition(R.transition.fade) - viewModel.checkBiometricsStatus(lifecycleScope) - } + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - AppSettingsScreen( - state = screenState.value, - onBackClick = { - store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) - }, - ) - } + @Inject + lateinit var detailsFeatureToggles: DetailsFeatureToggles + + @Inject + lateinit var appCurrencyRepository: AppCurrencyRepository + + private val viewModel by lazy(mode = LazyThreadSafetyMode.NONE) { + AppSettingsViewModel(store, detailsFeatureToggles, appCurrencyRepository) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.fade) + exitTransition = inflater.inflateTransition(R.transition.fade) + viewModel.checkBiometricsStatus(lifecycleScope) + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + AppSettingsScreen( + modifier = modifier, + state = viewModel.uiState, + onBackClick = { + store.dispatch(DetailsAction.ResetCardSettingsData) + store.dispatch(NavigationAction.PopBackTo()) + }, + ) + } + + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.detailsState == newState.detailsState + }.select { it.detailsState } } } - } - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } + override fun onResume() { + super.onResume() + viewModel.refreshBiometricsStatus(lifecycleScope) } - } - override fun onResume() { - super.onResume() - viewModel.refreshBiometricsStatus(lifecycleScope) - } + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } - override fun onStop() { - super.onStop() - store.unsubscribe(this) - } - - override fun newState(state: DetailsState) { - if (activity == null || view == null) return - screenState.value = viewModel.updateState(state) - } -} \ No newline at end of file + override fun newState(state: DetailsState) { + if (activity == null || view == null) return + viewModel.updateState(state) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt new file mode 100644 index 0000000000..0d1f66f8bb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -0,0 +1,91 @@ +package com.tangem.tap.features.details.ui.appsettings + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item +import com.tangem.wallet.R + +internal class AppSettingsItemsFactory { + + fun createEnrollBiometricsCard(onClick: () -> Unit): Item.Card { + return Item.Card( + id = "enroll_biometrics_card", + title = resourceReference(R.string.app_settings_enable_biometrics_title), + description = resourceReference(R.string.app_settings_enable_biometrics_description), + iconResId = R.drawable.ic_alert_circle_24, + onClick = onClick, + ) + } + + fun createSaveWalletsSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = "save_wallets_switch", + title = resourceReference(R.string.app_settings_saved_wallet), + description = resourceReference(R.string.app_settings_saved_wallet_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createSaveAccessCodeSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = "save_access_codes_switch", + title = resourceReference(R.string.app_settings_saved_access_codes), + description = resourceReference(R.string.app_settings_saved_access_codes_footer), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createFlipToHideBalanceSwitch( + isChecked: Boolean, + isEnabled: Boolean, + onCheckedChange: (Boolean) -> Unit, + ): Item.Switch { + return Item.Switch( + id = "flip_to_hide_balance_switch", + title = resourceReference(R.string.details_row_title_flip_to_hide), + description = resourceReference(R.string.details_row_description_flip_to_hide), + isEnabled = isEnabled, + isChecked = isChecked, + onCheckedChange = onCheckedChange, + ) + } + + fun createSelectAppCurrencyButton(currentAppCurrencyName: String, onClick: () -> Unit): Item.Button { + return Item.Button( + id = "select_app_currency_button", + title = resourceReference(R.string.details_row_title_currency), + description = stringReference(currentAppCurrencyName), + isEnabled = true, + onClick = onClick, + ) + } + + fun createSelectThemeModeButton(currentThemeMode: AppThemeMode, onClick: () -> Unit): Item.Button { + return Item.Button( + id = "select_theme_mode_button", + title = resourceReference(R.string.app_settings_theme_selector_title), + description = resourceReference( + id = when (currentThemeMode) { + AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark + AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light + AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system + }, + ), + isEnabled = true, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 9255526f4a..7c99ed8b24 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,233 +1,110 @@ package com.tangem.tap.features.details.ui.appsettings -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.Text +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerW32 +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.tap.features.details.redux.AppSetting -import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard -import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item +import com.tangem.tap.features.details.ui.appsettings.components.* import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold -import com.tangem.tap.features.details.ui.common.TangemSwitch import com.tangem.wallet.R +import kotlinx.collections.immutable.persistentListOf @Composable -fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit) { +internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( - content = { AppSettings(state = state) }, + modifier = modifier, + content = { + when (state) { + is AppSettingsScreenState.Content -> AppSettings(state = state) + is AppSettingsScreenState.Loading -> Unit + } + }, titleRes = R.string.app_settings_title, onBackClick = onBackClick, ) } @Composable -private fun AppSettings(state: AppSettingsScreenState) { - var dialogType by remember { mutableStateOf(null) } - val onDialogStateChange: (AppSetting?) -> Unit = { dialogType = it } - - dialogType?.let { - SettingsAlertDialog( - element = it, - onDialogStateChange = onDialogStateChange, - onSettingToggle = { state.onSettingToggled(it, false) }, - ) +private fun AppSettings(state: AppSettingsScreenState.Content) { + val dialog by rememberUpdatedState(newValue = state.dialog) + when (val safeDialog = dialog) { + is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog) + is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog) + null -> Unit } - Column(modifier = Modifier.fillMaxSize()) { - if (state.showEnrollBiometricsCard) { - EnrollBiometricsCard(onClick = state.onEnrollBiometrics) - SpacerH24() - } - - AppSettingsElement( - state = state, - setting = AppSetting.SaveWallets, - onDialogStateChange = onDialogStateChange, - ) - SpacerH32() - AppSettingsElement( - state = state, - setting = AppSetting.SaveAccessCode, - onDialogStateChange = onDialogStateChange, - ) - } -} - -@Suppress("LongMethod") -@Composable -private fun AppSettingsElement( - state: AppSettingsScreenState, - setting: AppSetting, - onDialogStateChange: (AppSetting?) -> Unit, -) { - val titleRes = when (setting) { - AppSetting.SaveWallets -> R.string.app_settings_saved_wallet - AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes - } - val subtitleRes = when (setting) { - AppSetting.SaveWallets -> R.string.app_settings_saved_wallet_footer - AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer - } - val checked = state.settings[setting] ?: false - - val titleTextColor by rememberUpdatedState( - newValue = if (state.isTogglesEnabled) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.secondary - }, - ) - val descriptionTextColor by rememberUpdatedState( - newValue = if (state.isTogglesEnabled) { - TangemTheme.colors.text.secondary - } else { - TangemTheme.colors.text.tertiary - }, - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing20), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column( - modifier = Modifier.weight(weight = .9f), - verticalArrangement = Arrangement.Center, - ) { - Text( - text = stringResource(id = titleRes), - style = TangemTheme.typography.subtitle1, - color = titleTextColor, - ) - SpacerH4() - Text( - text = stringResource(id = subtitleRes), - style = TangemTheme.typography.body2, - color = descriptionTextColor, - ) - } - SpacerW32() - TangemSwitch( - checked = checked, - enabled = state.isTogglesEnabled, - onCheckedChange = { isChecked -> - onCheckedChange( - element = setting, - enabled = isChecked, - onSettingToggled = state.onSettingToggled, - onDialogStateChange = onDialogStateChange, + LazyColumn { + items( + items = state.items, + key = Item::id, + ) { item -> + when (item) { + is Item.Card -> SettingsCardItem( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + item = item, ) - }, - ) - } -} - -private fun onCheckedChange( - element: AppSetting, - enabled: Boolean, - onSettingToggled: (AppSetting, Boolean) -> Unit, - onDialogStateChange: (AppSetting?) -> Unit, -) { - // Show warning if user wants to disable the switch - if (!enabled) { - onDialogStateChange(element) - } else { - onSettingToggled(element, true) + is Item.Button -> SettingsButtonItem( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8), + item = item, + ) + is Item.Switch -> SettingsSwitchItem( + modifier = Modifier.padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing20, + ), + item = item, + ) + } + } } } // region Preview -@Composable -private fun AppSettingsScreenSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - AppSettingsScreen( - state = AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to true, - AppSetting.SaveAccessCode to false, - ), - showEnrollBiometricsCard = false, - isTogglesEnabled = true, - onSettingToggled = { _, _ -> }, - onEnrollBiometrics = {}, - ), - onBackClick = { }, - ) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun AppSettingsScreenPreview_Light() { +private fun AppSettingsScreenPreview_Light( + @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, +) { TangemTheme { - AppSettingsScreenSample() + AppSettingsScreen(state = state, onBackClick = {}) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun AppSettingsScreenPreview_Dark() { +private fun AppSettingsScreenPreview_Dark( + @PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState, +) { TangemTheme(isDark = true) { - AppSettingsScreenSample() + AppSettingsScreen(state = state, onBackClick = {}) } } -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Sample(modifier: Modifier = Modifier) { - Column(modifier = modifier.background(TangemTheme.colors.background.primary)) { - AppSettingsScreen( - state = AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to true, - AppSetting.SaveAccessCode to false, - ), - showEnrollBiometricsCard = true, - isTogglesEnabled = false, - onSettingToggled = { _, _ -> }, - onEnrollBiometrics = {}, - ), - onBackClick = { }, +private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + val items = persistentListOf( + itemsFactory.createEnrollBiometricsCard {}, + itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {}, + itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }), + itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> }, + itemsFactory.createFlipToHideBalanceSwitch(isChecked = false, isEnabled = true) { _ -> }, + itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}), ) - } -} -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() { - TangemTheme { - AppSettingsScreen_EnrollBiometrics_Sample() - } -} - -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() { - TangemTheme(isDark = true) { - AppSettingsScreen_EnrollBiometrics_Sample() - } -} + AppSettingsScreenState.Content( + items = items, + dialog = null, + ).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt index fbc4640e8f..108b30fec8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -1,11 +1,70 @@ package com.tangem.tap.features.details.ui.appsettings -import com.tangem.tap.features.details.redux.AppSetting +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList -data class AppSettingsScreenState( - val settings: Map = emptyMap(), - val showEnrollBiometricsCard: Boolean = false, - val isTogglesEnabled: Boolean = false, - val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ }, - val onEnrollBiometrics: () -> Unit = { /* no-op */ }, -) \ No newline at end of file +@Immutable +internal sealed class AppSettingsScreenState { + + object Loading : AppSettingsScreenState() + + data class Content( + val items: ImmutableList, + val dialog: Dialog?, + ) : AppSettingsScreenState() + + @Immutable + sealed class Item { + + abstract val id: String + + data class Card( + override val id: String, + @DrawableRes val iconResId: Int, + val title: TextReference, + val description: TextReference, + val onClick: () -> Unit, + ) : Item() + + data class Switch( + override val id: String, + val title: TextReference, + val description: TextReference, + val isEnabled: Boolean, + val isChecked: Boolean, + val onCheckedChange: (Boolean) -> Unit, + ) : Item() + + data class Button( + override val id: String, + val title: TextReference, + val description: TextReference, + val isEnabled: Boolean, + val onClick: () -> Unit, + ) : Item() + } + + @Immutable + sealed class Dialog { + + abstract val onDismiss: () -> Unit + + data class Alert( + val title: TextReference, + val description: TextReference, + val confirmText: TextReference, + val onConfirm: () -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog() + + data class Selector( + val title: TextReference, + val selectedItemIndex: Int, + val items: ImmutableList, + val onSelect: (Int) -> Unit, + override val onDismiss: () -> Unit, + ) : Dialog() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 857ce03b4d..c76eaafa65 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -1,36 +1,57 @@ package com.tangem.tap.features.details.ui.appsettings +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.lifecycle.LifecycleCoroutineScope +import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles import com.tangem.tap.features.details.redux.AppSetting +import com.tangem.tap.features.details.redux.AppSettingsState import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.scope +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.rekotlin.Store -class AppSettingsViewModel(private val store: Store) { +internal class AppSettingsViewModel( + private val store: Store, + private val detailsFeatureToggles: DetailsFeatureToggles, + private val appCurrencyRepository: AppCurrencyRepository, +) { - fun updateState(state: DetailsState): AppSettingsScreenState { - return with(state.appSettingsState) { - AppSettingsScreenState( - settings = mapOf( - AppSetting.SaveWallets to saveWallets, - AppSetting.SaveAccessCode to saveAccessCodes, - ), - showEnrollBiometricsCard = needEnrollBiometrics, - isTogglesEnabled = !needEnrollBiometrics && !isInProgress, - onSettingToggled = { privacySetting, enabled -> - onSettingsToggled(privacySetting, enabled) - }, - onEnrollBiometrics = { - store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) - }, - ) + private val itemsFactory = AppSettingsItemsFactory() + private val dialogsFactory = AppSettingsDialogsFactory() + + private val appCurrencyUpdatesJobHolder = JobHolder() + + var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading) + private set + + init { + if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) { + bootstrapAppCurrencyUpdates() } } - private fun onSettingsToggled(setting: AppSetting, enable: Boolean) { - store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) + fun updateState(state: DetailsState) { + uiState = AppSettingsScreenState.Content( + items = buildItems(state.appSettingsState), + dialog = (uiState as? AppSettingsScreenState.Content)?.dialog, + ) } fun checkBiometricsStatus(lifecycleScope: LifecycleCoroutineScope) { @@ -50,4 +71,146 @@ class AppSettingsViewModel(private val store: Store) { ), ) } + + private fun buildItems(state: AppSettingsState): ImmutableList { + val items = buildList { + if (state.needEnrollBiometrics) { + itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics).let(::add) + } + + itemsFactory.createSelectAppCurrencyButton( + currentAppCurrencyName = state.selectedFiatCurrency.name, + onClick = ::showAppCurrencySelector, + ).let(::add) + + if (state.isBiometricsAvailable) { + val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress + + itemsFactory.createSaveWalletsSwitch( + isChecked = state.saveWallets, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveWalletsToggled, + ).let(::add) + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = state.saveAccessCodes, + isEnabled = canUseBiometrics, + onCheckedChange = ::onSaveAccessCodesToggled, + ).let(::add) + } + + itemsFactory.createFlipToHideBalanceSwitch( + isChecked = state.isHidingEnabled, + isEnabled = true, + onCheckedChange = ::onFlipToHideBalanceToggled, + ).let(::add) + + itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) { + showThemeModeSelector(state.selectedThemeMode) + }.let(::add) + + if (state.darkThemeSwitchEnabled) { + itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) { + showThemeModeSelector(state.selectedThemeMode) + }.let(::add) + } + } + + return items.toImmutableList() + } + + private fun enrollBiometrics() { + store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) + } + + private fun showAppCurrencySelector() { + val action = if (detailsFeatureToggles.isRedesignedAppCurrencySelectorEnabled) { + NavigationAction.NavigateTo(AppScreen.AppCurrencySelector) + } else { + WalletAction.AppCurrencyAction.ChooseAppCurrency + } + + store.dispatchOnMain(action) + } + + private fun showThemeModeSelector(selectedMode: AppThemeMode) { + updateContentState { + copy( + dialog = dialogsFactory.createThemeModeSelectorDialog( + selectedModeIndex = selectedMode.ordinal, + onSelect = { mode -> + store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode)) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + + private fun onSaveWalletsToggled(isChecked: Boolean) { + if (isChecked) { + onSettingsToggled(AppSetting.SaveWallets, enable = true) + } else { + updateContentState { + copy( + dialog = dialogsFactory.createDeleteSavedWalletsAlert( + onDelete = { + onSettingsToggled(AppSetting.SaveWallets, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + + private fun onSaveAccessCodesToggled(isChecked: Boolean) { + if (isChecked) { + onSettingsToggled(AppSetting.SaveAccessCode, enable = true) + } else { + updateContentState { + copy( + dialog = dialogsFactory.createDeleteSavedAccessCodesAlert( + onDelete = { + onSettingsToggled(AppSetting.SaveAccessCode, enable = false) + dismissDialog() + }, + onDismiss = ::dismissDialog, + ), + ) + } + } + } + + private fun onSettingsToggled(setting: AppSetting, enable: Boolean) { + store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) + } + + private fun onFlipToHideBalanceToggled(enable: Boolean) { + store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(hideBalance = enable)) + } + + private fun dismissDialog() { + updateContentState { copy(dialog = null) } + } + + private fun bootstrapAppCurrencyUpdates() { + appCurrencyRepository + .getSelectedAppCurrency() + .onEach { + val fiatCurrency = with(it) { FiatCurrency(code, name, symbol) } + store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(fiatCurrency)) + } + .launchIn(scope) + .saveIn(appCurrencyUpdatesJobHolder) + } + + private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) { + uiState = when (val state = uiState) { + is AppSettingsScreenState.Content -> block(state) + is AppSettingsScreenState.Loading -> state + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index c82113133b..1dce42e622 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -1,97 +1,60 @@ package com.tangem.tap.features.details.ui.appsettings.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.AlertDialog -import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextButton -import com.tangem.core.ui.components.WarningTextButton +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.tap.features.details.redux.AppSetting +import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog import com.tangem.wallet.R @Composable -internal fun SettingsAlertDialog( - element: AppSetting, - onDialogStateChange: (AppSetting?) -> Unit, - onSettingToggle: () -> Unit, -) { - val text = when (element) { - AppSetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message - AppSetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message - } - - AlertDialog( - onDismissRequest = { onDialogStateChange(null) }, - confirmButton = { - TextButton( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - text = stringResource(id = R.string.common_cancel), - onClick = { - onDialogStateChange(null) - }, - ) - }, - dismissButton = { - WarningTextButton( - text = stringResource(id = R.string.common_delete), - onClick = { - onDialogStateChange(null) - onSettingToggle() - }, - ) - }, - title = { - Text( - text = stringResource(id = R.string.common_attention), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - }, - text = { - Text( - text = stringResource(id = text), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) - }, - shape = TangemTheme.shapes.roundedCornersLarge, +internal fun SettingsAlertDialog(dialog: Dialog.Alert) { + BasicDialog( + title = dialog.title.resolveReference(), + message = dialog.description.resolveReference(), + isDismissable = false, + confirmButton = DialogButton( + title = dialog.confirmText.resolveReference(), + warning = true, + onClick = dialog.onConfirm, + ), + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = dialog.onDismiss, + ), + onDismissDialog = dialog.onDismiss, ) } // region Preview -@Composable -private fun SettingsAlertDialogSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - SettingsAlertDialog( - element = AppSetting.SaveAccessCode, - onDialogStateChange = {}, - onSettingToggle = { }, - ) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun SettingsAlertDialogPreview_Light() { +private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { TangemTheme { - SettingsAlertDialogSample() + SettingsAlertDialog(dialog = dialog) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun SettingsAlertDialogPreview_Dark() { +private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { TangemTheme(isDark = true) { - SettingsAlertDialogSample() + SettingsAlertDialog(dialog = dialog) } } + +private class AlertDialogProvider : CollectionPreviewParameterProvider( + collection = buildList { + val dialogsFactory = AppSettingsDialogsFactory() + + dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add) + dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt new file mode 100644 index 0000000000..946c62d2f2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsButtonItem.kt @@ -0,0 +1,80 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +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 + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun SettingsButtonItem(item: Item.Button, modifier: Modifier = Modifier) { + Surface( + modifier = modifier.fillMaxWidth(), + color = TangemTheme.colors.background.secondary, + onClick = item.onClick, + ) { + Column( + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing20, + vertical = TangemTheme.dimens.spacing8, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + horizontalAlignment = Alignment.Start, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = item.description.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { + TangemTheme { + SettingsButtonItem(item = item) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) { + TangemTheme(isDark = true) { + SettingsButtonItem(item = item) + } +} + +private class ButtonItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createSelectAppCurrencyButton( + currentAppCurrencyName = "US Dollar", + onClick = { /* no-op */ }, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt similarity index 56% rename from app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt rename to app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt index 1386a364d7..1560b14a43 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsCardItem.kt @@ -1,11 +1,6 @@ package com.tangem.tap.features.details.ui.appsettings.components -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.Icon import androidx.compose.material.Surface @@ -14,24 +9,25 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerW16 +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.wallet.R +import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item @OptIn(ExperimentalMaterialApi::class) @Composable -internal fun EnrollBiometricsCard(onClick: () -> Unit) { +internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) { Surface( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing8) - .fillMaxWidth(), - color = TangemTheme.colors.background.primary, + modifier = modifier.fillMaxWidth(), + color = TangemTheme.colors.button.disabled, shape = TangemTheme.shapes.roundedCornersLarge, - onClick = onClick, + onClick = item.onClick, ) { Row( modifier = Modifier.padding(all = 16.dp), @@ -39,20 +35,20 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) { horizontalArrangement = Arrangement.SpaceEvenly, ) { Icon( - painter = painterResource(id = R.drawable.ic_alert_circle_24), + painter = painterResource(id = item.iconResId), tint = TangemTheme.colors.icon.attention, contentDescription = null, ) SpacerW16() Column { Text( - text = stringResource(id = R.string.app_settings_enable_biometrics_title), + text = item.title.resolveReference(), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, ) SpacerH4() Text( - text = stringResource(id = R.string.app_settings_enable_biometrics_description), + text = item.description.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, ) @@ -62,28 +58,29 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) { } // region Preview -@Composable -private fun EnrollBiometricsCardSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier.background(TangemTheme.colors.background.secondary), - ) { - EnrollBiometricsCard(onClick = {}) - } -} - @Preview(showBackground = true, widthDp = 360) @Composable -private fun EnrollBiometricsCardPreview_Light() { +private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) { TangemTheme { - EnrollBiometricsCardSample() + SettingsCardItem(item = item) } } @Preview(showBackground = true, widthDp = 360) @Composable -private fun EnrollBiometricsCardPreview_Dark() { +private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) { TangemTheme(isDark = true) { - EnrollBiometricsCardSample() + SettingsCardItem(item = item) } } + +private class CardItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createEnrollBiometricsCard( + onClick = { /* no-op */ }, + ).let(::add) + }, +) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt new file mode 100644 index 0000000000..8c1a93e307 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt @@ -0,0 +1,58 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +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.components.DialogButton +import com.tangem.core.ui.components.SelectorDialog +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory +import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog +import com.tangem.wallet.R +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { + SelectorDialog( + title = dialog.title.resolveReference(), + selectedItemIndex = dialog.selectedItemIndex, + items = dialog.items.map { it.resolveReference() }.toImmutableList(), + confirmButton = DialogButton( + title = stringResource(R.string.common_cancel), + onClick = dialog.onDismiss, + ), + onSelect = dialog.onSelect, + onDismissDialog = dialog.onDismiss, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SettingsSelectorDialogPreview_Light(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { + TangemTheme(isDark = false) { + SettingsSelectorDialog(param) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SettingsSelectorDialogPreview_Dark(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { + TangemTheme(isDark = true) { + SettingsSelectorDialog(param) + } +} + +private class DialogProvider : CollectionPreviewParameterProvider( + collection = listOf( + AppSettingsDialogsFactory().createThemeModeSelectorDialog( + selectedModeIndex = 0, + onSelect = {}, + onDismiss = {}, + ), + ), +) +// endregion Preview \ 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 new file mode 100644 index 0000000000..78084743b7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSwitchItem.kt @@ -0,0 +1,113 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.SpacerW32 +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 + +@Composable +internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) { + val titleTextColor by rememberUpdatedState( + newValue = if (item.isEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + val descriptionTextColor by rememberUpdatedState( + newValue = if (item.isEnabled) { + TangemTheme.colors.text.secondary + } else { + TangemTheme.colors.text.tertiary + }, + ) + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column( + modifier = Modifier.weight(weight = .9f), + verticalArrangement = Arrangement.Center, + ) { + Text( + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = titleTextColor, + ) + SpacerH4() + Text( + text = item.description.resolveReference(), + style = TangemTheme.typography.body2, + color = descriptionTextColor, + ) + } + SpacerW32() + TangemSwitch( + checked = item.isChecked, + enabled = item.isEnabled, + onCheckedChange = item.onCheckedChange, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { + TangemTheme { + SettingsSwitchItem(item = item) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) { + TangemTheme(isDark = true) { + SettingsSwitchItem(item = item) + } +} + +private class SwitchItemProvider : CollectionPreviewParameterProvider( + collection = buildList { + val itemsFactory = AppSettingsItemsFactory() + + itemsFactory.createSaveAccessCodeSwitch( + isChecked = true, + isEnabled = true, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = false, + isEnabled = true, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = true, + isEnabled = false, + onCheckedChange = { /* no-op */ }, + ).let(::add) + itemsFactory.createSaveAccessCodeSwitch( + isChecked = false, + isEnabled = false, + onCheckedChange = { /* no-op */ }, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index 90c489ab41..28bd5b3d26 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -1,51 +1,49 @@ package com.tangem.tap.features.details.ui.cardsettings -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class CardSettingsFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class CardSettingsFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel = CardSettingsViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) + @Composable + override fun ScreenContent(modifier: Modifier) { + CardSettingsScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { + store.dispatch(DetailsAction.ResetCardSettingsData) + store.dispatch(NavigationAction.PopBackTo()) + }, + ) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - CardSettingsScreen( - state = screenState.value, - onBackClick = { - store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) - }, - ) - } - } - } + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 29906fd0e0..87c845a602 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -2,13 +2,7 @@ package com.tangem.tap.features.details.ui.cardsettings import androidx.compose.foundation.Image import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -18,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -29,10 +22,15 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) { +internal fun CardSettingsScreen( + state: CardSettingsScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { val needReadCard = state.cardDetails == null SettingsScreensScaffold( + modifier = modifier, content = { if (needReadCard) { CardSettingsReadCard(state.onScanCardClick) @@ -41,14 +39,13 @@ fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) } }, titleRes = R.string.card_settings_title, - backgroundColor = TangemTheme.colors.background.secondary, onBackClick = onBackClick, ) } @Suppress("MagicNumber") @Composable -fun CardSettingsReadCard(onScanCardClick: () -> Unit) { +private fun CardSettingsReadCard(onScanCardClick: () -> Unit) { Column( modifier = Modifier.fillMaxSize(), ) { @@ -84,13 +81,13 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) { ) { Text( text = stringResource(id = R.string.scan_card_settings_title), - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.h3, ) Spacer(modifier = Modifier.size(20.dp)) Text( text = stringResource(id = R.string.scan_card_settings_message), - color = colorResource(id = R.color.text_secondary), + color = TangemTheme.colors.text.secondary, style = TangemTheme.typography.body1, modifier = Modifier .verticalScroll(rememberScrollState()) @@ -107,7 +104,7 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) { @Suppress("ComplexMethod") @Composable -fun CardSettings(state: CardSettingsScreenState) { +private fun CardSettings(state: CardSettingsScreenState) { if (state.cardDetails == null) return LazyColumn( @@ -166,8 +163,25 @@ fun CardSettings(state: CardSettingsScreenState) { } } +// region Preview @Composable -@Preview -private fun CardSettingsPreview() { +private fun CardSettingsScreenStateSample() { CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {}) -} \ No newline at end of file +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun CardSettingsScreenStatePreview_Light() { + TangemTheme { + CardSettingsScreenStateSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun CardSettingsScreenStatePreview_Dark() { + TangemTheme(isDark = true) { + CardSettingsScreenStateSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt index e09b4e7c2b..4cf5e68186 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt @@ -11,14 +11,14 @@ import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText import com.tangem.wallet.R import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo -data class CardSettingsScreenState( +internal data class CardSettingsScreenState( val cardDetails: List? = null, val accessCodeRecoveryState: AccessCodeRecoveryState? = null, val onScanCardClick: () -> Unit, val onElementClick: (CardInfo) -> Unit, ) -sealed class CardInfo( +internal sealed class CardInfo( val titleRes: TextReference, val subtitle: TextReference, val clickable: Boolean = false, @@ -68,7 +68,7 @@ sealed class CardInfo( } // TODO("Remove and use the same from coreUI") -sealed interface TextReference { +internal sealed interface TextReference { class Res(@StringRes val id: Int, val formatArgs: List = emptyList()) : TextReference { constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList()) } @@ -78,7 +78,7 @@ sealed interface TextReference { @Composable @ReadOnlyComposable -fun TextReference.resolveReference(): String { +internal fun TextReference.resolveReference(): String { return when (this) { is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray()) is TextReference.Str -> this.value diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 805f373605..d40ed17ec9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -10,7 +10,7 @@ import com.tangem.tap.features.details.redux.CardSettingsState import com.tangem.tap.features.details.redux.DetailsAction import org.rekotlin.Store -class CardSettingsViewModel(private val store: Store) { +internal class CardSettingsViewModel(private val store: Store) { fun updateState(state: CardSettingsState?): CardSettingsScreenState { return if (state?.manageSecurityState == null) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index c92047d0bc..35b36c0f00 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -1,33 +1,41 @@ package com.tangem.tap.features.details.ui.common import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes import androidx.compose.foundation.layout.* import androidx.compose.foundation.selection.selectable import androidx.compose.material.* 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.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @Composable -fun SettingsScreensScaffold( - modifier: Modifier = Modifier, - content: @Composable () -> Unit, - background: @Composable (() -> Unit)? = null, - fab: @Composable (() -> Unit)? = null, - backgroundColor: Color = TangemTheme.colors.background.secondary, - titleRes: Int? = null, +internal fun SettingsScreensScaffold( onBackClick: () -> Unit, + content: @Composable () -> Unit, + modifier: Modifier = Modifier, + @StringRes titleRes: Int? = null, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + fab: @Composable () -> Unit = {}, ) { - BackHandler(true, onBackClick) + val state = rememberScaffoldState(snackbarHostState = snackbarHostState) + val backgroundColor = TangemTheme.colors.background.secondary + + BackHandler(onBack = onBackClick) + SystemBarsEffect { + setSystemBarsColor(backgroundColor) + } Scaffold( + scaffoldState = state, topBar = { EmptyTopBarWithNavigation( onBackClick = onBackClick, @@ -36,34 +44,32 @@ fun SettingsScreensScaffold( }, modifier = modifier.systemBarsPadding(), backgroundColor = backgroundColor, - floatingActionButton = { fab?.invoke() }, - ) { - if (titleRes != null) { - Box(modifier = modifier.fillMaxSize()) { - background?.invoke() - - Column(modifier = modifier.fillMaxWidth()) { + floatingActionButton = fab, + content = { paddings -> + Column( + modifier = Modifier + .padding(paddings) + .fillMaxSize(), + ) { + if (titleRes != null) { Text( text = stringResource(id = titleRes), - modifier = modifier.padding( - start = TangemTheme.dimens.spacing20, - end = TangemTheme.dimens.spacing20, - bottom = TangemTheme.dimens.spacing54, - ), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing20) + .padding(bottom = TangemTheme.dimens.spacing36), style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, ) - content() } + + content() } - } else { - content() - } - } + }, + ) } @Composable -fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { +internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { Text( text = stringResource(id = titleRes), modifier = modifier.padding(start = 20.dp, end = 20.dp), @@ -73,7 +79,7 @@ fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { } @Composable -fun EmptyTopBarWithNavigation( +internal fun EmptyTopBarWithNavigation( onBackClick: () -> Unit, backgroundColor: Color = TangemTheme.colors.background.primary, ) { @@ -95,7 +101,12 @@ fun EmptyTopBarWithNavigation( } @Composable -fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) { +internal fun DetailsMainButton( + title: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { PrimaryButtonIconEnd( text = title, enabled = enabled, @@ -107,7 +118,7 @@ fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = M } @Composable -fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) { +internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() @@ -122,8 +133,8 @@ fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean onClick = null, modifier = Modifier.padding(end = 20.dp), colors = RadioButtonDefaults.colors( - unselectedColor = colorResource(id = R.color.icon_secondary), - selectedColor = colorResource(id = R.color.icon_accent), + unselectedColor = TangemTheme.colors.icon.secondary, + selectedColor = TangemTheme.colors.icon.accent, ), ) @@ -131,13 +142,13 @@ fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean Text( text = title, style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, ) Spacer(modifier = Modifier.size(4.dp)) Text( text = subtitle, style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_secondary), + color = TangemTheme.colors.text.secondary, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt index c6c8e82cf4..9839280049 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt @@ -1,22 +1,12 @@ package com.tangem.tap.features.details.ui.common import androidx.compose.animation.animateColor -import androidx.compose.animation.core.FastOutLinearInEasing -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable @@ -25,17 +15,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.tangem.wallet.R +import com.tangem.core.ui.res.TangemTheme @Suppress("MagicNumber") @Composable fun TangemSwitch( onCheckedChange: (Boolean) -> Unit, - checkedColor: Color = colorResource(id = R.color.control_checked), - uncheckedColor: Color = colorResource(id = R.color.icon_informative), + checkedColor: Color = TangemTheme.colors.icon.accent, + uncheckedColor: Color = TangemTheme.colors.icon.informative, size: Dp = 48.dp, checked: Boolean = false, enabled: Boolean = true, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index 371b571cdd..5a4fa36b95 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -1,45 +1,53 @@ package com.tangem.tap.features.details.ui.details import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.features.details.DarkThemeFeatureToggle import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class DetailsFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class DetailsFragment : ComposeFragment(), StoreSubscriber { - private val detailsViewModel = DetailsViewModel(store) + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + + @Inject + lateinit var darkThemeFeatureToggle: DarkThemeFeatureToggle + + private lateinit var detailsViewModel: DetailsViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + detailsViewModel = DetailsViewModel(store, darkThemeFeatureToggle) Analytics.send(Settings.ScreenOpened()) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.fade) - exitTransition = inflater.inflateTransition(R.transition.fade) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - DetailsScreen( - state = detailsViewModel.detailsScreenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + @Composable + override fun ScreenContent(modifier: Modifier) { + DetailsScreen( + modifier = modifier, + state = detailsViewModel.detailsScreenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) + } + + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt index f05682f878..1d074d274a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreen.kt @@ -1,156 +1,133 @@ package com.tangem.tap.features.details.ui.details import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Icon -import androidx.compose.material.SnackbarHost -import androidx.compose.material.SnackbarHostState -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SystemBarsEffect +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.common.ScreenTitle import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R -import kotlinx.coroutines.launch +import kotlinx.collections.immutable.toImmutableList @Composable -fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) { - SystemBarsEffect { - setSystemBarsColor(color = TangemColorPalette.Light1) - } +internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { + val snackbarHostState = remember { SnackbarHostState() } SettingsScreensScaffold( + modifier = modifier, + snackbarHostState = snackbarHostState, content = { Content(state = state) }, onBackClick = onBackClick, ) + + ShowSnackbarIfNeeded( + snackbarHostState = snackbarHostState, + messageEvent = state.showSnackbar, + ) } @Composable -fun Content(state: DetailsScreenState) { - Box { +private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) { + Box(modifier = modifier) { Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), ) { - ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp)) - state.elements.map { element -> - if (element == SettingsElement.WalletConnect) { - WalletConnectDetailsItem(onItemsClick = state.onItemsClick) - } else { - DetailsItem( - item = element, - appCurrency = state.appCurrency, - onItemsClick = { state.onItemsClick(element) }, - ) - } - } - Spacer(modifier = Modifier.weight(1f)) - TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick) - Spacer(modifier = Modifier.size(12.dp)) - Text( - text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}", - style = TangemTheme.typography.caption, - color = colorResource(id = R.color.text_tertiary), - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp), + ScreenTitle(titleRes = R.string.details_title) + SpacerH(height = TangemTheme.dimens.spacing36) + SettingsItems( + items = state.elements, ) + SpacerHMax() + TangemSocialAccounts( + links = state.tangemLinks, + onSocialNetworkClick = state.onSocialNetworkClick, + ) + SpacerH(height = TangemTheme.dimens.spacing12) + TangemAppVersion( + appNameRes = state.appNameRes, + version = state.tangemVersion, + ) + SpacerH(height = TangemTheme.dimens.spacing16) } - ShowSnackbarIfNeeded(state.showErrorSnackbar.value) } } @Composable -fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { +private fun SettingsItems(items: List) { + items.forEach { item -> + if (item.isLarge) { + LargeDetailsItem(item) + } else { + DetailsItem(item) + } + } +} + +@Composable +private fun LargeDetailsItem(item: SettingsItem) { Row( modifier = Modifier - .defaultMinSize(minHeight = 84.dp) - .fillMaxWidth() - .clickable { onItemsClick(SettingsElement.WalletConnect) }, - horizontalArrangement = Arrangement.Start, + .clickable(onClick = item.onClick) + .padding(horizontal = TangemTheme.dimens.spacing20) + .heightIn(min = TangemTheme.dimens.size84) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, ) { - Icon( - painter = painterResource(id = R.drawable.ic_walletconnect), - contentDescription = stringResource(id = R.string.wallet_connect_title), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = colorResource(id = R.color.all_colors_azure), - ) + if (item.showProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.informative, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = item.iconResId), + contentDescription = item.title.resolveReference(), + tint = TangemColorPalette.Azure, + ) + } Column( - modifier = Modifier.defaultMinSize(minHeight = 56.dp), + modifier = Modifier.heightIn(min = TangemTheme.dimens.size56), horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens.spacing4, + alignment = Alignment.CenterVertically, + ), ) { Text( - text = stringResource(id = R.string.wallet_connect_title), - modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), + text = item.title.resolveReference(), style = TangemTheme.typography.h3, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, ) - Text( - text = stringResource(id = R.string.wallet_connect_subtitle), - modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), - style = TangemTheme.typography.body1, - color = colorResource(id = R.color.text_secondary), - ) - } - } -} -@Composable -fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) { - Row( - modifier = Modifier - .height(56.dp) - .fillMaxWidth() - .clickable(onClick = onItemsClick), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(id = item.iconRes), - contentDescription = stringResource(id = item.titleRes), - modifier = Modifier.padding(start = 20.dp, end = 20.dp), - tint = colorResource(id = R.color.icon_secondary), - ) - Column(modifier = Modifier.padding(end = 20.dp)) { - Text( - text = stringResource(id = item.titleRes), - modifier = Modifier, - style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), - ) - if (item == SettingsElement.AppCurrency) { + if (item.subtitle != null) { Text( - text = appCurrency, - style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_secondary), + text = item.subtitle.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, ) } } @@ -158,66 +135,159 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> } @Composable -fun TangemSocialAccounts(links: List, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { - LazyRow( - modifier = Modifier.padding(start = 8.dp, end = 8.dp), +private fun DetailsItem(item: SettingsItem) { + Row( + modifier = Modifier + .clickable(enabled = !item.showProgress, onClick = item.onClick) + .padding(horizontal = TangemTheme.dimens.spacing20) + .heightIn(min = TangemTheme.dimens.size56) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, ) { - items(links) { + if (item.showProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.informative, + ) + } else { Icon( - painter = painterResource(id = it.network.iconRes), - contentDescription = "", - modifier = Modifier - .padding(8.dp) - .clickable { onSocialNetworkClick(it) }, - tint = colorResource(id = R.color.icon_informative), + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = item.iconResId), + contentDescription = item.title.resolveReference(), + tint = TangemTheme.colors.icon.secondary, ) } - } -} -@Composable -fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { - val snackbarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - SnackbarHost( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(vertical = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - hostState = snackbarHostState, - ) - val errorTitle = when (snackbarErrorState) { - is EventError.DemoReferralNotAvailable -> stringResource(id = R.string.alert_demo_feature_disabled) - EventError.Empty -> "" - } - if (snackbarErrorState != EventError.Empty) { - SideEffect { - coroutineScope.launch { - snackbarHostState.showSnackbar(errorTitle) - } - when (snackbarErrorState) { - is EventError.DemoReferralNotAvailable -> snackbarErrorState.onErrorShow.invoke() - else -> { - /*no-op*/ - } + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceAround, + ) { + Text( + text = item.title.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + + if (item.subtitle != null) { + Text( + text = item.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) } } } } @Composable -@Preview -private fun Preview() { - DetailsScreen( - state = DetailsScreenState( - elements = SettingsElement.values().toList(), +private fun TangemSocialAccounts(links: List, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing8), + ) { + items(links) { + val onClick = remember(it) { + { onSocialNetworkClick(it) } + } + + IconButton( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing4) + .size(TangemTheme.dimens.size32), + onClick = onClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = it.network.iconRes), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } + } +} + +@Composable +private fun ShowSnackbarIfNeeded(snackbarHostState: SnackbarHostState, messageEvent: StateEvent) { + var message: TextReference? by remember { mutableStateOf(value = null) } + val resolvedMessage by rememberUpdatedState(newValue = message?.resolveReference()) + + LaunchedEffect(resolvedMessage) { + resolvedMessage?.let { + snackbarHostState.showSnackbar(it) + } + } + + EventEffect(messageEvent) { + message = it + } +} + +@Composable +private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + text = "${stringResource(id = appNameRes)} $version", + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.tertiary, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 900) +@Composable +private fun DetailsScreenPreview_Light( + @PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState, +) { + TangemTheme(isDark = false) { + DetailsScreen(param, onBackClick = {}) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 900) +@Composable +private fun DetailsScreenPreview_Dark(@PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState) { + TangemTheme(isDark = true) { + DetailsScreen(param, onBackClick = {}) + } +} + +private class DetailsScreenStateProvider : CollectionPreviewParameterProvider( + collection = buildList { + DetailsScreenState( + elements = buildList { + SettingsItem.WalletConnect({}).let(::add) + SettingsItem.AddWallet(showProgress = false, {}).let(::add) + SettingsItem.LinkMoreCards({}).let(::add) + SettingsItem.CardSettings({}).let(::add) + SettingsItem.AppSettings({}).let(::add) + SettingsItem.Chat({}).let(::add) + SettingsItem.SendFeedback({}).let(::add) + SettingsItem.ReferralProgram({}).let(::add) + SettingsItem.TermsOfService({}).let(::add) + }.toImmutableList(), tangemLinks = TangemSocialAccounts.accountsEn, tangemVersion = "Tangem 2.14.12 (343)", - appCurrency = "Dollar", - onItemsClick = {}, + showSnackbar = consumedEvent(), onSocialNetworkClick = {}, - ), - onBackClick = {}, - ) -} \ No newline at end of file + ).let(::add) + + DetailsScreenState( + elements = buildList { + SettingsItem.WalletConnect({}).let(::add) + SettingsItem.AddWallet(showProgress = true, {}).let(::add) + SettingsItem.CardSettings({}).let(::add) + SettingsItem.AppSettings({}).let(::add) + SettingsItem.Chat({}).let(::add) + SettingsItem.SendFeedback({}).let(::add) + SettingsItem.TermsOfService({}).let(::add) + }.toImmutableList(), + tangemLinks = TangemSocialAccounts.accountsRu, + tangemVersion = "Tangem 2.14.12 (343)", + showSnackbar = consumedEvent(), + onSocialNetworkClick = {}, + ).let(::add) + }, +) +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index c0dd629819..de0784dce0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -1,82 +1,164 @@ package com.tangem.tap.features.details.ui.details +import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.wallet.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Immutable -data class DetailsScreenState( - val elements: List, - val tangemLinks: List, +internal data class DetailsScreenState( + val elements: ImmutableList, + val tangemLinks: ImmutableList, val tangemVersion: String, - val appCurrency: String, - val onItemsClick: (SettingsElement) -> Unit, + val showSnackbar: StateEvent, val onSocialNetworkClick: (SocialNetworkLink) -> Unit, - val showErrorSnackbar: MutableState = mutableStateOf(EventError.Empty), ) { val appNameRes: Int = R.string.tangem_app_name } @Immutable -enum class SettingsElement( - val iconRes: Int, - val titleRes: Int, +internal sealed class SettingsItem( + @DrawableRes val iconResId: Int, + val title: TextReference, + val subtitle: TextReference? = null, + val isLarge: Boolean = false, ) { - WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title), - Chat(R.drawable.ic_chat, R.string.details_chat), - SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback), - ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title), - CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title), - AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency), - AppSettings(R.drawable.ic_settings, R.string.app_settings_title), - LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), - TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App - PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy), - TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu), + + abstract val onClick: () -> Unit + + open val showProgress: Boolean = false + + data class WalletConnect( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_walletconnect, + title = resourceReference(R.string.wallet_connect_title), + subtitle = resourceReference(R.string.wallet_connect_subtitle), + isLarge = true, + ) + + data class AddWallet( + override val showProgress: Boolean, + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_plus_24, + title = stringReference(value = "Add new wallet"), + ) + + data class ScanWallet( + override val showProgress: Boolean, + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_plus_24, + title = stringReference(value = "Scan new wallet"), + ) + + data class LinkMoreCards( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_more_cards, + title = resourceReference(R.string.details_row_title_create_backup), + ) + + data class CardSettings( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_card_settings, + title = resourceReference(R.string.card_settings_title), + ) + + data class AppSettings( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_settings, + title = resourceReference(R.string.app_settings_title), + ) + + data class Chat( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_chat, + title = resourceReference(R.string.details_chat), + ) + + data class SendFeedback( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_comment, + title = resourceReference(R.string.details_row_title_send_feedback), + ) + + data class ReferralProgram( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_add_friends, + title = resourceReference(R.string.details_referral_title), + ) + + data class TermsOfService( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_text, + title = resourceReference(R.string.disclaimer_title), + ) + + data class TesterMenu( + override val onClick: () -> Unit, + ) : SettingsItem( + iconResId = R.drawable.ic_alert_24, + title = resourceReference(R.string.tester_menu), + ) } @Immutable -data class SocialNetworkLink( +internal data class SocialNetworkLink( val network: SocialNetwork, val url: String, ) -sealed class EventError { +internal sealed class EventError { object Empty : EventError() data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError() } sealed class SocialNetwork(val id: String, val iconRes: Int) { - object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram) object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter) - object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook) + object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram) + object Discord : SocialNetwork("Discord", R.drawable.ic_discord) + object Reddit : SocialNetwork("Reddit", R.drawable.ic_reddit) object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram) object GitHub : SocialNetwork("GitHub", R.drawable.ic_github) - object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube) + object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook) object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin) - object Discord : SocialNetwork("Discord", R.drawable.ic_discord) + object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube) } -object TangemSocialAccounts { - val accountsEn: List = listOf( +internal object TangemSocialAccounts { + val accountsEn: ImmutableList = persistentListOf( + SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"), SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"), - SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), - SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"), + SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"), + SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"), SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"), SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"), - SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"), + SocialNetworkLink(SocialNetwork.Facebook, "https://facebook.com/TangemCards/"), SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"), - SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"), + SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem3890"), ) - val accountsRu: List = listOf( + val accountsRu: ImmutableList = persistentListOf( + SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"), SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"), - SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), - SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"), + SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"), + SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"), SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"), SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"), - SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"), + SocialNetworkLink(SocialNetwork.Facebook, "https://facebook.com/TangemCards/"), SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"), - SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"), + SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem3890"), ) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 8301791338..779885e231 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -5,111 +5,112 @@ import androidx.compose.runtime.mutableStateOf import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.feedback.SupportInfo import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.details.DarkThemeFeatureToggle +import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.home.LocaleRegionProvider import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.scope +import com.tangem.tap.userWalletsListManager import com.tangem.wallet.BuildConfig +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.rekotlin.Store -class DetailsViewModel(private val store: Store) { +internal class DetailsViewModel( + private val store: Store, + private val darkThemeFeatureToggle: DarkThemeFeatureToggle, +) { // TODO: change to Android ViewModel var detailsScreenState: MutableState = mutableStateOf(updateState(store.state.detailsState)) private set - @Suppress("ComplexMethod") - fun updateState(state: DetailsState): DetailsScreenState { - val cardTypesResolver = state.scanResponse?.cardTypesResolver - val settings = SettingsElement.values().mapNotNull { - when (it) { - SettingsElement.WalletConnect -> { - if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null - } - SettingsElement.SendFeedback -> it - SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null - SettingsElement.PrivacyPolicy -> { - if (state.privacyPolicyUrl != null) it else null - } - SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null - SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null - SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null - SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null - else -> it - } - } + init { + bootstrapScreenState() + } + fun updateState(state: DetailsState): DetailsScreenState { return DetailsScreenState( - elements = settings, + elements = createSettingsItems(state), tangemLinks = getSocialLinks(), tangemVersion = getTangemAppVersion(), - appCurrency = state.appCurrency.name, - onItemsClick = { handleClickingSettingsItem(it) }, - onSocialNetworkClick = { handleSocialNetworkClick(it) }, + showSnackbar = triggerErrorSnackbarIfNeeded(state.error), + onSocialNetworkClick = ::handleSocialNetworkClick, ) } - private fun handleSocialNetworkClick(link: SocialNetworkLink) { - Analytics.send(Settings.ButtonSocialNetwork(link.network)) - store.dispatch(NavigationAction.OpenUrl(link.url)) + private fun createSettingsItems(state: DetailsState): ImmutableList { + val scanResponse = state.scanResponse ?: return persistentListOf() + val cardTypesResolver = scanResponse.cardTypesResolver + + return buildList { + SettingsItem.WalletConnect(::navigateToWalletConnect) + .takeIf { cardTypesResolver.isMultiwalletAllowed() } + ?.let(::add) + + SettingsItem.AddWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet) + .takeIf { state.appSettingsState.saveWallets } + ?.let(::add) + + SettingsItem.ScanWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet) + .takeUnless { state.appSettingsState.saveWallets } + ?.let(::add) + + SettingsItem.LinkMoreCards(::linkMoreCards) + .takeIf { state.createBackupAllowed } + ?.let(::add) + + SettingsItem.CardSettings(::navigateToCardSettings) + .let(::add) + + SettingsItem.AppSettings(::navigateToAppSettings) + .let(::add) + + SettingsItem.Chat(::navigateToChat) + .let(::add) + + SettingsItem.SendFeedback(::sendFeedback) + .let(::add) + + SettingsItem.ReferralProgram(::navigateToReferralProgram) + .takeIf { cardTypesResolver.isTangemWallet() } + ?.let(::add) + + SettingsItem.TermsOfService(::navigateToToS) + .let(::add) + + SettingsItem.TesterMenu(::navigateToTesterMenu) + .takeIf { BuildConfig.TESTER_MENU_ENABLED } + ?.let(::add) + }.toImmutableList() } - private fun handleClickingSettingsItem(item: SettingsElement) { - when (item) { - SettingsElement.WalletConnect -> { - Analytics.send(Settings.ButtonWalletConnect()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) - } - SettingsElement.Chat -> { - Analytics.send(Settings.ButtonChat()) - store.dispatch(GlobalAction.OpenChat(SupportInfo())) - } - SettingsElement.SendFeedback -> { - Analytics.send(Settings.ButtonSendFeedback()) - store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) - } - SettingsElement.CardSettings -> { - Analytics.send(Settings.ButtonCardSettings()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) - } - SettingsElement.AppCurrency -> { - store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency) - } - SettingsElement.AppSettings -> { - Analytics.send(Settings.ButtonAppSettings()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) - } - SettingsElement.LinkMoreCards -> { - Analytics.send(Settings.ButtonCreateBackup()) - store.dispatch(WalletAction.MultiWallet.BackupWallet) - } - SettingsElement.TermsOfService -> { - store.dispatch(DisclaimerAction.Show(AppScreen.Details)) - } - SettingsElement.PrivacyPolicy -> { - // TODO: To be available later - } - SettingsElement.ReferralProgram -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) - } - SettingsElement.TesterMenu -> { - store.state.daggerGraphState.testerRouter?.startTesterScreen() - } - } - } - - private fun getSocialLinks(): List { - val locale = LocaleRegionProvider().getRegion() - return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { - TangemSocialAccounts.accountsRu + private fun triggerErrorSnackbarIfNeeded(text: TextReference?): StateEvent { + return if (text == null) { + consumedEvent() } else { - TangemSocialAccounts.accountsEn + triggeredEvent(text) { + store.dispatch(DetailsAction.DismissError) + } } } @@ -118,4 +119,79 @@ class DetailsViewModel(private val store: Store) { val versionName: String = BuildConfig.VERSION_NAME return "$versionName ($versionCode)" } + + private fun navigateToTesterMenu() { + store.state.daggerGraphState.testerRouter?.startTesterScreen() + } + + private fun navigateToToS() { + store.dispatch(DisclaimerAction.Show(AppScreen.Details)) + } + + private fun navigateToReferralProgram() { + store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) + } + + private fun sendFeedback() { + Analytics.send(Settings.ButtonSendFeedback()) + store.dispatch(GlobalAction.SendEmail(FeedbackEmail())) + } + + private fun navigateToChat() { + Analytics.send(Settings.ButtonChat()) + store.dispatch(GlobalAction.OpenChat(SupportInfo())) + } + + private fun navigateToAppSettings() { + Analytics.send(Settings.ButtonAppSettings()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) + } + + private fun navigateToCardSettings() { + Analytics.send(Settings.ButtonCardSettings()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) + } + + private fun linkMoreCards() { + Analytics.send(Settings.ButtonCreateBackup()) + store.dispatch(WalletAction.MultiWallet.BackupWallet) + } + + private fun scanAndSaveUserWallet() { + store.dispatch(DetailsAction.ScanAndSaveUserWallet) + } + + private fun navigateToWalletConnect() { + Analytics.send(Settings.ButtonWalletConnect()) + store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) + } + + private fun handleSocialNetworkClick(link: SocialNetworkLink) { + Analytics.send(Settings.ButtonSocialNetwork(link.network)) + store.dispatch(NavigationAction.OpenUrl(link.url)) + } + + private fun getSocialLinks(): ImmutableList { + val locale = LocaleRegionProvider().getRegion() + return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) { + TangemSocialAccounts.accountsRu + } else { + TangemSocialAccounts.accountsEn + } + } + + private fun bootstrapScreenState() { + userWalletsListManager.selectedUserWallet + .distinctUntilChanged() + .onEach { selectedUserWallet -> + store.dispatchWithMain( + DetailsAction.PrepareScreen( + selectedUserWallet.scanResponse, + darkThemeFeatureToggle.isDarkThemeEnabled, + ), + ) + } + .flowOn(Dispatchers.IO) + .launchIn(scope) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index 7a7c723779..f7a75b1b41 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -1,47 +1,45 @@ package com.tangem.tap.features.details.ui.resetcard -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class ResetCardFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class ResetCardFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel = ResetCardViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) + @Composable + override fun ScreenContent(modifier: Modifier) { + ResetCardScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - ResetCardScreen( - state = screenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index d64bf444ca..d7b85a7202 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -1,24 +1,15 @@ package com.tangem.tap.features.details.ui.resetcard -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconToggleButton 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 @@ -34,32 +25,36 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit) { +internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) { SettingsScreensScaffold( + modifier = modifier, content = { ResetCardView(state = state) }, onBackClick = onBackClick, - backgroundColor = Color.Transparent, ) } @Suppress("LongMethod", "MagicNumber") @Composable -fun ResetCardView(state: ResetCardScreenState) { +private fun ResetCardView(state: ResetCardScreenState) { Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween, ) { - Box { - Image( - painter = painterResource(id = R.drawable.ill_reset_background), - contentDescription = null, - modifier = Modifier.offset(y = (-82).dp), + ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory) + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 21.dp), + contentAlignment = Alignment.CenterStart, + ) { + Icon( + painter = painterResource(id = R.drawable.img_alert), + contentDescription = "", + tint = Color.Unspecified, ) - ScreenTitle(titleRes = R.string.card_settings_reset_card_to_factory) } - Spacer(modifier = Modifier.weight(1f)) Column( modifier = Modifier.offset(y = (-32).dp), verticalArrangement = Arrangement.Bottom, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt index fee3e41a5b..b4f297d11c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.details.ui.resetcard import com.tangem.tap.features.details.ui.cardsettings.TextReference -data class ResetCardScreenState( +internal data class ResetCardScreenState( val accepted: Boolean = false, val descriptionText: TextReference, val onAcceptWarningToggleClick: (Boolean) -> Unit, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index 9ba899bc7d..b694554734 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -7,7 +7,7 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText import org.rekotlin.Store -class ResetCardViewModel(private val store: Store) { +internal class ResetCardViewModel(private val store: Store) { fun updateState(state: CardSettingsState?): ResetCardScreenState { val descriptionText = state?.cardInfo diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index c3b2015297..92b2cc3f50 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -1,47 +1,45 @@ package com.tangem.tap.features.details.ui.securitymode -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject -class SecurityModeFragment : Fragment(), StoreSubscriber { +@AndroidEntryPoint +internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder private val viewModel = SecurityModeViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState)) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) + @Composable + override fun ScreenContent(modifier: Modifier) { + SecurityModeScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, + ) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - SecurityModeScreen( - state = screenState.value, - onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, - ) - } - } - } + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt index ba9c01e84d..fe5164bf2f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt @@ -1,10 +1,6 @@ package com.tangem.tap.features.details.ui.securitymode -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable @@ -20,8 +16,13 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.wallet.R @Composable -fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) { +internal fun SecurityModeScreen( + state: SecurityModeScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { SettingsScreensScaffold( + modifier = modifier, content = { SecurityModeOptions(state = state) }, // titleRes = R.string.card_settings_security_mode, onBackClick = onBackClick, @@ -29,7 +30,7 @@ fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) } @Composable -fun SecurityModeOptions(state: SecurityModeScreenState) { +private fun SecurityModeOptions(state: SecurityModeScreenState) { Column( modifier = Modifier .fillMaxSize() @@ -55,7 +56,7 @@ fun SecurityModeOptions(state: SecurityModeScreenState) { } @Composable -fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) { +private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) { val selected = option == state.selectedSecurityMode val title = option.toTitleRes() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt index 97482c7c01..27c7629382 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreenState.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.securitymode import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.wallet.R -data class SecurityModeScreenState( +internal data class SecurityModeScreenState( val availableOptions: List, val selectedSecurityMode: SecurityOption, val isSaveChangesEnabled: Boolean, @@ -11,7 +11,7 @@ data class SecurityModeScreenState( val onSaveChangesClicked: () -> Unit, ) -fun SecurityOption.toTitleRes(): Int { +internal fun SecurityOption.toTitleRes(): Int { return when (this) { SecurityOption.LongTap -> R.string.details_manage_security_long_tap SecurityOption.PassCode -> R.string.details_manage_security_passcode diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt index a0666ff19d..7aa863a7df 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt @@ -6,7 +6,7 @@ import com.tangem.tap.features.details.redux.ManageSecurityState import com.tangem.tap.features.details.redux.SecurityOption import org.rekotlin.Store -class SecurityModeViewModel(val store: Store) { +internal class SecurityModeViewModel(val store: Store) { fun updateState(state: ManageSecurityState?): SecurityModeScreenState { if (state == null) { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt index 2c7ebdea6e..eff4598340 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt @@ -1,24 +1,30 @@ package com.tangem.tap.features.details.ui.walletconnect import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup +import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment +import androidx.compose.ui.Modifier import androidx.transition.TransitionInflater import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.store +import com.tangem.wallet.R +import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject + +@AndroidEntryPoint +internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder -class WalletConnectFragment : Fragment(), StoreSubscriber { private val viewModel = WalletConnectViewModel(store) private var screenState: MutableState = mutableStateOf(viewModel.updateState(store.state.walletConnectState)) @@ -26,32 +32,31 @@ class WalletConnectFragment : Fragment(), StoreSubscriber { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Analytics.send(WalletConnect.ScreenOpened()) - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return ComposeView(requireContext()).apply { - setContent { - isTransitionGroup = true - TangemTheme { - WalletConnectScreen( - state = screenState.value, - onBackClick = { - if (screenState.value.isLoading) { - store.dispatch( - WalletConnectAction.FailureEstablishingSession( - store.state.walletConnectState.newSessionData?.session?.session, - ), - ) - } - store.dispatch(NavigationAction.PopBackTo()) - }, + @Composable + override fun ScreenContent(modifier: Modifier) { + WalletConnectScreen( + modifier = modifier, + state = screenState.value, + onBackClick = { + if (screenState.value.isLoading) { + store.dispatch( + WalletConnectAction.FailureEstablishingSession( + store.state.walletConnectState.newSessionData?.session?.session, + ), ) } - } - } + store.dispatch(NavigationAction.PopBackTo()) + }, + ) + } + + override fun TransitionInflater.inflateTransitions(): Boolean { + enterTransition = inflateTransition(R.transition.fade) + exitTransition = inflateTransition(R.transition.fade) + + return true } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt index c9f4292991..79e8b38b23 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -26,10 +25,15 @@ import com.tangem.wallet.R import kotlinx.collections.immutable.persistentListOf @Composable -fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit) { +internal fun WalletConnectScreen( + state: WalletConnectScreenState, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { val context = LocalContext.current SettingsScreensScaffold( + modifier = modifier, content = { if (state.sessions.isEmpty()) { EmptyScreen(state) @@ -56,8 +60,8 @@ fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit private fun AddSessionFab(onAddSession: () -> Unit, modifier: Modifier = Modifier) { FloatingActionButton( onClick = onAddSession, - backgroundColor = colorResource(id = R.color.button_primary), - contentColor = colorResource(id = R.color.icon_primary_2), + backgroundColor = TangemTheme.colors.button.primary, + contentColor = TangemTheme.colors.icon.primary2, shape = RoundedCornerShape(16.dp), modifier = modifier, ) { @@ -73,7 +77,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { if (state.isLoading) { LinearProgressIndicator( modifier = Modifier.fillMaxWidth(), - color = colorResource(id = R.color.icon_accent), + color = TangemTheme.colors.icon.accent, ) } Column( @@ -86,7 +90,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { Image( painter = painterResource(id = R.drawable.ic_walletconnect), contentDescription = "", - colorFilter = ColorFilter.tint(colorResource(id = R.color.icon_inactive)), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), contentScale = ContentScale.FillWidth, modifier = Modifier.width(width = 100.dp), ) @@ -94,7 +98,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) { Text( text = stringResource(id = R.string.wallet_connect_subtitle), style = TangemTheme.typography.body2, - color = colorResource(id = R.color.text_tertiary), + color = TangemTheme.colors.text.tertiary, ) } } @@ -106,7 +110,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { modifier = Modifier .fillMaxWidth() .height(2.dp), - color = colorResource(id = R.color.icon_accent), + color = TangemTheme.colors.icon.accent, ) } else { Spacer(modifier = Modifier.height(2.dp)) @@ -127,7 +131,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { Text( text = session.description, style = TangemTheme.typography.subtitle1, - color = colorResource(id = R.color.text_primary_1), + color = TangemTheme.colors.text.primary1, modifier = Modifier.weight(1f), ) IconButton( @@ -139,7 +143,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) { Icon( painter = painterResource(id = R.drawable.ic_cross_rounded_24), contentDescription = "", - tint = colorResource(id = R.color.icon_warning), + tint = TangemTheme.colors.icon.warning, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt index 24878f3183..d0a711e3f0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectScreenState.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.walletconnect import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import kotlinx.collections.immutable.ImmutableList -data class WalletConnectScreenState( +internal data class WalletConnectScreenState( val sessions: ImmutableList, val isLoading: Boolean = false, val onRemoveSession: (String) -> Unit = {}, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt index 4707547c51..9713a5da28 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectViewModel.kt @@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList import org.rekotlin.Store import timber.log.Timber -class WalletConnectViewModel(private val store: Store) { +internal class WalletConnectViewModel(private val store: Store) { fun updateState(state: WalletConnectState): WalletConnectScreenState { Timber.d("WC2 Sessions: ${state.wc2Sessions}") val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index 930f33afe6..f208ac437a 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -56,7 +56,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs override fun onStart() { super.onStart() - setStatusBarColor(R.color.backgroundLightGray) + setStatusBarColor(R.color.background_secondary) webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) } store.subscribe(subscriber = this) { state -> @@ -122,6 +122,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs webView.loadLocalTermsOfServices() } else -> { + webView.setBackgroundColor(resources.getColor(R.color.transparent, null)) webView.loadUrl(disclaimer.getUri().toString()) } } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index ceb3ead5f5..2127b6e8c6 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -17,11 +17,11 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 9a76813cab..28bc7cb1f9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -113,6 +113,7 @@ private fun handleOtherCardsAction(action: Action) { } scope.launch { + // TODO: Use new repo [REDACTED_JIRA] userTokensRepository.saveUserTokens( card = result.data.card, tokens = blockchainNetworks.toCurrencies(), diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index a4b12b434c..7d12941132 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -105,7 +105,7 @@ class TwinsCardsFragment : BaseOnboardingFragment() { override fun onStart() { super.onStart() - setStatusBarColor(R.color.backgroundWhite) + setStatusBarColor(R.color.background_primary) } private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) = @@ -375,6 +375,9 @@ class TwinsCardsFragment : BaseOnboardingFragment() { mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), ) + mainBinding.onboardingTopContainer.imvCardBackground.backgroundTintList = + requireContext().resources.getColorStateList(R.color.onboarding_card_background, null) + updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index b255ab3536..2ab9ded42f 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -135,6 +135,7 @@ private fun handleWalletAction(action: Action) { } scope.launch { + // TODO: Use new repo [REDACTED_JIRA] userTokensRepository.saveUserTokens( card = result.data.card, tokens = blockchainNetworks.toCurrencies(), diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index ff40ab5ad6..2ee6338d5e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -22,6 +22,7 @@ import com.tangem.common.CardIdFormatter import com.tangem.common.CompletionResult import com.tangem.common.core.CardIdDisplayFormat import com.tangem.core.analytics.Analytics +import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource @@ -158,6 +159,7 @@ class OnboardingWalletFragment : oldState.onboardingWalletState == newState.onboardingWalletState }.select { it.onboardingWalletState } } + setStatusBarColor(R.color.background_primary) } override fun onStop() { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt index c8e883cdb8..d2f4d2392a 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt @@ -17,7 +17,7 @@ object ResetBackupCardDialog { setPositiveButton(R.string.common_cancel) { _, _ -> Analytics.send(Onboarding.Backup.ResetCancelEvent) } - setNegativeButton(R.string.common_reset) { _, _ -> + setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ -> Analytics.send(Onboarding.Backup.ResetPerformEvent) store.dispatch(BackupAction.ResetBackupCard(cardId)) } diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt index ae186c4912..67cdd2c0bd 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt @@ -1,18 +1,26 @@ package com.tangem.tap.features.saveWallet.ui import androidx.lifecycle.ViewModel +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog import com.tangem.tap.store +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import org.rekotlin.StoreSubscriber +import javax.inject.Inject -internal class SaveWalletViewModel : ViewModel(), StoreSubscriber { +@HiltViewModel +internal class SaveWalletViewModel @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, +) : ViewModel(), StoreSubscriber { private val stateInternal = MutableStateFlow(SaveWalletScreenState()) val state: StateFlow = stateInternal @@ -22,10 +30,12 @@ internal class SaveWalletViewModel : ViewModel(), StoreSubscriber state.copy(error = action.error) is AmountAction.SetDecimalSeparator -> state.copy(decimalSeparator = action.separator) + is AmountAction.HideBalance -> { + val rescaledBalance = sendState.convertExtractCryptoToFiat(state.balanceCrypto, true) + + state.copy( + hideBalance = action.hide, + viewBalanceValue = if (action.hide) STARS else rescaledBalance.stripZeroPlainString(), + ) + } } return updateLastState(sendState.copy(amountState = result), result) } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index 8400f07c87..fa9aa1d60e 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -130,6 +130,7 @@ data class AmountState( val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, FiatCurrency.Default.code), val amountToSendCrypto: BigDecimal = BigDecimal.ZERO, val balanceCrypto: BigDecimal = BigDecimal.ZERO, + val hideBalance: Boolean = false, val cursorAtTheSamePosition: Boolean = true, val maxLengthOfAmount: Int = 2, val decimalSeparator: String = ".", diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 0e963d3ac5..5da7c2fe6a 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -11,6 +11,7 @@ import android.view.inputmethod.EditorInfo import android.widget.EditText import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import by.kirich1409.viewbindingdelegate.viewBinding @@ -45,6 +46,7 @@ import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentSendBinding +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* @@ -56,8 +58,11 @@ private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L [REDACTED_AUTHOR] */ @OptIn(FlowPreview::class) +@AndroidEntryPoint class SendFragment : BaseStoreFragment(R.layout.fragment_send) { + private val viewModel by viewModels() + lateinit var sendBtn: ViewStateWidget private lateinit var etAmountToSend: TextInputEditText @@ -70,6 +75,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) Analytics.send(Token.Send.ScreenOpened()) } @@ -348,6 +354,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { override fun onDestroy() { store.dispatch(ReleaseSendState) + lifecycle.removeObserver(viewModel) super.onDestroy() } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt new file mode 100644 index 0000000000..1ac28bda46 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -0,0 +1,39 @@ +package com.tangem.tap.features.send.ui + +import androidx.lifecycle.* +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase +import com.tangem.tap.features.send.redux.AmountAction +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +internal class SendViewModel @Inject constructor( + private val dispatchers: CoroutineDispatcherProvider, + private val appStateHolder: AppStateHolder, + private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, +) : ViewModel(), DefaultLifecycleObserver { + + override fun onCreate(owner: LifecycleOwner) { + isBalanceHiddenUseCase() + .flowWithLifecycle(owner.lifecycle) + .onEach { isBalanceHidden -> + withContext(dispatchers.main) { + appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(isBalanceHidden)) + } + } + .launchIn(viewModelScope) + + viewModelScope.launch { + listenToFlipsUseCase() + .flowWithLifecycle(owner.lifecycle) + .collect() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index e05278fceb..1e44546acc 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -260,7 +260,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0 tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0) - val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray + val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary tvAmountCurrency.setTextColor(fg.getColor(textColor)) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt index ddc7a2a15b..85fd22ab71 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListInteractorModule.kt @@ -35,7 +35,6 @@ internal object TokensListInteractorModule { reduxStateHolder = reduxStateHolder, testnetTokensStorage = testnetTokensStorage, ), - reduxStateHolder = reduxStateHolder, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt index 07a7a3412d..2a7da5cca4 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/di/TokensListRouterModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.tokens.impl.di -import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import dagger.Module @@ -18,7 +17,5 @@ internal object TokensListRouterModule { @Provides @ViewModelScoped - fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter { - return DefaultTokensListRouter(customTokenFeatureToggles) - } + fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt index 64bd577f52..c470316894 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt @@ -1,262 +1,17 @@ package com.tangem.tap.features.tokens.impl.domain import androidx.paging.PagingData -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.common.CompletionResult -import com.tangem.common.card.EllipticCurve -import com.tangem.common.extensions.ByteArrayKey -import com.tangem.common.extensions.guard -import com.tangem.common.extensions.toMapKey -import com.tangem.common.flatMap -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.configs.CardConfig -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.common.util.supportsHdWallet -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.operations.derivation.ExtendedPublicKeysMap -import com.tangem.tap.* -import com.tangem.tap.common.extensions.dispatchDebugErrorNotification -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.proxy.AppStateHolder -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import timber.log.Timber /** * Default implementation of tokens list interactor - * FIXME("Necessary to avoid using redux actions") * - * @property repository repository of tokens list feature - * @property reduxStateHolder redux state holder + * @property repository repository of tokens list feature */ -internal class DefaultTokensListInteractor( - private val repository: TokensListRepository, - private val reduxStateHolder: AppStateHolder, -) : TokensListInteractor { +internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor { override fun getTokensList(searchText: String): Flow> { return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null })) } - - override suspend fun saveChanges(tokens: List, blockchains: List) { - val scanResponse = requireNotNull(reduxStateHolder.scanResponse) - - val derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle() - val currentTokens = store.state.tokensState.addedWallets - .toNonCustomTokensWithBlockchains(derivationStyle = derivationStyle) - - val currentBlockchains = store.state.tokensState.addedWallets - .toNonCustomBlockchains(derivationStyle = derivationStyle) - - val blockchainsToAdd = blockchains.filterNot(currentBlockchains::contains) - val blockchainsToRemove = currentBlockchains.filterNot(blockchains::contains) - - val tokensToAdd = tokens.filterNot(currentTokens::contains) - val tokensToRemove = currentTokens.filterNot { token -> tokens.any { it.token == token.token } } - - val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty() - val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() - if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { - store.dispatchDebugErrorNotification(message = "Nothing to save") - return - } - - remove( - tokens = tokensToRemove, - blockchains = blockchainsToRemove, - derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), - ) - - add(tokens = tokensToAdd, blockchains = blockchainsToAdd, scanResponse = scanResponse) - } - - private fun List.toNonCustomTokensWithBlockchains( - derivationStyle: DerivationStyle?, - ): List { - return this.map(WalletDataModel::currency) - .mapNotNull { currency -> - if (currency !is Currency.Token || currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - TokenWithBlockchain(token = currency.token, blockchain = currency.blockchain) - } - .distinct() - } - - private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { - return this.map(WalletDataModel::currency) - .mapNotNull { currency -> - if (currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - (currency as? Currency.Blockchain)?.blockchain - } - .distinct() - } - - private suspend fun remove( - tokens: List, - blockchains: List, - derivationStyle: DerivationStyle?, - ) { - val currencies = convertToCurrencies(tokens, blockchains, derivationStyle) - if (currencies.isEmpty()) return - - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - Timber.e("Unable to remove currencies, no user wallet selected") - return - } - - walletCurrenciesManager.removeCurrencies(userWallet = selectedUserWallet, currenciesToRemove = currencies) - } - - private suspend fun add( - tokens: List, - blockchains: List, - scanResponse: ScanResponse, - ) { - val currenciesToAdd = convertToCurrencies( - tokens = tokens, - blockchains = blockchains, - derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(), - ) - - // TODO("[REDACTED_TASK_KEY] use DerivationManager") - if (scanResponse.supportsHdWallet()) { - deriveMissingBlockchains(scanResponse, currenciesToAdd) - } else { - submitAdd(scanResponse, currenciesToAdd) - return - } - } - - private suspend fun deriveMissingBlockchains(scanResponse: ScanResponse, currencies: List) { - val config = CardConfig.createConfig(scanResponse.card) - val derivationDataList = currencies.mapNotNull { currency -> - val curve = config.primaryCurve(currency.blockchain) - curve?.let { getDerivations(curve, scanResponse, currency) } - } - - val derivations = buildMap> { - derivationDataList.forEach { - val current = this[it.derivations.first] - if (current != null) { - current.addAll(it.derivations.second) - current.distinct() - } else { - this[it.derivations.first] = it.derivations.second.toMutableList() - } - } - } - if (derivations.isEmpty()) { - submitAdd(scanResponse, currencies) - return - } - - when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) { - is CompletionResult.Success -> { - val newDerivedKeys = result.data.entries - val oldDerivedKeys = scanResponse.derivedKeys - - val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() - - val updatedDerivedKeys = walletKeys.associateWith { walletKey -> - val oldDerivations = ExtendedPublicKeysMap(map = oldDerivedKeys[walletKey] ?: emptyMap()) - val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(map = emptyMap()) - ExtendedPublicKeysMap(map = oldDerivations + newDerivations) - } - - val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys) - - store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) - delay(DELAY_SDK_DIALOG_CLOSE) - - submitAdd(scanResponse, currencies) - return - } - is CompletionResult.Failure -> { - store.dispatchDebugErrorNotification(TapError.CustomError(customMessage = "Error adding tokens")) - } - } - } - - private fun getDerivations( - curve: EllipticCurve, - scanResponse: ScanResponse, - currency: Currency, - ): TokensMiddleware.DerivationData? { - val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null - - val supportedCurves = currency.blockchain.getSupportedCurves() - val path = currency.blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) - .takeIf { supportedCurves.contains(curve) } - - val customPath = currency.derivationPath?.let { - DerivationPath(it) - }.takeIf { supportedCurves.contains(curve) } - - val bothCandidates = listOfNotNull(path, customPath).distinct().toMutableList() - if (bothCandidates.isEmpty()) return null - - if (currency is Currency.Blockchain && currency.blockchain == Blockchain.Cardano) { - currency.derivationPath?.let { - bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) - } - } - - val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() - val alreadyDerivedKeys: ExtendedPublicKeysMap = - scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) - val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() - - val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } - if (toDerive.isEmpty()) return null - - return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) - } - - private suspend fun submitAdd(scanResponse: ScanResponse, currencies: List) { - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - Timber.e("Unable to add currencies, no user wallet selected") - return - } - - userWalletsListManager - .update( - userWalletId = selectedUserWallet.walletId, - update = { userWallet -> userWallet.copy(scanResponse = scanResponse) }, - ) - .flatMap { updatedUserWallet -> - walletCurrenciesManager.addCurrencies( - userWallet = updatedUserWallet, - currenciesToAdd = currencies, - ) - } - } - - private fun convertToCurrencies( - tokens: List, - blockchains: List, - derivationStyle: DerivationStyle?, - ): List { - return tokens.map { tokenWithBlockchain -> - Currency.Token( - token = tokenWithBlockchain.token, - blockchain = tokenWithBlockchain.blockchain, - derivationPath = tokenWithBlockchain.blockchain.derivationPath(derivationStyle)?.rawPath, - ) - }.plus( - blockchains.map { blockchain -> - Currency.Blockchain( - blockchain = blockchain, - derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath, - ) - }, - ) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt index fee08220bf..c363dfc3c2 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/TokensListInteractor.kt @@ -1,9 +1,7 @@ package com.tangem.tap.features.tokens.impl.domain import androidx.paging.PagingData -import com.tangem.blockchain.common.Blockchain import com.tangem.tap.features.tokens.impl.domain.models.Token -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain import kotlinx.coroutines.flow.Flow /** @@ -15,12 +13,4 @@ internal interface TokensListInteractor { /** Get tokens list using filter by text [searchText] */ fun getTokensList(searchText: String): Flow> - - /** - * Save added tokens - * - * @param tokens tokens list that need to save - * @param blockchains blockchains list that need to save - */ - suspend fun saveChanges(tokens: List, blockchains: List) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt deleted file mode 100644 index 207976d628..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/models/TokensListArgs.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.tap.features.tokens.impl.presentation.models - -import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.store - -/** - * Required data for tokens list screen - * FIXME("Necessary to avoid using redux state") - * -[REDACTED_AUTHOR] - */ -class TokensListArgs { - /** Tokens list screen mode */ - val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess - - /** Tokens list that accessible from the main screen */ - val mainScreenTokenList: List get() = store.state.tokensState.addedTokens - - /** Blockchains list that accessible from the main screen */ - val mainScreenBlockchainList: List get() = store.state.tokensState.addedBlockchains -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt index 93ae4e1b17..68b277ea7e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt @@ -6,8 +6,6 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.store import com.tangem.wallet.R @@ -18,20 +16,14 @@ import com.tangem.wallet.R * [REDACTED_AUTHOR] */ -internal class DefaultTokensListRouter( - private val customTokenFeatureToggles: CustomTokenFeatureToggles, -) : TokensListRouter { +internal class DefaultTokensListRouter : TokensListRouter { override fun popBackStack() { store.dispatch(NavigationAction.PopBackTo()) } override fun openAddCustomTokenScreen() { - if (customTokenFeatureToggles.isRedesignedScreenEnabled) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) - } else { - store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken) - } + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) } override fun showAddressCopiedNotification() { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt new file mode 100644 index 0000000000..da83face06 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListCryptoCurrencies.kt @@ -0,0 +1,9 @@ +package com.tangem.tap.features.tokens.impl.presentation.viewmodels + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.tokens.TokenWithBlockchain + +internal data class TokensListCryptoCurrencies( + val coins: List, + val tokens: List, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt new file mode 100644 index 0000000000..8340df9959 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -0,0 +1,187 @@ +package com.tangem.tap.features.tokens.impl.presentation.viewmodels + +import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import com.tangem.tap.domain.model.WalletDataModel +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.store +import timber.log.Timber +import kotlin.properties.Delegates + +/** + * Class that divide a new and legacy logic when user uses tokens list screen + * + * @property walletFeatureToggles wallet feature toggles + * @property getSelectedWalletUseCase use case that returns selected wallet + * @property getCurrenciesUseCase use case that returns crypto currencies of a specified wallet + */ +internal class TokensListMigration( + private val walletFeatureToggles: WalletFeatureToggles, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, +) { + + private var currentNewCoins: List by Delegates.notNull() + private var currentNewTokens: List by Delegates.notNull() + private var currentUserWallet: UserWallet by Delegates.notNull() + + private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } + + suspend fun getCurrentCryptoCurrencies(): TokensListCryptoCurrencies { + return if (walletFeatureToggles.isRedesignedScreenEnabled) { + getNewCryptoCurrencies() + } else { + getLegacyCryptoCurrencies() + } + } + + private suspend fun getNewCryptoCurrencies(): TokensListCryptoCurrencies { + return when (val selectedWalletEither = getSelectedWalletUseCase()) { + is Either.Left -> { + Timber.e(selectedWalletEither.value.toString()) + TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) + } + is Either.Right -> { + currentUserWallet = selectedWalletEither.value + + when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) { + is Either.Left -> { + Timber.e(currenciesEither.value.toString()) + TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList()) + } + is Either.Right -> { + TokensListCryptoCurrencies( + coins = currenciesEither.value + .filterIsInstance() + .filterNot { it.isCustom } + .also { currentNewCoins = it } + .map { Blockchain.fromId(it.network.id.value) }, + tokens = currenciesEither.value + .filterIsInstance() + .filterNot(CryptoCurrency.Token::isCustom) + .also { currentNewTokens = it } + .map { token -> + TokenWithBlockchain( + token = Token( + name = token.name, + symbol = token.symbol, + contractAddress = token.contractAddress, + decimals = token.decimals, + id = token.id.rawCurrencyId, + ), + blockchain = Blockchain.fromId(token.network.id.value), + ) + }, + ) + } + } + } + } + } + + private fun getLegacyCryptoCurrencies(): TokensListCryptoCurrencies { + val wallets = store.state.walletState.walletsDataFromStores + val derivationStyle = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle() + + return TokensListCryptoCurrencies( + coins = wallets.toNonCustomBlockchains(derivationStyle), + tokens = wallets.toNonCustomTokensWithBlockchains(derivationStyle), + ) + } + + private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { + return this + .mapNotNull { walletDataModel -> + if (walletDataModel.currency.isCustomCurrency(derivationStyle)) { + null + } else { + (walletDataModel.currency as? Currency.Blockchain)?.blockchain + } + } + .distinct() + } + + private fun List.toNonCustomTokensWithBlockchains( + derivationStyle: DerivationStyle?, + ): List { + return this + .mapNotNull { walletDataModel -> + if (walletDataModel.currency !is Currency.Token) return@mapNotNull null + if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null + + TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain) + } + .distinct() + } + + fun onSaveButtonClick( + currentTokensList: List, + currentBlockchainList: List, + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + if (walletFeatureToggles.isRedesignedScreenEnabled) { + saveByNewWay(changedTokensList = changedTokensList, changedBlockchainList = changedBlockchainList) + } else { + saveByOldWay(currentTokensList, currentBlockchainList, changedTokensList, changedBlockchainList) + } + } + + private fun saveByNewWay( + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + store.dispatch( + action = TokensAction.NewSaveChanges( + currentTokens = currentNewTokens, + currentCoins = currentNewCoins, + changedTokens = changedTokensList.mapNotNull { + cryptoCurrencyFactory.createToken( + sdkToken = it.token, + blockchain = it.blockchain, + extraDerivationPath = null, + derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, + ) + }, + changedCoins = changedBlockchainList.mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = it, + extraDerivationPath = null, + derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider, + ) + }, + userWallet = currentUserWallet, + ), + ) + } + + private fun saveByOldWay( + currentTokensList: List, + currentBlockchainList: List, + changedTokensList: MutableList, + changedBlockchainList: List, + ) { + val scanResponse = store.state.globalState.scanResponse ?: return + + store.dispatch( + action = TokensAction.LegacySaveChanges( + currentTokens = currentTokensList, + currentBlockchains = currentBlockchainList, + changedTokens = changedTokensList, + changedBlockchains = changedBlockchainList, + scanResponse = scanResponse, + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 78ff6ff6ce..05237c3172 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -19,6 +19,10 @@ import com.tangem.domain.common.extensions.canHandleToken import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getNetworkName @@ -26,14 +30,11 @@ import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.tap.features.tokens.impl.domain.models.Token.Network import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState -import com.tangem.tap.features.tokens.impl.presentation.models.TokensListArgs import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState -import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.store import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider @@ -43,9 +44,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.coroutines.plus import timber.log.Timber import javax.inject.Inject +import kotlin.properties.Delegates import com.tangem.blockchain.common.Token as BlockchainToken /** @@ -59,6 +62,7 @@ import com.tangem.blockchain.common.Token as BlockchainToken * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") @HiltViewModel internal class TokensListViewModel @Inject constructor( private val interactor: TokensListInteractor, @@ -66,9 +70,12 @@ internal class TokensListViewModel @Inject constructor( private val dispatchers: AppCoroutineDispatcherProvider, private val reduxStateHolder: AppStateHolder, analyticsEventHandler: AnalyticsEventHandler, + getCurrenciesUseCase: GetCryptoCurrenciesUseCase, + getSelectedWalletUseCase: GetSelectedWalletUseCase, + walletFeatureToggles: WalletFeatureToggles, ) : ViewModel(), DefaultLifecycleObserver { - private val args = TokensListArgs() + private val isManageAccess = store.state.tokensState.isManageAccess private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler) private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer()) @@ -76,15 +83,36 @@ internal class TokensListViewModel @Inject constructor( var uiState by mutableStateOf(value = getInitialUiState()) private set - private val changedTokensList: MutableList = args.mainScreenTokenList.toMutableList() - private val changedBlockchainList: MutableList = args.mainScreenBlockchainList.toMutableList() + private var currentTokensList: List by Delegates.notNull() + private var currentBlockchainList: List by Delegates.notNull() + + private var changedTokensList: MutableList by Delegates.notNull() + private var changedBlockchainList: MutableList by Delegates.notNull() + + private val tokensListMigration = TokensListMigration( + walletFeatureToggles = walletFeatureToggles, + getSelectedWalletUseCase = getSelectedWalletUseCase, + getCurrenciesUseCase = getCurrenciesUseCase, + ) + + init { + viewModelScope.launch(dispatchers.main) { + val (currentCoins, currentTokens) = tokensListMigration.getCurrentCryptoCurrencies() + + currentBlockchainList = currentCoins + currentTokensList = currentTokens + + changedBlockchainList = currentCoins.toMutableList() + changedTokensList = currentTokens.toMutableList() + } + } override fun onCreate(owner: LifecycleOwner) { - if (args.isManageAccess) analyticsSender.sendWhenScreenOpened() + if (isManageAccess) analyticsSender.sendWhenScreenOpened() } private fun getInitialUiState(): TokensListStateHolder { - return if (args.isManageAccess) { + return if (isManageAccess) { TokensListStateHolder.ManageContent( toolbarState = getInitialToolbarState(), isLoading = true, @@ -105,7 +133,7 @@ internal class TokensListViewModel @Inject constructor( } private fun getInitialToolbarState(): TokensListToolbarState { - return if (args.isManageAccess) { + return if (isManageAccess) { TokensListToolbarState.Title.Manage( titleResId = R.string.add_tokens_title, onBackButtonClick = actionsHandler::onBackButtonClick, @@ -130,7 +158,7 @@ internal class TokensListViewModel @Inject constructor( return interactor.getTokensList(searchText = searchText).map { it.map { token -> - if (args.isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) + if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) } } } @@ -264,7 +292,12 @@ internal class TokensListViewModel @Inject constructor( fun onSaveButtonClick() { analyticsSender.sendWhenSaveButtonClicked() - store.dispatch(TokensAction.SaveChanges(changedTokensList, changedBlockchainList)) + tokensListMigration.onSaveButtonClick( + currentTokensList = currentTokensList, + currentBlockchainList = currentBlockchainList, + changedTokensList = changedTokensList, + changedBlockchainList = changedBlockchainList, + ) } private fun onSearchValueChange(newValue: String) { @@ -291,7 +324,7 @@ internal class TokensListViewModel @Inject constructor( if (isRemoveAction) { val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain } - val isAddedOnMainScreen = args.mainScreenBlockchainList.contains(blockchain) + val isAddedOnMainScreen = currentBlockchainList.contains(blockchain) if (isTokenWithSameBlockchainFound) { router.openUnableHideMainTokenAlert( @@ -341,7 +374,7 @@ internal class TokensListViewModel @Inject constructor( val isRemoveAction = changedTokensList.contains(token) if (isRemoveAction) { - val isAddedOnMainScreen = args.mainScreenTokenList.contains(token) + val isAddedOnMainScreen = currentTokensList.contains(token) if (isAddedOnMainScreen) { router.openRemoveWalletAlert( diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt deleted file mode 100644 index c24f982fca..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.features.tokens.legacy.redux - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.tap.domain.model.WalletDataModel -import org.rekotlin.Action - -sealed interface TokensAction : Action { - - /** Single way to pass data to the screen */ - sealed interface SetArgs : TokensAction { - - data class ManageAccess(val wallets: List, val derivationStyle: DerivationStyle?) : SetArgs - - object ReadAccess : SetArgs - } - - // TODO: [REDACTED_TASK_KEY] Remove this action - data class SaveChanges(val tokens: List, val blockchains: List) : TokensAction - - // TODO: Remove this action in 4.7 release - object PrepareAndNavigateToAddCustomToken : TokensAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt index 2785c63857..791e0f1547 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt @@ -9,29 +9,25 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.flatMap -import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.DomainWrapped import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.common.util.supportsHdWallet -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.domainStore +import com.tangem.domain.tokens.TokenWithBlockchain +import com.tangem.domain.tokens.TokensAction +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* -import com.tangem.tap.common.analytics.events.ManageTokens import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware @@ -43,29 +39,69 @@ object TokensMiddleware { { next -> { action -> when (action) { - is TokensAction.SaveChanges -> handleSaveChanges(action) - is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken() + is TokensAction.LegacySaveChanges -> handleLegacySaveChanges(action) + is TokensAction.NewSaveChanges -> handleNewSaveChanges(action) } next(action) } } } - private fun handleSaveChanges(action: TokensAction.SaveChanges) { + private fun handleNewSaveChanges(action: TokensAction.NewSaveChanges) { scope.launch { - val scanResponse = store.state.globalState.scanResponse ?: return@launch + val scanResponse = action.userWallet.scanResponse - val currentTokens = store.state.tokensState.addedTokens - val currentBlockchains = store.state.tokensState.addedBlockchains + val currentTokens = action.currentTokens + val currentBlockchains = action.currentCoins - val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains) - val blockchainsToRemove = - store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains) + val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains) + val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains) - val tokensToAdd = action.tokens.filterNot(currentTokens::contains) - val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } } + val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains) + val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } } - removeCurrenciesIfNeeded( + removeNewCurrenciesIfNeeded( + userWalletId = action.userWallet.walletId, + currencies = blockchainsToRemove + tokensToRemove, + ) + + val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty() + val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() + if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { + store.dispatchDebugErrorNotification(message = "Nothing to save") + store.dispatchOnMain(NavigationAction.PopBackTo()) + return@launch + } + + val currencyList = blockchainsToAdd + tokensToAdd + + if (scanResponse.supportsHdWallet()) { + deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) { + submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + } else { + submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList) + store.dispatchOnMain(NavigationAction.PopBackTo()) + } + } + } + + private fun handleLegacySaveChanges(action: TokensAction.LegacySaveChanges) { + scope.launch { + val scanResponse = action.scanResponse + + val currentTokens = action.currentTokens + val currentBlockchains = action.currentBlockchains + + val blockchainsToAdd = action.changedBlockchains.filterNot(currentBlockchains::contains) + val blockchainsToRemove = currentBlockchains.filterNot(action.changedBlockchains::contains) + + val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains) + val tokensToRemove = + currentTokens.filterNot { token -> action.changedTokens.any { it.token == token.token } } + + removeLegacyCurrenciesIfNeeded( currencies = convertToCurrencies( blockchains = blockchainsToRemove, tokens = tokensToRemove, @@ -89,11 +125,11 @@ object TokensMiddleware { if (scanResponse.supportsHdWallet()) { deriveMissingBlockchains(scanResponse, currencyList) { - submitAdd(it, currencyList) + submitLegacyAdd(it, currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } else { - submitAdd(scanResponse, currencyList) + submitLegacyAdd(scanResponse, currencyList) store.dispatchOnMain(NavigationAction.PopBackTo()) } } @@ -104,15 +140,14 @@ object TokensMiddleware { tokens: List, derivationStyle: DerivationStyle?, ): List { - return blockchains.map { - Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) - } + tokens.map { - Currency.Token( - it.token, - it.blockchain, - it.blockchain.derivationPath(derivationStyle)?.rawPath, - ) - } + return blockchains.map { Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) } + + tokens.map { + Currency.Token( + token = it.token, + blockchain = it.blockchain, + derivationPath = it.blockchain.derivationPath(derivationStyle)?.rawPath, + ) + } } private fun deriveMissingBlockchains( @@ -123,7 +158,7 @@ object TokensMiddleware { val config = CardConfig.createConfig(scanResponse.card) val derivationDataList = currencyList.mapNotNull { currency -> val curve = config.primaryCurve(currency.blockchain) - curve?.let { getDerivations(curve, scanResponse, currency) } + curve?.let { getLegacyDerivations(curve, scanResponse, currency) } } val derivations = buildMap> { derivationDataList.forEach { @@ -174,7 +209,55 @@ object TokensMiddleware { } } - private fun getDerivations(curve: EllipticCurve, scanResponse: ScanResponse, currency: Currency): DerivationData? { + private fun deriveMissingCoins( + scanResponse: ScanResponse, + currencyList: List, + onSuccess: (ScanResponse) -> Unit, + ) { + val config = CardConfig.createConfig(scanResponse.card) + val derivationDataList = currencyList.mapNotNull { + config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value)) + ?.let { curve -> getNewDerivations(curve, scanResponse, currencyList) } + } + val derivations = derivationDataList.associate(DerivationData::derivations) + if (derivations.isEmpty()) { + onSuccess(scanResponse) + return + } + + scope.launch { + val result = tangemSdkManager.derivePublicKeys( + cardId = null, + derivations = derivations, + ) + when (result) { + is CompletionResult.Success -> { + val newDerivedKeys = result.data.entries + val oldDerivedKeys = scanResponse.derivedKeys + + val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet() + + val updatedDerivedKeys = walletKeys.associateWith { walletKey -> + val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap()) + val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap()) + ExtendedPublicKeysMap(oldDerivations + newDerivations) + } + val updatedScanResponse = scanResponse.copy( + derivedKeys = updatedDerivedKeys, + ) + store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse)) + delay(DELAY_SDK_DIALOG_CLOSE) + + onSuccess(updatedScanResponse) + } + is CompletionResult.Failure -> { + store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens")) + } + } + } + } + + private fun getLegacyDerivations(curve: EllipticCurve, scanResponse: ScanResponse, currency: Currency): DerivationData? { val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null val supportedCurves = currency.blockchain.getSupportedCurves() @@ -205,9 +288,48 @@ object TokensMiddleware { return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) } + private fun getNewDerivations( + curve: EllipticCurve, + scanResponse: ScanResponse, + currencyList: List, + ): DerivationData? { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null + + val manageTokensCandidates = currencyList + .map { Blockchain.fromId(it.network.id.value) } + .distinct() + .filter { it.getSupportedCurves().contains(curve) } + .mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) } + + val customTokensCandidates = currencyList + .filter { Blockchain.fromId(it.network.id.value).getSupportedCurves().contains(curve) } + .mapNotNull { it.network.derivationPath.value } + .map(::DerivationPath) + + val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList() + if (bothCandidates.isEmpty()) return null + + currencyList.find { it is CryptoCurrency.Coin && Blockchain.fromId(it.network.id.value) == Blockchain.Cardano } + ?.let { currency -> + currency.network.derivationPath.value?.let { + bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it))) + } + } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys: ExtendedPublicKeysMap = + scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap()) + val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList() + + val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } + if (toDerive.isEmpty()) return null + + return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) + } + class DerivationData(val derivations: Pair>) - private fun submitAdd(scanResponse: ScanResponse, currencyList: List) { + private fun submitLegacyAdd(scanResponse: ScanResponse, currencyList: List) { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to add currencies, no user wallet selected") return @@ -228,7 +350,15 @@ object TokensMiddleware { } } - private suspend fun removeCurrenciesIfNeeded(currencies: List) { + private fun submitNewAdd(userWalletId: UserWalletId, currencyList: List) { + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) + + scope.launch { + currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList) + } + } + + private suspend fun removeLegacyCurrenciesIfNeeded(currencies: List) { if (currencies.isEmpty()) return val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { Timber.e("Unable to remove currencies, no user wallet selected") @@ -237,55 +367,10 @@ object TokensMiddleware { walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies) } - private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean { - return currency.derivationPath?.let { - !scanResponse.hasDerivation(currency.blockchain, it) - } ?: false - } + private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) return + val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository) - private fun handleAddingCustomToken() = scope.launch { - val onAddCustomToken = fun(customCurrency: CustomCurrency) { - val scanResponse = store.state.globalState.scanResponse ?: return - - fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List) { - submitAdd(scanResponse, currencyList) - // pop from the AddCustomTokenScreen - store.dispatchOnMain(NavigationAction.PopBackTo()) - store.dispatchOnMain(NavigationAction.PopBackTo()) - } - - Analytics.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency)) - val currency = Currency.fromCustomCurrency(customCurrency) - val isNeedToDerive = isNeedToDerive(scanResponse, currency) - val currencyList = listOf(currency) - if (isNeedToDerive) { - deriveMissingBlockchains(scanResponse, currencyList) { - submitAndPopBack(it, currencyList) - } - } else { - submitAndPopBack(scanResponse, currencyList) - } - } - - val addedCurrencies = store.state.walletState.walletsStores - .map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) } - .flatten() - .map { currency -> - when (currency) { - is Currency.Blockchain -> DomainWrapped.Currency.Blockchain( - currency.blockchain, - currency.derivationPath, - ) - - is Currency.Token -> DomainWrapped.Currency.Token( - currency.token, - currency.blockchain, - currency.derivationPath, - ) - } - } - domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies)) - domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) + currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt index 8e33fdc8f4..389a02219a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensReducer.kt @@ -1,11 +1,7 @@ package com.tangem.tap.features.tokens.legacy.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.model.WalletDataModel -import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.models.Currency.Token import org.rekotlin.Action object TokensReducer { @@ -16,40 +12,8 @@ private fun internalReduce(action: Action, state: AppState): TokensState { if (action !is TokensAction) return state.tokensState return when (action) { - is TokensAction.SetArgs.ManageAccess -> { - state.tokensState.copy( - isManageAccess = true, - addedWallets = action.wallets, - addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle), - addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle), - ) - } - - is TokensAction.SetArgs.ReadAccess -> { - state.tokensState.copy(isManageAccess = false) - } - + is TokensAction.SetArgs.ManageAccess -> state.tokensState.copy(isManageAccess = true) + is TokensAction.SetArgs.ReadAccess -> state.tokensState.copy(isManageAccess = false) else -> state.tokensState } -} - -private fun List.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List { - return mapNotNull { walletDataModel -> - if (walletDataModel.currency.isCustomCurrency(derivationStyle)) { - null - } else { - (walletDataModel.currency as? Currency.Blockchain)?.blockchain - } - }.distinct() -} - -private fun List.toNonCustomTokensWithBlockchains( - derivationStyle: DerivationStyle?, -): List { - return mapNotNull { walletDataModel -> - if (walletDataModel.currency !is Token) return@mapNotNull null - if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null - - TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain) - }.distinct() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt index 156fd0c922..14c5d9d721 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt @@ -1,16 +1,5 @@ package com.tangem.tap.features.tokens.legacy.redux -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.tap.domain.model.WalletDataModel import org.rekotlin.StateType -data class TokensState( - val isManageAccess: Boolean = false, - val addedWallets: List = emptyList(), - val addedTokens: List = emptyList(), - val addedBlockchains: List = emptyList(), -) : StateType - -// TODO: [REDACTED_TASK_KEY] Remove this class -data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ No newline at end of file +data class TokensState(val isManageAccess: Boolean = false) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt index 8b22dd95e6..8506d82036 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/converters/CryptoCurrencyConverter.kt @@ -1,13 +1,15 @@ package com.tangem.tap.features.wallet.converters +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter -internal class CryptoCurrencyConverter : Converter { +internal class CryptoCurrencyConverter : TwoWayConverter { private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } @@ -16,6 +18,7 @@ internal class CryptoCurrencyConverter : Converter { is Currency.Blockchain -> requireNotNull( cryptoCurrencyFactory.createCoin( blockchain = value.blockchain, + extraDerivationPath = value.derivationPath, derivationStyleProvider = requireNotNull( store.state.globalState .userWalletsListManager @@ -29,6 +32,7 @@ internal class CryptoCurrencyConverter : Converter { cryptoCurrencyFactory.createToken( sdkToken = value.token, blockchain = value.blockchain, + extraDerivationPath = value.derivationPath, derivationStyleProvider = requireNotNull( store.state.globalState .userWalletsListManager @@ -40,4 +44,26 @@ internal class CryptoCurrencyConverter : Converter { ) } } + + override fun convertBack(value: CryptoCurrency): Currency { + val blockchain = Blockchain.fromId(value.network.id.value) + if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain") + return when (value) { + is CryptoCurrency.Coin -> Currency.Blockchain( + blockchain = blockchain, + derivationPath = value.network.derivationPath.value, + ) + is CryptoCurrency.Token -> Currency.Token( + token = Token( + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + id = value.id.value, + ), + blockchain = blockchain, + derivationPath = value.network.derivationPath.value, + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 2528753b4e..f0110e52fa 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -11,16 +11,17 @@ import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDebugErrorNotification +import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.redux.AppState +import com.tangem.tap.domain.TapError import com.tangem.tap.domain.tokens.getIconUrl import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE @@ -39,7 +40,10 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency +@Suppress("LargeClass") class TradeCryptoMiddleware { + + @Suppress("LongMethod", "CyclomaticComplexMethod") fun handle(state: () -> AppState?, action: TradeCryptoAction) { if (DemoHelper.tryHandle(state, action)) return @@ -52,11 +56,10 @@ class TradeCryptoMiddleware { openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()) } is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) - TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send()) is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) - is TradeCryptoAction.New.Swap -> { - openSwap(currency = action.cryptoCurrency.toSwapCurrency()) - } + is TradeCryptoAction.New.Swap -> openSwap(currency = action.cryptoCurrency.toSwapCurrency()) + is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action) + is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action) } } @@ -124,9 +127,7 @@ class TradeCryptoMiddleware { .getOrCreateWalletManager( userWallet = action.userWallet, blockchain = blockchain, - derivationPath = blockchain.derivationPath( - style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(), - ), + derivationPath = currency.network.derivationPath.value, ) if (walletManager !is EthereumWalletManager) { @@ -301,4 +302,94 @@ class TradeCryptoMiddleware { ) } } + + private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) { + val cryptoStatus = action.tokenStatus + val currency = cryptoStatus.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWallet = action.userWallet, + blockchain = blockchain, + derivationPath = currency.network.derivationPath.value, + ) + + if (walletManager == null) { + val error = TapError.UnsupportedState(stateError = "WalletManager is null") + FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type is AmountType.Token } + when (currency) { + is CryptoCurrency.Coin -> error("Action.tokenStatus.currency is Coin") + is CryptoCurrency.Token -> { + store.dispatchOnMain( + action = PrepareSendScreen( + walletManager = walletManager, + coinAmount = walletManager.wallet.amounts[AmountType.Coin], + coinRate = action.coinFiatRate, + tokenAmount = sendableAmounts.first(), + tokenRate = cryptoStatus.value.fiatRate, + ), + ) + } + } + + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) + } + } + + private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) { + val cryptoStatus = action.coinStatus + val currency = cryptoStatus.currency + val blockchain = Blockchain.fromId(currency.network.id.value) + + scope.launch { + val walletManager = store.state.daggerGraphState + .get(DaggerGraphState::walletManagersFacade) + .getOrCreateWalletManager( + userWallet = action.userWallet, + blockchain = blockchain, + derivationPath = currency.network.derivationPath.value, + ) + + if (walletManager == null) { + val error = TapError.UnsupportedState(stateError = "WalletManager is null") + FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin } + when (currency) { + is CryptoCurrency.Coin -> { + val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol } + + if (amountToSend == null) { + val error = TapError.UnsupportedState(stateError = "Amount to send is null") + FirebaseCrashlytics.getInstance() + .recordException(IllegalStateException(error.stateError)) + store.dispatchErrorNotification(error) + return@launch + } + + store.dispatchOnMain( + action = PrepareSendScreen( + walletManager = walletManager, + coinAmount = amountToSend, + coinRate = cryptoStatus.value.fiatRate, + ), + ) + } + is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token") + } + + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index 44a7dd5341..e908e27b26 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -154,9 +154,8 @@ class WarningsMiddleware { store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId) } is SimpleResult.Failure -> - if (result.error is BlockchainSdkError.SignatureCountNotMatched) { - addWarningMessage(alreadySignedHashesWarning, true) - } else if (signedHashes > 0) { + if (signedHashes > 0 || result.error is BlockchainSdkError.SignatureCountNotMatched) { + alreadySignedHashesWarning.isHidden = false addWarningMessage(alreadySignedHashesWarning, true) } null -> Unit diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index cbb33a7dcd..67c74962d6 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -37,7 +37,6 @@ import com.tangem.tap.common.utils.SafeStoreSubscriber import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.statePrinter.printScanResponseState import com.tangem.tap.domain.statePrinter.printWalletState -import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.WalletAction @@ -309,17 +308,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber { store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) - store.state.globalState.scanResponse?.let { scanResponse -> - store.dispatch( - DetailsAction.PrepareScreen( - scanResponse = scanResponse, - wallets = store.state.walletState.walletManagers.map { it.wallet }, - ), - ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.Details)) - true - } - false + store.dispatch(NavigationAction.NavigateTo(AppScreen.Details)) + + true } else -> super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 8f17e52682..290bd68881 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -6,7 +6,7 @@ import com.badoo.mvicore.modelWatcher import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction -import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.entities.FiatCurrency @@ -14,7 +14,6 @@ import com.tangem.tap.common.extensions.getQuantityString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.model.TotalFiatBalance -import com.tangem.tap.features.tokens.legacy.redux.TokensAction import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState @@ -106,14 +105,8 @@ class MultiWalletView : WalletView() { binding.btnAddToken.setOnClickListener { Analytics.send(Portfolio.ButtonManageTokens()) - store.dispatch( - TokensAction.SetArgs.ManageAccess( - wallets = state.walletsDataFromStores, - derivationStyle = store.state.globalState.scanResponse - ?.derivationStyleProvider?.getDerivationStyle(), - ), - ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) + store.dispatch(action = TokensAction.SetArgs.ManageAccess) + store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.AddTokens)) } handleErrorStates(state = state, binding = binding, fragment = fragment) } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt index 472a4322ec..0993a1c9ea 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt @@ -177,7 +177,7 @@ private fun Footer( PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource( - id = R.string.user_wallet_list_unlock_all, + id = R.string.user_wallet_list_unlock_all_with, stringResource(id = R.string.common_biometrics), ), showProgress = showUnlockProgress, diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt new file mode 100644 index 0000000000..9c9184e488 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.network.exchangeServices + +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter + +class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager { + + private val cryptoCurrencyConverter = CryptoCurrencyConverter() + override fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean { + return exchangeService?.availableForBuy( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) ?: false + } + + override fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean { + return exchangeService?.availableForSell( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) ?: false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index cdcbc7fd9a..d19842021e 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -14,6 +14,7 @@ import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.network.exchangeServices.ExchangeService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import org.rekotlin.Action @@ -45,6 +46,7 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl var tangemSdkManager: TangemSdkManager? = null var walletStoresManager: WalletStoresManager? = null var appFiatCurrency: FiatCurrency = FiatCurrency.Default + var exchangeService: ExchangeService? = null fun getActualCard(): CardDTO? { return scanResponse?.card @@ -56,6 +58,10 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl override fun getBackStack(): List = mainStore?.state?.navigationState?.backStack.orEmpty() + override fun popBackStack(screen: AppScreen?) { + mainStore?.dispatch(NavigationAction.PopBackTo(screen)) + } + override fun dispatch(action: Action) { mainStore?.dispatch(action) } diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index 19474ce5cc..4a4acf7415 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.proxy.di import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver @@ -9,6 +10,7 @@ import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager +import com.tangem.tap.features.details.DarkThemeFeatureToggle import com.tangem.tap.proxy.* import dagger.Module import dagger.Provides @@ -58,6 +60,12 @@ class ProxyModule { ) } + @Provides + @Singleton + fun provideDarkThemeFeatureToggle(featureTogglesManager: FeatureTogglesManager): DarkThemeFeatureToggle { + return DarkThemeFeatureToggle(featureTogglesManager) + } + // regions FeatureConsumers @Provides @Singleton diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index c677f70300..cd68dae6e3 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -3,9 +3,12 @@ package com.tangem.tap.proxy.redux import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles @@ -16,6 +19,8 @@ import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles +import com.tangem.tap.features.details.featuretoggles.DetailsFeatureToggles +import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.StateType data class DaggerGraphState( @@ -35,6 +40,13 @@ data class DaggerGraphState( val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, val walletManagersFacade: WalletManagersFacade? = null, + val appStateHolder: AppStateHolder? = null, + val appThemeModeRepository: AppThemeModeRepository? = null, + val balanceHidingRepository: BalanceHidingRepository? = null, + val detailsFeatureToggles: DetailsFeatureToggles? = null, + + // FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList + val currenciesRepository: CurrenciesRepository? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/app/src/main/res/color/selector_chip_background.xml b/app/src/main/res/color/selector_chip_background.xml index e0b0431767..16be87b913 100644 --- a/app/src/main/res/color/selector_chip_background.xml +++ b/app/src/main/res/color/selector_chip_background.xml @@ -1,10 +1,10 @@ - - - + + + - - + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_stroke.xml b/app/src/main/res/color/selector_chip_stroke.xml index e4a5d64acc..c2827e783e 100644 --- a/app/src/main/res/color/selector_chip_stroke.xml +++ b/app/src/main/res/color/selector_chip_stroke.xml @@ -1,9 +1,9 @@ - - - - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_text.xml b/app/src/main/res/color/selector_chip_text.xml new file mode 100644 index 0000000000..e205f4e82d --- /dev/null +++ b/app/src/main/res/color/selector_chip_text.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_edit_text.xml b/app/src/main/res/color/selector_edit_text.xml index b6c0697011..192514f802 100644 --- a/app/src/main/res/color/selector_edit_text.xml +++ b/app/src/main/res/color/selector_edit_text.xml @@ -1,7 +1,7 @@ - - - - + + + + \ No newline at end of file diff --git a/app/src/main/res/color/selector_edit_text_secondary.xml b/app/src/main/res/color/selector_edit_text_secondary.xml new file mode 100644 index 0000000000..df06469793 --- /dev/null +++ b/app/src/main/res/color/selector_edit_text_secondary.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_activation_success.xml b/app/src/main/res/drawable/ic_activation_success.xml deleted file mode 100644 index 7957c82493..0000000000 --- a/app/src/main/res/drawable/ic_activation_success.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/ic_arrows_up_down.xml b/app/src/main/res/drawable/ic_arrows_up_down.xml index 6e7d34d5e6..96faa98e71 100644 --- a/app/src/main/res/drawable/ic_arrows_up_down.xml +++ b/app/src/main/res/drawable/ic_arrows_up_down.xml @@ -4,6 +4,6 @@ android:viewportWidth="14" android:viewportHeight="19"> diff --git a/app/src/main/res/drawable/ic_discord.xml b/app/src/main/res/drawable/ic_discord.xml index 88444ddf14..6502165e77 100644 --- a/app/src/main/res/drawable/ic_discord.xml +++ b/app/src/main/res/drawable/ic_discord.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M19.636,4.924C18.212,4.259 16.689,3.775 15.097,3.5C14.902,3.853 14.673,4.327 14.516,4.705C12.824,4.451 11.147,4.451 9.486,4.705C9.328,4.327 9.095,3.853 8.897,3.5C7.304,3.775 5.779,4.26 4.355,4.927C1.483,9.26 0.704,13.486 1.093,17.651C2.999,19.071 4.845,19.934 6.66,20.498C7.108,19.882 7.508,19.228 7.852,18.538C7.196,18.289 6.568,17.983 5.975,17.626C6.132,17.51 6.286,17.388 6.435,17.263C10.055,18.953 13.988,18.953 17.565,17.263C17.715,17.388 17.869,17.51 18.025,17.626C17.43,17.984 16.8,18.291 16.144,18.54C16.489,19.228 16.886,19.884 17.336,20.5C19.153,19.935 21.001,19.073 22.906,17.651C23.363,12.822 22.126,8.636 19.636,4.924ZM8.345,15.089C7.259,15.089 6.368,14.076 6.368,12.843C6.368,11.61 7.24,10.596 8.345,10.596C9.451,10.596 10.342,11.608 10.323,12.843C10.325,14.076 9.451,15.089 8.345,15.089ZM15.655,15.089C14.568,15.089 13.677,14.076 13.677,12.843C13.677,11.61 14.549,10.596 15.655,10.596C16.76,10.596 17.651,11.608 17.632,12.843C17.632,14.076 16.76,15.089 15.655,15.089Z" + android:fillColor="#909090"/> diff --git a/app/src/main/res/drawable/ic_dot.xml b/app/src/main/res/drawable/ic_dot.xml index 321894f58a..bef54d1532 100644 --- a/app/src/main/res/drawable/ic_dot.xml +++ b/app/src/main/res/drawable/ic_dot.xml @@ -7,7 +7,7 @@ android:thickness="4.5dp" android:useLevel="false"> diff --git a/app/src/main/res/drawable/ic_facebook.xml b/app/src/main/res/drawable/ic_facebook.xml index 9343c46fe7..1db025e42c 100644 --- a/app/src/main/res/drawable/ic_facebook.xml +++ b/app/src/main/res/drawable/ic_facebook.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml index 57bbd79618..daa1de7021 100644 --- a/app/src/main/res/drawable/ic_github.xml +++ b/app/src/main/res/drawable/ic_github.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_instagram.xml b/app/src/main/res/drawable/ic_instagram.xml index 844639d9e3..76744cf7f5 100644 --- a/app/src/main/res/drawable/ic_instagram.xml +++ b/app/src/main/res/drawable/ic_instagram.xml @@ -1,10 +1,19 @@ - - - - - - - + + + + + + + diff --git a/app/src/main/res/drawable/ic_linkedin.xml b/app/src/main/res/drawable/ic_linkedin.xml index 36600f605d..2df3cb834a 100644 --- a/app/src/main/res/drawable/ic_linkedin.xml +++ b/app/src/main/res/drawable/ic_linkedin.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_paste.xml b/app/src/main/res/drawable/ic_paste.xml index c49d12dc4b..b971249451 100644 --- a/app/src/main/res/drawable/ic_paste.xml +++ b/app/src/main/res/drawable/ic_paste.xml @@ -4,6 +4,6 @@ android:viewportWidth="16" android:viewportHeight="19"> diff --git a/app/src/main/res/drawable/ic_paste_disabled.xml b/app/src/main/res/drawable/ic_paste_disabled.xml index 7666c5a52d..156ddafc4b 100644 --- a/app/src/main/res/drawable/ic_paste_disabled.xml +++ b/app/src/main/res/drawable/ic_paste_disabled.xml @@ -6,6 +6,6 @@ android:viewportHeight="19"> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_qr_code_scan.xml b/app/src/main/res/drawable/ic_qr_code_scan.xml index 34a2bf62a8..5ec455e546 100644 --- a/app/src/main/res/drawable/ic_qr_code_scan.xml +++ b/app/src/main/res/drawable/ic_qr_code_scan.xml @@ -5,5 +5,5 @@ android:viewportHeight="18"> + android:fillColor="@color/text_primary_1" /> diff --git a/app/src/main/res/drawable/ic_reddit.xml b/app/src/main/res/drawable/ic_reddit.xml new file mode 100644 index 0000000000..2a1e04a004 --- /dev/null +++ b/app/src/main/res/drawable/ic_reddit.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_selected_dot.xml b/app/src/main/res/drawable/ic_selected_dot.xml index e847725fc2..f747ab54ba 100644 --- a/app/src/main/res/drawable/ic_selected_dot.xml +++ b/app/src/main/res/drawable/ic_selected_dot.xml @@ -6,7 +6,7 @@ android:shape="ring" android:thickness="4.5dp" android:useLevel="false"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_telegram.xml b/app/src/main/res/drawable/ic_telegram.xml index 5d2351d73e..020ee3bc71 100644 --- a/app/src/main/res/drawable/ic_telegram.xml +++ b/app/src/main/res/drawable/ic_telegram.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_twitter.xml b/app/src/main/res/drawable/ic_twitter.xml index de09724525..1c3cb6cfef 100644 --- a/app/src/main/res/drawable/ic_twitter.xml +++ b/app/src/main/res/drawable/ic_twitter.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_walletconnect.xml b/app/src/main/res/drawable/ic_walletconnect.xml index 925a858a8a..d4b4c99942 100644 --- a/app/src/main/res/drawable/ic_walletconnect.xml +++ b/app/src/main/res/drawable/ic_walletconnect.xml @@ -1,5 +1,7 @@ - + diff --git a/app/src/main/res/drawable/ic_youtube.xml b/app/src/main/res/drawable/ic_youtube.xml index 81eb8d7e8c..6d5ade9d08 100644 --- a/app/src/main/res/drawable/ic_youtube.xml +++ b/app/src/main/res/drawable/ic_youtube.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ill_reset_background.xml b/app/src/main/res/drawable/ill_reset_background.xml deleted file mode 100644 index 8ce3137945..0000000000 --- a/app/src/main/res/drawable/ill_reset_background.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/img_alert.xml b/app/src/main/res/drawable/img_alert.xml new file mode 100644 index 0000000000..42b087d91c --- /dev/null +++ b/app/src/main/res/drawable/img_alert.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/img_onboarding_success.xml b/app/src/main/res/drawable/img_onboarding_success.xml new file mode 100644 index 0000000000..84574803ea --- /dev/null +++ b/app/src/main/res/drawable/img_onboarding_success.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/shape_ellipse.xml b/app/src/main/res/drawable/shape_ellipse.xml index b5f3fc7939..ca6cad24b5 100644 --- a/app/src/main/res/drawable/shape_ellipse.xml +++ b/app/src/main/res/drawable/shape_ellipse.xml @@ -10,7 +10,7 @@ diff --git a/app/src/main/res/drawable/shape_refresh_button.xml b/app/src/main/res/drawable/shape_refresh_button.xml index ac9508438d..cade7f63f3 100644 --- a/app/src/main/res/drawable/shape_refresh_button.xml +++ b/app/src/main/res/drawable/shape_refresh_button.xml @@ -2,11 +2,11 @@ - + + android:color="@color/button_secondary" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml index 32d2544981..131299d4d5 100644 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml @@ -85,7 +85,7 @@ android:layout_gravity="center" android:elevation="18dp" android:indeterminate="true" - android:indeterminateTint="@color/backgroundLightGray" + android:indeterminateTint="@color/background_secondary" android:visibility="invisible" /> @@ -93,6 +93,9 @@ + app:srcCompat="@drawable/ic_angle_bracket_up" + app:tint="@color/icon_primary_1" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_onboarding_address_info.xml b/app/src/main/res/layout/dialog_onboarding_address_info.xml index eadb8f7f36..b4a422e77b 100644 --- a/app/src/main/res/layout/dialog_onboarding_address_info.xml +++ b/app/src/main/res/layout/dialog_onboarding_address_info.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="502dp"> + android:layout_height="wrap_content" + android:background="@color/background_primary"> @@ -52,13 +54,14 @@ android:layout_marginStart="32dp" android:layout_marginEnd="8dp" android:background="@drawable/shape_rectangle_rounded_100" - android:backgroundTint="@color/lightGray0" + android:layout_marginTop="30dp" android:paddingStart="16dp" android:paddingEnd="16dp" app:layout_constraintEnd_toStartOf="@+id/btn_fl_share" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/guideline3"> + android:backgroundTint="@color/button_secondary" + app:layout_constraintTop_toBottomOf="@id/tv_receive_message"> @@ -98,13 +103,14 @@ android:layout_height="40dp" android:layout_marginEnd="32dp" android:background="@drawable/shape_rectangle_rounded_100" - android:backgroundTint="@color/lightGray0" + android:layout_marginTop="30dp" android:paddingStart="16dp" android:paddingEnd="16dp" + android:backgroundTint="@color/background_primary" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/btn_fl_copy_address" - app:layout_constraintTop_toTopOf="@+id/guideline3"> + app:layout_constraintTop_toBottomOf="@+id/tv_receive_message"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_russians_cardholders_warning.xml b/app/src/main/res/layout/dialog_russians_cardholders_warning.xml index 29bf1c052b..ae04b02d83 100644 --- a/app/src/main/res/layout/dialog_russians_cardholders_warning.xml +++ b/app/src/main/res/layout/dialog_russians_cardholders_warning.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/backgroundWhite" + android:background="@color/background_primary" android:minHeight="420dp" tools:layout_gravity="bottom"> @@ -25,7 +25,7 @@ android:layout_width="32dp" android:layout_height="32dp" android:background="@drawable/shape_circle" - android:backgroundTint="@color/backgroundWhite" + android:backgroundTint="@color/background_primary" app:layout_constraintBottom_toBottomOf="@id/iv_cross" app:layout_constraintEnd_toEndOf="@id/iv_cross" app:layout_constraintStart_toStartOf="@id/iv_cross" diff --git a/app/src/main/res/layout/dialog_wallet_trade.xml b/app/src/main/res/layout/dialog_wallet_trade.xml index dea867e05a..96dd6d34c1 100644 --- a/app/src/main/res/layout/dialog_wallet_trade.xml +++ b/app/src/main/res/layout/dialog_wallet_trade.xml @@ -11,7 +11,7 @@ android:layout_height="56dp" android:padding="16dp" android:text="@string/wallet_choose_trade_action" - android:textColor="@color/darkGray2" + android:textColor="@color/text_secondary" android:textSize="14sp" /> @@ -39,7 +39,7 @@ android:gravity="center_vertical" android:padding="16dp" android:text="@string/common_sell" - android:textColor="@color/darkGray3" + android:textColor="@color/text_primary_1" android:textSize="14sp" android:textStyle="bold" app:drawableStartCompat="@drawable/ic_arrow_down_24" /> @@ -54,7 +54,7 @@ android:gravity="center_vertical" android:padding="16dp" android:text="@string/swapping_swap_action" - android:textColor="@color/darkGray3" + android:textColor="@color/text_primary_1" android:textSize="14sp" android:textStyle="bold" app:drawableStartCompat="@drawable/ic_exchange_vertical_24" /> diff --git a/app/src/main/res/layout/fragment_disclaimer.xml b/app/src/main/res/layout/fragment_disclaimer.xml index 328a435e5c..1c237b7938 100644 --- a/app/src/main/res/layout/fragment_disclaimer.xml +++ b/app/src/main/res/layout/fragment_disclaimer.xml @@ -4,7 +4,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:orientation="vertical"> @@ -21,6 +21,8 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" + app:navigationIconTint="@color/icon_primary_1" + app:titleTextColor="@color/text_primary_1" app:title="@string/disclaimer_title" /> @@ -29,7 +31,7 @@ android:id="@+id/cl_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white" + android:background="@color/background_secondary" app:layout_behavior="@string/appbar_scrolling_view_behavior"> diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml index 821f0d3ef8..217d2db70e 100644 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_primary" android:clipChildren="false" android:clipToPadding="false" android:orientation="vertical"> @@ -21,8 +21,10 @@ @@ -59,7 +61,7 @@ android:layout_height="236dp" android:adjustViewBounds="true" android:background="@drawable/shape_circle" - android:backgroundTint="@color/lightGray0" + android:backgroundTint="@color/background_primary" android:elevation="0dp" android:scaleType="fitCenter" app:layout_constraintBottom_toBottomOf="@id/fl_cards_container" @@ -113,17 +115,35 @@ - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + android:layout_marginStart="@dimen/dimen16" + android:layout_marginEnd="@dimen/dimen16" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + android:layout_marginStart="@dimen/dimen16" + android:layout_marginEnd="@dimen/dimen16" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml index 20949f4932..a7f8c54c42 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_wallet" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:orientation="vertical"> @@ -22,7 +22,9 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:navigationIcon="@drawable/ic_baseline_arrow_back_24" - app:title="@string/common_send" /> + app:navigationIconTint="@color/text_primary_1" + app:title="@string/common_send" + app:titleTextColor="@color/text_primary_1" /> @@ -125,7 +127,7 @@ android:layout_gravity="center" android:elevation="18dp" android:indeterminate="true" - android:indeterminateTint="@color/backgroundLightGray" /> + android:indeterminateTint="@color/background_primary" /> diff --git a/app/src/main/res/layout/fragment_shop.xml b/app/src/main/res/layout/fragment_shop.xml index 3d6e2f4020..48384d34c6 100644 --- a/app/src/main/res/layout/fragment_shop.xml +++ b/app/src/main/res/layout/fragment_shop.xml @@ -5,7 +5,7 @@ android:id="@+id/coordinator_details_confirm" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:clipChildren="false" android:clipToPadding="false" android:focusableInTouchMode="true" @@ -16,7 +16,7 @@ style="@style/ThemeOverlay.MyTheme.Toolbar.AccentColorMenu" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/backgroundLightGray" + android:background="@color/background_secondary" android:fitsSystemWindows="true" app:liftOnScroll="true"> diff --git a/app/src/main/res/layout/item_backup_info_adapter.xml b/app/src/main/res/layout/item_backup_info_adapter.xml index ecbfdbdb1b..469434318b 100644 --- a/app/src/main/res/layout/item_backup_info_adapter.xml +++ b/app/src/main/res/layout/item_backup_info_adapter.xml @@ -2,7 +2,8 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + android:layout_height="match_parent" + android:background="@color/background_primary"> + app:layout_constraintTop_toTopOf="parent" + app:tint="@color/icon_informative" /> @@ -65,6 +67,7 @@ android:layout_marginStart="46dp" android:layout_marginTop="8dp" android:src="@drawable/ic_feature_2" + app:tint="@color/icon_primary_1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_feature_2_title" /> @@ -98,6 +101,7 @@ android:layout_marginStart="46dp" android:layout_marginTop="8dp" android:src="@drawable/ic_feature_3" + app:tint="@color/icon_primary_1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_feature_3_title" /> @@ -117,10 +121,10 @@ @@ -28,12 +29,17 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="40dp" android:hint="@string/onboarding_wallet_info_title_third" + android:textColorHint="@color/text_primary_1" android:theme="@style/EditTextThemeOverlay" app:boxStrokeColor="@color/selector_edit_text" app:boxStrokeWidth="1dp" app:endIconDrawable="@drawable/selector_password_toggle" + app:boxStrokeErrorColor="@color/icon_warning" app:endIconMode="password_toggle" app:hintTextColor="@color/accent" + app:endIconTint="@color/icon_primary_1" + app:errorIconTint="@color/icon_warning" + app:errorTextColor="@color/icon_warning" app:layout_constraintTop_toBottomOf="@id/tv_access_code_enter_description"> @@ -48,10 +55,10 @@ @@ -75,7 +78,9 @@ @@ -93,6 +98,9 @@ - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pseudo_toolbar.xml b/app/src/main/res/layout/layout_pseudo_toolbar.xml index c1bc683d3a..0f9b853f36 100644 --- a/app/src/main/res/layout/layout_pseudo_toolbar.xml +++ b/app/src/main/res/layout/layout_pseudo_toolbar.xml @@ -4,7 +4,7 @@ android:id="@+id/pseudo_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - android:background="@color/backgroundLightGray" + android:background="@color/background_action" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -18,6 +18,7 @@ android:clickable="true" android:focusable="true" android:padding="16dp" + android:tint="@color/icon_primary_1" android:src="@drawable/ic_close_24" /> diff --git a/app/src/main/res/layout/layout_receipt_total.xml b/app/src/main/res/layout/layout_receipt_total.xml index 7cca4f9165..5ff5d3377c 100644 --- a/app/src/main/res/layout/layout_receipt_total.xml +++ b/app/src/main/res/layout/layout_receipt_total.xml @@ -47,7 +47,7 @@ android:layout_gravity="end" android:layout_marginTop="4dp" android:gravity="end" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" tools:text="123.29837729 ADA 39487593.109342039402938049 will be sent" /> @@ -64,7 +64,7 @@ android:layout_height="wrap_content" android:layout_gravity="start" android:text="@string/send_total_label" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="14sp" android:textStyle="bold" /> @@ -74,7 +74,7 @@ android:layout_height="wrap_content" android:layout_gravity="end" android:textAllCaps="true" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="14sp" android:textStyle="bold" tools:text="usd" /> diff --git a/app/src/main/res/layout/layout_send_address.xml b/app/src/main/res/layout/layout_send_address.xml index c6dd2873c8..8367bf3524 100644 --- a/app/src/main/res/layout/layout_send_address.xml +++ b/app/src/main/res/layout/layout_send_address.xml @@ -21,23 +21,24 @@ android:id="@+id/tilAddress" android:layout_width="0dp" android:layout_height="wrap_content" - app:boxBackgroundColor="@color/backgroundLightGray" app:errorIconDrawable="@null" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" + style="@style/SecondaryTextInputLayout" app:layout_constraintTop_toTopOf="parent"> @@ -67,10 +68,10 @@ android:id="@+id/flQrCode" android:layout_width="@dimen/btn_rounded_size" android:layout_height="@dimen/btn_rounded_size" - android:layout_marginTop="10dp" android:background="@drawable/shape_ellipse" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/tilAddress"> + android:layout_marginTop="4dp" + app:layout_constraintTop_toTopOf="parent"> @@ -74,7 +75,7 @@ android:drawablePadding="10dp" android:fontFamily="sans-serif-light" android:textAllCaps="true" - android:textColor="@color/blue" + android:textColor="@color/accent" android:textSize="32sp" app:drawableEndCompat="@drawable/ic_arrows_up_down" app:layout_constraintEnd_toEndOf="parent" @@ -88,7 +89,7 @@ android:layout_gravity="end" android:layout_marginTop="8dp" android:layout_marginEnd="16dp" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textSize="16sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@+id/flAmountToSend" /> diff --git a/app/src/main/res/layout/layout_send_fee.xml b/app/src/main/res/layout/layout_send_fee.xml index f54f8f8bfe..bba30587f9 100644 --- a/app/src/main/res/layout/layout_send_fee.xml +++ b/app/src/main/res/layout/layout_send_fee.xml @@ -85,6 +85,7 @@ android:layout_marginStart="16dp" android:layout_marginEnd="8dp" android:text="@string/send_fee_include_description" + android:textColor="@color/text_primary_1" android:textSize="13sp" /> diff --git a/app/src/main/res/layout/layout_send_receipt.xml b/app/src/main/res/layout/layout_send_receipt.xml index 2cf18b1ed5..4a8fab6816 100644 --- a/app/src/main/res/layout/layout_send_receipt.xml +++ b/app/src/main/res/layout/layout_send_receipt.xml @@ -34,7 +34,7 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/send_fee_label" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/tvReceiptAmount" /> @@ -59,7 +59,7 @@ android:layout_height="wrap_content" android:layout_gravity="end" android:textAllCaps="true" - android:textColor="@color/darkGray1" + android:textColor="@color/text_tertiary" android:textStyle="bold" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/tvReceiptFee" diff --git a/app/src/main/res/layout/lp_onboarding_create_wallet.xml b/app/src/main/res/layout/lp_onboarding_create_wallet.xml index 68e87f3e61..85bf7d1c41 100644 --- a/app/src/main/res/layout/lp_onboarding_create_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_create_wallet.xml @@ -104,15 +104,39 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_done.xml b/app/src/main/res/layout/lp_onboarding_done.xml index 8f7be0c671..a5db6527a8 100644 --- a/app/src/main/res/layout/lp_onboarding_done.xml +++ b/app/src/main/res/layout/lp_onboarding_done.xml @@ -105,14 +105,33 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/pb_state"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_done_activation.xml b/app/src/main/res/layout/lp_onboarding_done_activation.xml index 02512421c5..b1517de7c9 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation.xml @@ -104,14 +104,39 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml b/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml index c465795c75..31a41e9321 100644 --- a/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml +++ b/app/src/main/res/layout/lp_onboarding_done_activation_twins.xml @@ -103,14 +103,39 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml index e4e5587025..6d4f2d8ee5 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet.xml @@ -30,7 +30,7 @@ android:layout_marginTop="@dimen/onboarding_square_background_margin_top" android:layout_marginEnd="32dp" android:background="@drawable/shape_rectangle_rounded_8" - android:backgroundTint="@color/lightGray0" + android:backgroundTint="@color/onboarding_card_background" android:elevation="0dp" android:scaleType="fitCenter" app:layout_constraintEnd_toEndOf="parent" @@ -102,15 +102,40 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml b/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml index 66351f4062..2a0c9ed3ba 100644 --- a/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml +++ b/app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml @@ -30,7 +30,7 @@ android:layout_marginTop="@dimen/onboarding_square_background_margin_top" android:layout_marginEnd="32dp" android:background="@drawable/shape_rectangle_rounded_8" - android:backgroundTint="@color/lightGray0" + android:backgroundTint="@color/onboarding_card_background" android:elevation="0dp" android:scaleType="fitCenter" app:layout_constraintEnd_toEndOf="parent" @@ -102,15 +102,40 @@ app:layout_constraintStart_toStartOf="@+id/imv_card_background" app:layout_constraintTop_toBottomOf="@+id/imv_card_background" /> - + app:layout_constraintTop_toBottomOf="@+id/imv_card_background"> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/test_leapfrog_fragment.xml b/app/src/main/res/layout/test_leapfrog_fragment.xml index 52b720ad27..9e0865e85a 100644 --- a/app/src/main/res/layout/test_leapfrog_fragment.xml +++ b/app/src/main/res/layout/test_leapfrog_fragment.xml @@ -4,7 +4,7 @@ android:id="@+id/coordinator_onboarding" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/backgroundWhite" + android:background="@color/background_primary" android:clipChildren="false" android:fitsSystemWindows="true" android:orientation="vertical"> diff --git a/app/src/main/res/layout/view_bg_twins_welcome.xml b/app/src/main/res/layout/view_bg_twins_welcome.xml index acfee09420..f9a713eab6 100644 --- a/app/src/main/res/layout/view_bg_twins_welcome.xml +++ b/app/src/main/res/layout/view_bg_twins_welcome.xml @@ -11,7 +11,7 @@ android:alpha="0.4" android:scaleType="fitXY" android:src="@drawable/shape_circle" - android:tint="#DEDEE0" + android:tint="@color/onboarding_twin_wave_1" android:transitionName="bg_circle_large" android:translationX="-272dp" android:translationY="-200dp" @@ -25,7 +25,7 @@ android:alpha="0.4" android:scaleType="fitXY" android:src="@drawable/shape_circle" - android:tint="#DCDCDC" + android:tint="@color/onboarding_twin_wave_2" android:transitionName="bg_circle_medium" android:translationX="-254dp" android:translationY="-269dp" @@ -39,7 +39,7 @@ android:alpha="0.4" android:scaleType="fitXY" android:src="@drawable/shape_circle" - android:tint="#D9D9D9" + android:tint="@color/onboarding_twin_wave_3" android:transitionName="bg_circle_min" android:translationX="-394dp" android:translationY="-411dp" diff --git a/app/src/main/res/layout/view_compose_fragment.xml b/app/src/main/res/layout/view_compose_fragment.xml deleted file mode 100644 index af9273a84d..0000000000 --- a/app/src/main/res/layout/view_compose_fragment.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/view_currency_icon.xml b/app/src/main/res/layout/view_currency_icon.xml index aa1330c398..7e76b227e7 100644 --- a/app/src/main/res/layout/view_currency_icon.xml +++ b/app/src/main/res/layout/view_currency_icon.xml @@ -45,7 +45,7 @@ android:layout_width="18dp" android:layout_height="18dp" android:background="@drawable/shape_circle" - android:backgroundTint="@color/backgroundWhite" + android:backgroundTint="@color/background_primary" android:contentDescription="@null" android:padding="2dp" android:visibility="gone" diff --git a/app/src/main/res/layout/view_onboarding_progress.xml b/app/src/main/res/layout/view_onboarding_progress.xml index 1d840e7958..696a282eda 100644 --- a/app/src/main/res/layout/view_onboarding_progress.xml +++ b/app/src/main/res/layout/view_onboarding_progress.xml @@ -7,6 +7,6 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:progressBackgroundTint="#32000000" - android:progressTint="@color/background_action" /> + android:progressBackgroundTint="@color/icon_informative" + android:progressTint="@color/icon_primary_1" /> diff --git a/app/src/main/res/layout/view_onboarding_refresh_balance.xml b/app/src/main/res/layout/view_onboarding_refresh_balance.xml index 88daba2ce2..1bf18a57ac 100644 --- a/app/src/main/res/layout/view_onboarding_refresh_balance.xml +++ b/app/src/main/res/layout/view_onboarding_refresh_balance.xml @@ -11,6 +11,7 @@ android:id="@+id/imv_bg_circle" android:layout_width="60dp" android:layout_height="60dp" + android:layout_gravity="center" android:src="@drawable/shape_refresh_button" /> @@ -39,23 +41,9 @@ android:layout_height="24dp" android:layout_gravity="center" android:indeterminate="true" - app:indicatorColor="@android:color/black" + app:indicatorColor="@color/icon_primary_1" app:indicatorSize="23dp" app:trackThickness="1.8dp" /> - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_onboarding_tv_balance.xml b/app/src/main/res/layout/view_onboarding_tv_balance.xml index 84aceb28fa..f421e4d992 100644 --- a/app/src/main/res/layout/view_onboarding_tv_balance.xml +++ b/app/src/main/res/layout/view_onboarding_tv_balance.xml @@ -14,7 +14,7 @@ android:letterSpacing="0.036" android:text="@string/onboarding_balance_title" android:textAllCaps="true" - android:textColor="#ABABAB" + android:textColor="@color/text_secondary" android:textSize="14sp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -31,6 +31,7 @@ android:maxLines="1" android:textSize="28sp" android:textStyle="bold" + android:textColor="@color/text_primary_1" app:layout_constraintEnd_toStartOf="@+id/tv_balance_currency" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_chainStyle="packed" @@ -45,6 +46,7 @@ android:letterSpacing="0.036" android:textSize="28sp" android:textStyle="bold" + android:textColor="@color/text_primary_1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintStart_toEndOf="@+id/tv_balance_value" diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 9be291c700..5e6d3cdd75 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -2,32 +2,37 @@ - + #303030 - - + #1E1E1E + #000000 - + #1E1E1E - - + #F5F5F5 + #303030 - + #1ACE80 - - - - - - - - + #3B3B3B + #3E3E3E + #404040 + #444444 + + #1ACE80 + #FFB71B + #3B3B3B + #656565 + #FFFFFF + #1E1E1E + #919191 + #FF5B5B @@ -36,13 +41,13 @@ - + #FFB71B - - - - - + #494949 + #FFFFFF + #1E1E1E + #B0B0B0 + #919191 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3509ec1ee5..cf46dfba11 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,7 +3,7 @@ #0029FF #0022D4 - #19C878 + #0099FF @color/accent @@ -11,7 +11,7 @@ #FFB71B - @color/backgroundLightGray + @color/background_secondary #FFFFFF #F3F3F3 @@ -19,12 +19,10 @@ #D1D1D6 #8E8E90 #666668 - #48484A + #3A3A3C #1C1C1E - #FFFFFF - #F9F9F9 #F4F5F6 #DE000000 @@ -38,13 +36,19 @@ @color/colorSecondary #E0E6FA + #F3F3F3 + #DDDEE0 + #DCDCDC + #D9D9D9 + + #1C1C1E #FFB71B #CA0F03 #007AFF - #000000 + #FFFFFF #FFFFFF #F5F5F5 @@ -61,9 +65,10 @@ #FFB71B #C9C9C9 #B0B0B0 + #1E1E1E #FFFFFF #656565 - #DE1010 + #FF3333 #FFB71B @@ -74,6 +79,5 @@ #919191 #000000 - - + #00000000 \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9b5f1ccfa6..34b6f8789f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,17 +1,20 @@ - - - + + + @@ -39,6 +48,16 @@ @color/accent + + diff --git a/common/src/main/java/com/tangem/common/Filter.kt b/common/src/main/java/com/tangem/common/Filter.kt deleted file mode 100644 index 6607f4080f..0000000000 --- a/common/src/main/java/com/tangem/common/Filter.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.common - -/** -[REDACTED_AUTHOR] - */ -interface Filter { - fun filter(value: T): Boolean -} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/Strings.kt b/common/src/main/java/com/tangem/common/Strings.kt new file mode 100644 index 0000000000..402f3daa07 --- /dev/null +++ b/common/src/main/java/com/tangem/common/Strings.kt @@ -0,0 +1,6 @@ +package com.tangem.common + +object Strings { + + const val STARS = "***" +} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/Validator.kt b/common/src/main/java/com/tangem/common/Validator.kt deleted file mode 100644 index 8654e9d07e..0000000000 --- a/common/src/main/java/com/tangem/common/Validator.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.common - -/** -[REDACTED_AUTHOR] - */ -interface Validator { - fun validate(data: Data? = null): Error? -} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/module/ModuleMessage.kt b/common/src/main/java/com/tangem/common/module/ModuleMessage.kt index 511ffa8d99..1f8414a0ca 100644 --- a/common/src/main/java/com/tangem/common/module/ModuleMessage.kt +++ b/common/src/main/java/com/tangem/common/module/ModuleMessage.kt @@ -17,11 +17,6 @@ abstract class ModuleError : Throwable(), ModuleMessage { abstract val data: Any? } -/** - * An exception marked as FbConsumeException should be submitted to Firebase.Crashlytics as a non-fatal issue. - */ -interface FbConsumeException - interface ModuleMessageConverter { fun convert(message: ModuleMessage): R } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt new file mode 100644 index 0000000000..feb88dfba6 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -0,0 +1,138 @@ +package com.tangem.core.analytics.models + +sealed class AnalyticsParam { + + sealed class CardBalanceState(val value: String) { + object Empty : CardBalanceState("Empty") + object Full : CardBalanceState("Full") + object CustomToken : CardBalanceState("Custom token") + object BlockchainError : CardBalanceState("Blockchain error") + companion object + } + + sealed class RateApp(val value: String) { + object Liked : RateApp("Liked") + object Disliked : RateApp("Disliked") + object Closed : RateApp("Close") + } + + sealed class OnOffState(val value: String) { + object On : OnOffState("On") + object Off : OnOffState("Off") + } + + sealed class OrganizeSortType(val value: String) { + object ByBalance : OrganizeSortType("By Balance") + object Manually : OrganizeSortType("Manually") + } + + sealed class UserCode(val value: String) { + object AccessCode : UserCode("Access Code") + object Passcode : UserCode("Passcode") + } + + sealed class AccessCodeRecoveryStatus(val value: String) { + + val key: String = "Status" + + object Enabled : AccessCodeRecoveryStatus("Enabled") + object Disabled : AccessCodeRecoveryStatus("Disabled") + + companion object { + fun from(enabled: Boolean): AccessCodeRecoveryStatus { + return if (enabled) Enabled else Disabled + } + } + } + + sealed class Error(val value: String) { + object App : Error("App Error") + object CardSdk : Error("Card Sdk Error") + object BlockchainSdk : Error("Blockchain Sdk Error") + } + + sealed class ScannedFrom(val value: String) { + object Introduction : ScannedFrom("Introduction") + object Main : ScannedFrom("Main") + object SignIn : ScannedFrom("Sign In") + object MyWallets : ScannedFrom("My Wallets") + } + + sealed class TxSentFrom(val value: String) { + data class Send( + override val blockchain: String, + override val token: String, + override val feeType: FeeType, + ) : TxSentFrom("Send"), TxData + + data class Swap( + override val blockchain: String, + override val token: String, + override val feeType: FeeType, + ) : TxSentFrom("Swap"), TxData + + data class Approve( + override val blockchain: String, + override val token: String, + override val feeType: FeeType, + val permissionType: String, + ) : TxSentFrom("Approve"), TxData + + object WalletConnect : TxSentFrom("WalletConnect") + object Sell : TxSentFrom("Sell") + } + + sealed interface TxData { + val blockchain: String + val token: String + val feeType: FeeType + } + + sealed class FeeType(val value: String) { + object Fixed : FeeType("Fixed") + object Min : FeeType("Min") + object Normal : FeeType("Normal") + object Max : FeeType("Max") + + companion object { + fun fromString(feeType: String): FeeType { + return when (feeType) { + Min.value -> Min + Normal.value -> Normal + Max.value -> Max + Fixed.value -> Fixed + else -> Fixed + } + } + } + } + + sealed class WalletCreationType(val value: String) { + object PrivateKey : WalletCreationType("Private key") + object NewSeed : WalletCreationType("New seed") + object SeedImport : WalletCreationType("Seed import") + } + + companion object Key { + const val BLOCKCHAIN = "blockchain" + const val TOKEN = "Token" + const val SOURCE = "Source" + const val BALANCE = "Balance" + const val BATCH = "Batch" + const val FEE_TYPE = "Fee Type" + const val PERMISSION_TYPE = "Permission Type" + const val PRODUCT_TYPE = "Product Type" + const val FIRMWARE = "Firmware" + const val CURRENCY = "Currency" + const val ERROR_DESCRIPTION = "Error Description" + const val ERROR_CODE = "Error Code" + const val ERROR_KEY = "Error Key" + const val CREATION_TYPE = "Creation type" + const val DAPP_NAME = "DApp Name" + const val DAPP_URL = "DApp Url" + const val METHOD_NAME = "Method Name" + const val VALIDATION = "Validation" + const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host" + const val BLOCKCHAIN_SELECTED_HOST = "selected_host" + } +} \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index f304966f2a..79b6816470 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -11,8 +11,10 @@ dependencies { /** Project */ implementation(projects.core.utils) implementation(projects.libs.auth) + implementation(projects.domain.appTheme.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.balanceHiding.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt new file mode 100644 index 0000000000..b11c76fccd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt @@ -0,0 +1,29 @@ +package com.tangem.datasource.api.common + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.ToJson +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat + +class LocalDateAdapter : JsonAdapter() { + + private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd") + + @FromJson + override fun fromJson(reader: JsonReader): LocalDate? { + val dateString = reader.nextString() + return LocalDate.parse(dateString, formatter) + } + + @ToJson + override fun toJson(writer: JsonWriter, value: LocalDate?) { + if (value != null) { + writer.value(formatter.print(value)) + } else { + writer.nullValue() + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt index 712c98c40a..2abb6e2a57 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt @@ -1,14 +1,12 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import org.joda.time.LocalDate -/** - * Main response class for referral API - * contains all necessary info about users program status - */ data class ReferralResponse( @Json(name = "conditions") val conditions: Conditions, @Json(name = "referral") val referral: Referral?, + @Json(name = "expectedAwards") val expectedAwards: ExpectedAwards?, ) { data class Conditions( @@ -45,4 +43,16 @@ data class ReferralResponse( @Json(name = "walletsPurchased") val walletsPurchased: Int, @Json(name = "termsAcceptedAt") val termsAcceptedAt: String?, ) + + data class ExpectedAwards( + @Json(name = "numberOfWallets") val numberOfWallets: Int, + @Json(name = "list") val list: List, + ) { + + data class AwardItem( + @Json(name = "currency") val currency: String, + @Json(name = "paymentDate") val paymentDate: LocalDate, + @Json(name = "amount") val amount: Int, + ) + } } \ 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 new file mode 100644 index 0000000000..2041fdee99 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppThemeModeDataModule.kt @@ -0,0 +1,29 @@ +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.SharedPreferencesDataStore +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 = SharedPreferencesDataStore( + 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/HiddenBalanceDataModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/HiddenBalanceDataModule.kt new file mode 100644 index 0000000000..3cb897a49f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/HiddenBalanceDataModule.kt @@ -0,0 +1,32 @@ +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.SharedPreferencesDataStore +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 = SharedPreferencesDataStore( + 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/MarketCoinsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt new file mode 100644 index 0000000000..bd2098a670 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MarketCoinsStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.DefaultUserMarketCoinsStore +import com.tangem.datasource.local.token.UserMarketCoinsStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object MarketCoinsStoreModule { + + @Provides + @Singleton + fun provideUserMarketCoinsStore(): UserMarketCoinsStore { + return DefaultUserMarketCoinsStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 65668982d4..bf3d94095f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.BigDecimalAdapter +import com.tangem.datasource.api.common.LocalDateAdapter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,6 +21,7 @@ class MoshiModule { fun provideNetworkMoshi(): Moshi { return Moshi.Builder() .add(BigDecimalAdapter()) + .add(LocalDateAdapter()) .add(KotlinJsonAdapterFactory()) .build() } 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 9c11afc771..e68a69e06e 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 @@ -8,7 +8,6 @@ import com.tangem.datasource.utils.RequestHeader.* import com.tangem.datasource.utils.addHeaders import com.tangem.datasource.utils.allowLogging import com.tangem.lib.auth.AuthProvider -import com.tangem.lib.auth.BuildConfig import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -76,7 +75,7 @@ class NetworkModule { private fun createBasePromotionRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): PromotionApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL) + .baseUrl(PROD_TANGEM_TECH_BASE_URL) .client(okHttpClient) .build() .create(PromotionApi::class.java) 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 new file mode 100644 index 0000000000..a29714251b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/BalanceHidingSettingsStore.kt @@ -0,0 +1,13 @@ +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/implementation/BalanceStateHidingSettingsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt new file mode 100644 index 0000000000..740005d56b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appcurrency/implementation/BalanceStateHidingSettingsStore.kt @@ -0,0 +1,18 @@ +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, + ) + } +} \ 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 index b6d8f61ade..c79bf3045b 100644 --- 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 @@ -7,8 +7,4 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStore internal class DefaultSelectedAppCurrencyStore( dataStore: StringKeyDataStore, -) : SelectedAppCurrencyStore, KeylessDataStoreDecorator(dataStore) { - override suspend fun isEmpty(): Boolean { - return getSyncOrNull() == null - } -} \ No newline at end of file +) : 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 new file mode 100644 index 0000000000..78e1fd514c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/AppThemeModeStore.kt @@ -0,0 +1,13 @@ +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 new file mode 100644 index 0000000000..483bf4b525 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/apptheme/DefaultAppThemeModeStore.kt @@ -0,0 +1,9 @@ +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/datastore/core/KeylessDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt index 5e74e2f386..6010c456b5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/KeylessDataStoreDecorator.kt @@ -22,6 +22,10 @@ internal abstract class KeylessDataStoreDecorator( store(Unit, item) } + open suspend fun isEmpty(): Boolean { + return getSyncOrNull() == null + } + private companion object { const val STRING_KEY = "key" } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt new file mode 100644 index 0000000000..c08a627df9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserMarketCoinsStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.wallets.models.UserWalletId + +internal class DefaultUserMarketCoinsStore( + private val dataStore: StringKeyDataStore, +) : UserMarketCoinsStore { + + override suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? { + return dataStore.getSyncOrNull(userWalletId.stringValue) + } + + override suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) { + dataStore.store(userWalletId.stringValue, item) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt new file mode 100644 index 0000000000..10895699ac --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserMarketCoinsStore.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.domain.wallets.models.UserWalletId + +interface UserMarketCoinsStore { + + suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? + + suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index b7f8daf04c..25f17ea484 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -3,10 +3,6 @@ "name": "OPTIMISM_SWAP_FEATURE_ENABLED", "version": "4.3.1" }, - { - "name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED", - "version": "4.7.0" - }, { "name": "NEW_CARD_SCANNING_ENABLED", "version": "undefined" @@ -26,5 +22,13 @@ { "name": "SHOPIFY_DYNAMIC_ENABLED", "version": "undefined" + }, + { + "name": "REDESIGNED_APP_CURRENCY_SELECTOR_ENABLED", + "version": "undefined" + }, + { + "name": "DARK_THEME_ENABLED", + "version": "undefined" } ] diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt index db33dd330d..9aea608408 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt @@ -35,4 +35,5 @@ enum class AppScreen(val isDialogFragment: Boolean = false) { Welcome, SaveWallet(isDialogFragment = true), WalletSelector(isDialogFragment = true), + AppCurrencySelector, } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt b/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt index ff8e8ced9e..20e37f3958 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt @@ -10,5 +10,7 @@ interface ReduxNavController { /** Navigate by [action] */ fun navigate(action: NavigationAction) + fun popBackStack(screen: AppScreen? = null) + fun getBackStack(): List } \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 1ed0453bfb..510b31b595 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -28,11 +28,16 @@ Подключите функцию хранения кодов доступа от карт на телефоне в зашифрованном виде, и при работе с картой вместо кода доступа будет запрашиваться биометрическая аутентификация. Cохранение кошелька Подключите функцию привязки карты в приложении, а также возможность биометрической аутентификации. Подпись транзакции все так же потребует карту. + Тёмная + Светлая + Как в системе + Тема Настройки приложения Пожалуйста, отсканируйте карту Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту Слишком много попыток Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона. + Начать резервное копирование %d карта %d карты @@ -43,6 +48,7 @@ Использовать эту карту для сброса кода доступа на других картах в этом кошельке Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька Восстановление кода доступа + Сбросить Вы уверены, что хотите это сделать? Смена кода доступа Код доступа будет изменен только на данной карте @@ -65,6 +71,7 @@ биометрическую аутентификацию биометрией Купить + Купить %1$s Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. Отмена Закрыть @@ -96,7 +103,6 @@ Отклонить Перезагрузить Переименовать - Сбросить Сохранить изменения Искать Поиск токенов @@ -187,6 +193,7 @@ Приложите карту Внутренняя ошибка: не удается найти менеджер кошельков Вы обновили данные биометрии, отсканируйте свою карту для входа + Чтобы начать отслеживать свои криптоактивы и транзакции, добавьте токены Вы успешно прошли все уроки и теперь можете получить 1INCH токены Пройдите 3 урока и получите %d 1INCH токен на свой кошелек @@ -196,7 +203,7 @@ Управление токенами Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру - Бэкап кошелька не был произведен + Резервное копирование не выполнено Баланс В сумме учтены не все монеты 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов @@ -210,6 +217,9 @@ Вам надо сгенерировать адреса для %d новых сетей, используя вашу карту Некоторые адреса отсутствуют + Добавить + Изменить + Невозможно покрыть %1$s комиссию Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -407,7 +417,7 @@ Расплачивайтесь Отправляйте Храните - Встречайте\nTangem + Встречайте Tangem Обменивайте, покупайте NFT, получайте займы и делайте вклады в более чем 100 различных децентрализованных сервисах Поддержка DeFi Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. @@ -487,7 +497,8 @@ Переименование кошелька Одновалютные Мои кошельки - Разблокировать все с %s + Разблокировать все + Разблокировать все с %s История транзакций Сеть недоступна Блокчейн недоступен. Попробуйте позже. @@ -573,5 +584,5 @@ Войти с %s Сканировать карту Используйте %s или отсканируйте карту для входа в приложение - С возвращением! + C возвращением! diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 294f3413b7..545a1272b2 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -38,6 +38,7 @@ 允許您使用此卡重置此錢包中其他卡上的訪問密碼 禁用重置此卡或此錢包中其他卡上的訪問密碼的功能 恢復訪問密碼 + 重置 您確定要這麼做嗎? 更改訪問密碼 訪問密碼將僅在此卡上更改 @@ -80,7 +81,6 @@ 主卡片 拒絕 重新命名 - 重置 保存設置 搜索 搜尋代幣 @@ -280,7 +280,6 @@ 我了解執行此操作後,我將無法再訪問當前錢包 恢復原廠設置將從所選卡中完全刪除錢包。您將無法恢復當前錢包或使用卡恢復訪問密碼 恢復原廠設置將從所選卡中完全刪除錢包並將其從應用程序中刪除。您將無法恢復當前錢包 - 您有其他國家的銀行卡或銀聯卡嗎? 目前不接受俄羅斯銀行卡 登錄應用程序並在不掃描卡片的情況下檢查您的資產 訪問應用程序 @@ -405,7 +404,7 @@ 重新命名錢包 單一幣種 我的錢包 - 用 %s 解鎖全部 + 用 %s 解鎖全部 交易記錄 網路無法使用 區塊鍊無法使用。稍後再試 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 186b92d9ea..660803875f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -28,11 +28,16 @@ Biometric authentication will be requested instead of the access code for interactions with your card. Keep the wallet in the app Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card. + Dark + Light + System default + Theme App Settings Please scan the card Please try again in 30 seconds or scan the card Too many attempts You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. + Start backup process %d card %d cards @@ -41,6 +46,7 @@ Allows you to use this card to reset access code on other cards in this wallet Disable the ability to reset the access code on this card or other cards in this wallet Access code recovery + Reset Are you sure you want to do this? Change Access Code Access code will be changed on this card only @@ -63,6 +69,7 @@ biometric authentication biometrics Buy + Buy %1$s You have not given access to your camera, please adjust your privacy settings Cancel Close @@ -95,7 +102,6 @@ Reject Reload Rename - Reset Save changes Search Search tokens @@ -129,7 +135,10 @@ Required field Decimal number must be a valid integer, no higher than %d Custom derivation + E. g. m/00\'/0000\'/0\'/0/0 + Enter custom derivation Decimals + Derivation Path Default BIP44 coin type The derivation path you\'ve entered is not valid @@ -137,6 +146,8 @@ Name Not selected Network + Token network + You can manually add a token that is not natively supported by Tangem E.g. USDC Token symbol This token/network has already been added to your list @@ -154,6 +165,8 @@ Card ID Link More Cards App Currency + Flip-to-Hide Balances + Flip your device screen down to quickly hide and show balances Issuer Send Feedback Signed @@ -186,7 +199,7 @@ Tap the card Internal error: wallet manager not found You have updated biometrics, scan your card to enter - To begin tracking your crypto assets and transactions, add tokens. + To begin tracking your crypto assets and transactions, add tokens You have completed all of the lessons, and are now eligible to receive your 1INCH tokens Complete three lessons and receive %d 1INCH token to your wallet @@ -194,7 +207,7 @@ Manage tokens To protect your assets, we advise you to carry out this procedure - Your wallet has not been backed up + Your wallet hasn\'t been backed up Total balance The amount does not include some of your funds 1INCH tokens will be credited to your %s wallet address within 48 hours @@ -206,6 +219,30 @@ You need to generate addresses for %d new networks using your card Some addresses are missing + Add + Custom + Edit + 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. + Not original or primary blockchain the token is hosted + Non-native networks + Blockchain the cryptocurrency was initially created + Networks + Choose networks + Wallet + Couldn’t find this token, you can add it manually + %d of %#@total_wallets@ + + %d wallet + %d wallets + + e.g. BTC I trust, hodl I must + 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 + Choose wallet + Unable to cover %1$s fee 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 @@ -233,7 +270,7 @@ Skip for later How does it work? Let\'s generate all the keys on your card and create a secure wallet - Create a wallet + Create wallet Create a wallet Other options Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it. @@ -400,7 +437,7 @@ Pay Send Store - Meet\nTangem + Meet Tangem Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services DeFi Compatible Approvals are considered an industry standard across all decentralized exchanges and protect your wallet from being accessed by a smart contract without your permission. By design, smart contracts can\'t access your tokens unless you approve access from your end. By \"unlocking\" your tokens, you are give permission to the 1inch smart contract to spend your assets. The miners of the network are compensated with a gas fee (paid by you) to record this action on the blockchain. Once permission has been granted you will be able to swap your token. @@ -478,7 +515,8 @@ Rename Wallet Single-currency My Wallets - Unlock all with %s + Unlock all + Unlock all with %s Transaction history Network is unreachable Blockchain is unreachable. Try later @@ -543,6 +581,7 @@ Tangem Can be better Learn more + Love it! Ok, Got it! Really cool! %1$s network has a concept of Existential Deposit. If your account drops below %2$s it will be deactivated and any remaining funds will be destroyed. @@ -553,7 +592,11 @@ How do you like Tangem? One question This card has signed transactions in the past + Network currently is unreachable. Please try again later. + Some networks currently are unreachable. Please try again later. This is a Testnet card. Don\'t accept it as a payment. This card must only be used for testing and development purposes. + Note top up + Some networks are unreachable Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 20d4303b93..bcd3be240f 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -29,4 +29,5 @@ dependencies { implementation(deps.material) implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) + implementation(deps.zxing.qrCore) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 24a2316d60..41e2d008b6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -1,44 +1,33 @@ package com.tangem.core.ui.components +import androidx.compose.foundation.LocalIndication 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.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material.RadioButton +import androidx.compose.material.RadioButtonDefaults import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import com.tangem.core.ui.R +import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams import com.tangem.core.ui.res.TangemTheme - -/** - * Dialog button params - * - * @param title Button text. If not provided default values will be used - * @param warning If true then button text will be in theme warning color - * @param enabled If false button will be disabled - * @param onClick Button click callback - */ -data class DialogButton( - val title: String? = null, - val warning: Boolean = false, - val enabled: Boolean = true, - val onClick: () -> Unit, -) - -/** - * Additional params for dialog text field - */ -data class AdditionalTextInputDialogParams( - val label: String? = null, - val placeholder: String? = null, - val caption: String? = null, - val enabled: Boolean = true, - val isError: Boolean = false, -) +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList /** * Simple alert dialog with a message and 'OK' button @@ -129,6 +118,54 @@ fun TextInputDialog( ) } +@Composable +fun SelectorDialog( + selectedItemIndex: Int, + items: ImmutableList, + confirmButton: DialogButton, + onSelect: (index: Int) -> Unit, + onDismissDialog: () -> Unit, + title: String? = null, + isDismissable: Boolean = true, +) { + TangemDialog( + type = DialogType.Selector(selectedItemIndex, items, onSelect), + confirmButton = confirmButton, + title = title, + onDismissDialog = onDismissDialog, + properties = DialogProperties( + dismissOnBackPress = isDismissable, + dismissOnClickOutside = isDismissable, + ), + ) +} + +/** + * Dialog button params + * + * @param title Button text. If not provided default values will be used + * @param warning If true then button text will be in theme warning color + * @param enabled If false button will be disabled + * @param onClick Button click callback + */ +data class DialogButton( + val title: String? = null, + val warning: Boolean = false, + val enabled: Boolean = true, + val onClick: () -> Unit, +) + +/** + * Additional params for dialog text field + */ +data class AdditionalTextInputDialogParams( + val label: String? = null, + val placeholder: String? = null, + val caption: String? = null, + val enabled: Boolean = true, + val isError: Boolean = false, +) + // region Defaults @Composable private fun TangemDialog( @@ -146,15 +183,18 @@ private fun TangemDialog( shape = TangemTheme.shapes.roundedCornersLarge, color = TangemTheme.colors.background.plain, ) - .padding(all = TangemTheme.dimens.spacing24), + .padding(vertical = TangemTheme.dimens.spacing24), ) { if (title != null) { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing24) + .fillMaxWidth(), text = title, style = when (type) { is DialogType.Message -> TangemTheme.typography.h2 is DialogType.TextInput -> TangemTheme.typography.h3 + is DialogType.Selector -> TangemTheme.typography.h2 }, color = TangemTheme.colors.text.primary1, ) @@ -162,7 +202,13 @@ private fun TangemDialog( } DialogContent(type = type) SpacerH24() - DialogButtons(confirmButton = confirmButton, dismissButton = dismissButton) + DialogButtons( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing24) + .fillMaxWidth(), + confirmButton = confirmButton, + dismissButton = dismissButton, + ) } } } @@ -177,7 +223,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { when (type) { is DialogType.Message -> { Text( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing24) + .fillMaxWidth(), text = type.message, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, @@ -185,7 +233,9 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { } is DialogType.TextInput -> { OutlineTextField( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing24) + .fillMaxWidth(), value = type.value, label = type.params.label, placeholder = type.params.placeholder, @@ -197,6 +247,14 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { }, ) } + is DialogType.Selector -> { + SelectorDialogContent( + modifier = Modifier.fillMaxWidth(), + selectedItemIndex = type.selectedItemIndex, + items = type.items, + onSelect = type.onSelect, + ) + } } } } @@ -204,7 +262,7 @@ private fun DialogContent(type: DialogType, modifier: Modifier = Modifier) { @Composable private fun DialogButtons(confirmButton: DialogButton, dismissButton: DialogButton?, modifier: Modifier = Modifier) { Row( - modifier = modifier.fillMaxWidth(), + modifier = modifier, horizontalArrangement = Arrangement.spacedBy( space = TangemTheme.dimens.spacing4, alignment = Alignment.End, @@ -252,14 +310,72 @@ private fun DialogButton( } } -private sealed interface DialogType { - data class Message(val message: String) : DialogType +@Composable +private fun SelectorDialogContent( + selectedItemIndex: Int, + items: ImmutableList, + onSelect: (index: Int) -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn(modifier = modifier) { + itemsIndexed(items = items) { index, itemText -> + val onClick = remember(index) { + { onSelect(index) } + } + + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = onClick, + ) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing18, + ) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + verticalAlignment = Alignment.CenterVertically, + ) { + RadioButton( + modifier = Modifier.size(TangemTheme.dimens.size24), + selected = index == selectedItemIndex, + onClick = onClick, + colors = RadioButtonDefaults.colors( + selectedColor = TangemTheme.colors.icon.accent, + unselectedColor = TangemTheme.colors.icon.secondary, + ), + interactionSource = interactionSource, + ) + Text( + text = itemText, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } +} + +@Immutable +private sealed class DialogType { + + data class Message(val message: String) : DialogType() data class TextInput( val value: TextFieldValue, val onValueChange: (TextFieldValue) -> Unit, val params: AdditionalTextInputDialogParams = AdditionalTextInputDialogParams(), - ) : DialogType + ) : DialogType() + + data class Selector( + val selectedItemIndex: Int, + val items: ImmutableList, + val onSelect: (index: Int) -> Unit, + ) : DialogType() } // endregion Defaults @@ -381,4 +497,65 @@ private fun TextInputDialogPreview_Dark() { TextInputDialogSample() } } + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SelectorDialogPreview_Light( + @PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams, +) { + TangemTheme(isDark = false) { + SelectorDialog( + title = param.title, + items = param.items, + selectedItemIndex = param.selectedItemIndex, + confirmButton = DialogButton(title = "Cancel", onClick = {}), + onSelect = {}, + onDismissDialog = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SelectorDialogPreview_Dark( + @PreviewParameter(SelctorDialogParamsProvider::class) param: SelectorDialogParams, +) { + TangemTheme(isDark = true) { + SelectorDialog( + title = param.title, + items = param.items, + selectedItemIndex = param.selectedItemIndex, + confirmButton = DialogButton(title = "Cancel", onClick = {}), + onSelect = {}, + onDismissDialog = {}, + ) + } +} + +private class SelctorDialogParamsProvider : CollectionPreviewParameterProvider( + collection = listOf( + SelectorDialogParams( + title = "Theme", + selectedItemIndex = 0, + persistentListOf("Light", "Dark", "Follow system"), + ), + SelectorDialogParams( + title = null, + selectedItemIndex = 2, + persistentListOf("Light", "Dark", "Follow system"), + ), + SelectorDialogParams( + title = "Count", + selectedItemIndex = 8, + List(size = 10) { it.toString() }.toImmutableList(), + ), + ), +) { + + data class SelectorDialogParams( + val title: String?, + val selectedItemIndex: Int, + val items: ImmutableList, + ) +} // endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/QrCode.kt b/core/ui/src/main/java/com/tangem/core/ui/components/QrCode.kt new file mode 100644 index 0000000000..afda35efe7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/QrCode.kt @@ -0,0 +1,29 @@ +package com.tangem.core.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.extensions.toQrCode +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun rememberQrPainters( + content: List, + size: Dp = TangemTheme.dimens.size248, + padding: Dp = TangemTheme.dimens.spacing0, +): List { + val density = LocalDensity.current + return remember(content) { + content.map { code -> + BitmapPainter( + code.toQrCode( + sizePx = with(density) { size.roundToPx() }, + paddingPx = with(density) { padding.roundToPx() }, + ).asImageBitmap(), + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt index 0c4c0c3610..d4d1abb476 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/ResizableText.kt @@ -1,14 +1,15 @@ package com.tangem.core.ui.components +import androidx.annotation.FloatRange import androidx.compose.material.LocalTextStyle import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp @@ -54,6 +55,63 @@ fun ResizableText( ) } +/** + * A Composable function that displays text which can be resized based on its content's overflow. + * + * This function draws text on the screen and checks if it overflows. If the text overflows, + * its font size is reduced recursively until it either fits the available space or reaches a + * specified minimum font size. + */ +@Composable +fun ResizableText( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + textAlign: TextAlign? = null, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + style: TextStyle = LocalTextStyle.current, + minFontSize: TextUnit = TextUnit.Unspecified, + @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) + reduceFactor: Double = 0.9, +) { + var fontSize by remember { mutableStateOf(style.fontSize) } + var readyToDraw by remember { mutableStateOf(value = false) } + + Text( + modifier = modifier.drawWithContent { + if (readyToDraw) drawContent() + }, + text = text, + color = color, + fontSize = fontSize, + textAlign = textAlign, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + style = style, + onTextLayout = { result -> + fun reduceFontSize() { + val reducedFontSize = fontSize * reduceFactor + + if (minFontSize != TextUnit.Unspecified && reducedFontSize <= minFontSize) { + fontSize = minFontSize + readyToDraw = true + } else { + fontSize = reducedFontSize + } + } + + if (result.hasVisualOverflow) { + reduceFontSize() + } else { + readyToDraw = true + } + }, + ) +} + data class FontSizeRange( val min: TextUnit, val max: TextUnit, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt index 640f986b3e..311fe1aae8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Shimmers.kt @@ -1,15 +1,25 @@ package com.tangem.core.ui.components +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme -import com.valentinilk.shimmer.shimmer +import com.valentinilk.shimmer.* /** * Rectangle shimmer item with rounded shape from DS @@ -18,11 +28,8 @@ import com.valentinilk.shimmer.shimmer fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dimens.radius6) { Box( modifier = modifier - .shimmer() - .background( - color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(size = radius), - ), + .clip(RoundedCornerShape(size = radius)) + .shimmer(TangemShimmer), ) } @@ -34,27 +41,67 @@ fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = TangemTheme.dim fun CircleShimmer(modifier: Modifier = Modifier) { Box( modifier = modifier - .shimmer() - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ), + .clip(CircleShape) + .shimmer(TangemShimmer), ) } +private val TangemShimmer: Shimmer + @Composable + get() = rememberShimmer( + shimmerBounds = ShimmerBounds.View, + theme = defaultShimmerTheme.copy( + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 800, + easing = LinearEasing, + delayMillis = 800, + ), + repeatMode = RepeatMode.Restart, + ), + shaderColors = TangemShimmerColors, + blendMode = BlendMode.Src, + shaderColorStops = null, + ), + ) + +private val TangemShimmerColors: List + @Composable + @ReadOnlyComposable + get() { + val isInDarkTheme = LocalIsInDarkTheme.current + + return buildList { + if (isInDarkTheme) { + TangemColorPalette.Dark3.let(::add) + TangemColorPalette.Dark4.let(::add) + TangemColorPalette.Dark6.let(::add) + TangemColorPalette.Dark4.let(::add) + TangemColorPalette.Dark3.let(::add) + } else { + TangemColorPalette.Light2.let(::add) + TangemColorPalette.Light1.let(::add) + TangemColorPalette.White.let(::add) + TangemColorPalette.Light1.let(::add) + TangemColorPalette.Light2.let(::add) + } + } + } + // region preview @Composable private fun ShimmersPreview() { Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.primary), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), ) { RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size72, - height = TangemTheme.dimens.size12, - ), + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size24), ) CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size42)) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt new file mode 100644 index 0000000000..b8b5086187 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -0,0 +1,31 @@ +package com.tangem.core.ui.components.bottomsheets + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import com.tangem.core.ui.res.TangemTheme + +/** + * Tangem bottom sheet with custom draggable header and config + * + * @param config data model containing logic and ui models + * @param content custom bottom sheet content + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TangemBottomSheet( + config: TangemBottomSheetConfig, + content: @Composable ColumnScope.(TangemBottomSheetConfigContent) -> Unit, +) { + ModalBottomSheet( + onDismissRequest = config.onDismissRequest, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.bottomSheetLarge, + dragHandle = { TangemBottomSheetDraggableHeader() }, + ) { + content(config.content) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt new file mode 100644 index 0000000000..0bb0162ccc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.core.ui.components.bottomsheets + +/** + * Tangem bottom sheet config + * + * @property isShow flag that determine if bottom sheet is shown + * @property onDismissRequest lambda be invoked when bottom sheet is dismissed + * @property content content config + */ +data class TangemBottomSheetConfig( + val isShow: Boolean, + val onDismissRequest: () -> Unit, + val content: TangemBottomSheetConfigContent, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfigContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfigContent.kt new file mode 100644 index 0000000000..812d593533 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetConfigContent.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.components.bottomsheets + +/** + * General interface for bottom sheet config model + */ +interface TangemBottomSheetConfigContent \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt new file mode 100644 index 0000000000..5b1e13f57a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheetDraggableHeader.kt @@ -0,0 +1,33 @@ +package com.tangem.core.ui.components.bottomsheets + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun TangemBottomSheetDraggableHeader() { + Surface( + modifier = Modifier + .height(TangemTheme.dimens.size20), + color = TangemTheme.colors.background.primary, + ) { + Box( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing8) + .size( + width = TangemTheme.dimens.size32, + height = TangemTheme.dimens.size4, + ) + .background( + color = TangemTheme.colors.icon.inactive, + shape = TangemTheme.shapes.roundedCornersSmall, + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt new file mode 100644 index 0000000000..0314b21846 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/AddressModel.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.components.bottomsheets.tokenreceive + +data class AddressModel( + val value: String, + val type: Type = Type.Default, +) { + enum class Type { + Legacy, Default + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt new file mode 100644 index 0000000000..beadf33d24 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -0,0 +1,192 @@ +package com.tangem.core.ui.components.bottomsheets.tokenreceive + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.R +import com.tangem.core.ui.components.MiddleEllipsisText +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.rememberQrPainters +import com.tangem.core.ui.extensions.shareText +import com.tangem.core.ui.res.TangemTheme + +@Composable +fun TokenReceiveBottomSheet(config: TangemBottomSheetConfig) { + if (config.content is TokenReceiveBottomSheetConfig && config.isShow) { + TangemBottomSheet(config) { content -> + TokenReceiveBottomSheetContent( + content = content as TokenReceiveBottomSheetConfig, + ) + } + } +} + +@Composable +private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfig) { + var selectedAddress by remember { mutableStateOf(content.addresses.first()) } + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing24, + top = TangemTheme.dimens.spacing24, + end = TangemTheme.dimens.spacing24, + bottom = TangemTheme.dimens.spacing16, + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing24), + ) { + QrCodeContent( + content = content, + onAddressChange = { selectedAddress = it }, + ) + Text( + text = stringResource(R.string.receive_bottom_sheet_warning_message_full, content.name), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + val clipboardManager = LocalClipboardManager.current + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + SecondaryButtonIconStart( + modifier = Modifier.weight(1f), + text = stringResource(id = R.string.common_copy), + iconResId = R.drawable.ic_copy_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + clipboardManager.setText(AnnotatedString(selectedAddress.value)) + }, + ) + SecondaryButtonIconStart( + modifier = Modifier.weight(1f), + text = stringResource(id = R.string.common_share), + iconResId = R.drawable.ic_share_24, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + context.shareText(selectedAddress.value) + }, + ) + } + } +} + +@Suppress("LongMethod") +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun QrCodeContent(content: TokenReceiveBottomSheetConfig, onAddressChange: (AddressModel) -> Unit) { + val qrCodes = rememberQrPainters(content.addresses.map(AddressModel::value)) + val pagerState = rememberPagerState() + val pageCount = content.addresses.count() + + LaunchedEffect(key1 = pagerState.currentPage) { + onAddressChange.invoke(content.addresses[pagerState.currentPage]) + } + + HorizontalPager( + pageCount = pageCount, + state = pagerState, + ) { currentPage -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + ) { + Text( + text = stringResource( + R.string.receive_bottom_sheet_warning_message, + getName(content = content, index = pagerState.currentPage), + content.symbol, + content.network, + ), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h3, + ) + Image( + painter = qrCodes[currentPage], + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .size(TangemTheme.dimens.size248), + ) + MiddleEllipsisText( + text = content.addresses[currentPage].value, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.subtitle1, + ) + } + } + + if (pageCount > 1) { + val indicatorState = rememberLazyListState() + val selectedColor = TangemTheme.colors.icon.primary1 + val unselectedColor = TangemTheme.colors.icon.informative + LazyRow( + modifier = Modifier + .height(TangemTheme.dimens.size20), + state = indicatorState, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + repeat(pageCount) { iteration -> + item(key = iteration) { + val color = if (pagerState.currentPage == iteration) { + selectedColor + } else { + unselectedColor + } + Box( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing4, + top = TangemTheme.dimens.spacing6, + end = TangemTheme.dimens.spacing4, + bottom = TangemTheme.dimens.spacing6, + ) + .background(color, CircleShape) + .size(TangemTheme.dimens.size7), + ) + } + } + } + } +} + +@Composable +private fun getName(content: TokenReceiveBottomSheetConfig, index: Int): String { + return if (content.addresses.size < 2) { + content.name + } else { + "${ + stringResource( + id = when (content.addresses[index].type) { + AddressModel.Type.Default -> R.string.address_type_default + AddressModel.Type.Legacy -> R.string.address_type_legacy + }, + ) + } ${content.name}" + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt new file mode 100644 index 0000000000..b1b84dc016 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.components.bottomsheets.tokenreceive + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +class TokenReceiveBottomSheetConfig( + val name: String, + val symbol: String, + val network: String, + val addresses: List, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 2167ad385c..02cf8f05ba 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerW8 -import com.tangem.core.ui.components.buttons.common.* import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -99,7 +98,7 @@ private fun Button( val iconTint by animateColorAsState( targetValue = when { !config.enabled -> TangemTheme.colors.icon.informative - config.dimContent -> TangemTheme.colors.icon.secondary + config.dimContent -> TangemTheme.colors.icon.informative else -> TangemTheme.colors.icon.primary1 }, label = "Update tint color", @@ -117,7 +116,7 @@ private fun Button( val textColor by animateColorAsState( targetValue = when { !config.enabled -> TangemTheme.colors.text.disabled - config.dimContent -> TangemTheme.colors.text.secondary + config.dimContent -> TangemTheme.colors.text.tertiary else -> TangemTheme.colors.text.primary1 }, label = "Update text color", 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 6560c617e0..fc114059fe 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 @@ -1,20 +1,19 @@ package com.tangem.core.ui.components.buttons.common -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.requiredSizeIn +import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerW +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.res.TangemTheme @Suppress("LongParameterList") @@ -40,31 +39,35 @@ fun TangemButton( colors = colors, contentPadding = size.toContentPadding(icon = icon), ) { + val maxContentSize = getMaxButtonContentSize(buttonTextStyle = textStyle) + ButtonContentContainer( buttonIcon = icon, iconPadding = size.toIconPadding(), showProgress = showProgress, progressIndicator = { CircularProgressIndicator( - modifier = Modifier.buttonContentSize(textStyle), + modifier = Modifier.buttonContentSize(maxContentSize), color = colors.contentColor(enabled = enabled).value, - strokeWidth = TangemTheme.dimens.size4, ) }, text = { - Text( - modifier = Modifier.alignByBaseline(), + ResizableText( + modifier = Modifier + .weight(1f, fill = false) + .heightIn(MinButtonContentSize, maxContentSize), text = text, style = textStyle, color = colors.contentColor(enabled = enabled).value, textAlign = TextAlign.Center, maxLines = 1, overflow = TextOverflow.Ellipsis, + minFontSize = 12.sp, ) }, icon = { iconResId -> Icon( - modifier = Modifier.buttonContentSize(textStyle), + modifier = Modifier.buttonContentSize(maxContentSize), painter = painterResource(id = iconResId), tint = colors.contentColor(enabled = enabled).value, contentDescription = null, @@ -89,26 +92,36 @@ private inline fun RowScope.ButtonContentContainer( } else { if (buttonIcon is TangemButtonIconPosition.Start) { icon(buttonIcon.iconResId) - SpacerW(width = iconPadding) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) } text() if (buttonIcon is TangemButtonIconPosition.End) { - SpacerW(width = iconPadding) + Spacer(modifier = Modifier.requiredWidth(iconPadding)) icon(buttonIcon.iconResId) } } } -private fun Modifier.buttonContentSize(buttonTextStyle: TextStyle): Modifier = composed { - val minContentElementSize = TangemTheme.dimens.size20 - val maxContentElementSize = remember(key1 = buttonTextStyle.lineHeight) { - buttonTextStyle.lineHeight.value.dp + 4.dp +private val MinButtonContentSize: Dp + @Composable + @ReadOnlyComposable + get() = TangemTheme.dimens.size20 + +@Composable +@ReadOnlyComposable +private fun getMaxButtonContentSize(buttonTextStyle: TextStyle): Dp { + val buttonLineHeight = with(LocalDensity.current) { + buttonTextStyle.lineHeight.toDp() } + return buttonLineHeight.coerceAtLeast(MinButtonContentSize) +} + +private fun Modifier.buttonContentSize(maxSize: Dp): Modifier = composed { this.requiredSizeIn( - minWidth = minContentElementSize, - minHeight = minContentElementSize, - maxWidth = maxContentElementSize, - maxHeight = maxContentElementSize, + minWidth = MinButtonContentSize, + minHeight = MinButtonContentSize, + maxWidth = maxSize, + maxHeight = maxSize, ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index f498b4caf7..61ae23802d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -2,10 +2,10 @@ package com.tangem.core.ui.components.marketprice import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -136,13 +136,17 @@ private fun PriceChangeInPercent(config: PriceChangeConfig) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4), ) { - Image( + Icon( painter = painterResource( id = when (type) { - PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 + PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8 + PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8 }, ), + tint = when (type) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning + }, contentDescription = null, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 76b7894881..6e569c56bb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -1,112 +1,140 @@ package com.tangem.core.ui.components.notifications import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState 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.res.painterResource +import androidx.compose.ui.semantics.Role 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.R -import com.tangem.core.ui.components.SpacerH2 +import com.tangem.core.ui.components.* import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState /** * Notification component from Design system. * Use this for Notification with title, subtitle, clickable or not. * - * @param state component state + * @param config component config * @param modifier modifier + * @param iconTint icon tint * * @see Figma component */ @Composable -fun Notification(state: NotificationState, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius18)) - .background( - color = TangemTheme.colors.button.secondary, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius18), - ) - .clickable( - enabled = when (state) { - is NotificationState.Clickable -> true - is NotificationState.Simple, is NotificationState.Closable -> false - }, - onClick = if (state is NotificationState.Clickable) { - state.onClick - } else { - {} - }, - ), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing8), +fun Notification(config: NotificationConfig, modifier: Modifier = Modifier, iconTint: Color? = null) { + BaseContainer(buttonsState = config.buttonsState, onClick = config.onClick, modifier = modifier) { + Column( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), ) { - NotificationIcon( - iconResId = state.iconResId, - iconTint = state.tint, + MainContent( + iconResId = config.iconResId, + iconTint = iconTint, + title = config.title, + subtitle = config.subtitle, + isClickableComponent = config.onClick != null, + ) + + Buttons(state = config.buttonsState) + } + + CloseableIconButton( + onClick = config.onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + ) + } +} + +@Composable +private fun BaseContainer( + buttonsState: NotificationConfig.ButtonsState?, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + val containerColor by rememberUpdatedState( + newValue = if (buttonsState != null || onClick != null) { + TangemTheme.colors.background.primary + } else { + TangemTheme.colors.button.disabled + }, + ) + + Surface( + onClick = onClick ?: {}, + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size62) + .fillMaxWidth(), + enabled = onClick != null, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = containerColor, + ) { + Box(content = content) + } +} + +@Composable +private fun MainContent( + iconResId: Int, + iconTint: Color?, + title: TextReference, + subtitle: TextReference, + isClickableComponent: Boolean, +) { + Row { + Icon( + iconResId = iconResId, + tint = iconTint, + modifier = Modifier.align(alignment = Alignment.CenterVertically), + ) + + SpacerW(width = TangemTheme.dimens.spacing10) + + TextsBlock(title = title, subtitle = subtitle) + + if (isClickableComponent) { + SpacerWMax() + + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, modifier = Modifier .size(size = TangemTheme.dimens.size20) - .align(alignment = Alignment.CenterStart), + .align(alignment = Alignment.CenterVertically), + tint = TangemTheme.colors.icon.informative, ) - - NotificationInfoBlock( - title = state.title.resolveReference(), - subtitle = state.subtitle?.resolveReference(), - modifier = Modifier.align(alignment = Alignment.CenterStart), - ) - - if (state is NotificationState.Closable) { - Icon( - modifier = Modifier - .size(size = TangemTheme.dimens.size20) - .align(alignment = Alignment.TopEnd), - painter = painterResource(id = R.drawable.ic_close_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - - if (state is NotificationState.Clickable) { - Icon( - modifier = Modifier - .size(size = TangemTheme.dimens.size20) - .align(alignment = Alignment.CenterEnd), - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } } } } @Composable -private fun NotificationIcon(@DrawableRes iconResId: Int, iconTint: Color?, modifier: Modifier = Modifier) { - if (iconTint != null) { +private fun Icon(@DrawableRes iconResId: Int, tint: Color?, modifier: Modifier = Modifier) { + if (tint != null) { Icon( painter = painterResource(id = iconResId), contentDescription = null, modifier = modifier, - tint = iconTint, + tint = tint, ) } else { Image( @@ -118,20 +146,104 @@ private fun NotificationIcon(@DrawableRes iconResId: Int, iconTint: Color?, modi } @Composable -private fun NotificationInfoBlock(title: String, subtitle: String?, modifier: Modifier = Modifier) { - Column(modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing30)) { +private fun TextsBlock(title: TextReference, subtitle: TextReference) { + Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) { Text( - text = title, + text = title.resolveReference(), color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body2, + style = TangemTheme.typography.button, ) - if (!subtitle.isNullOrEmpty()) { - SpacerH2() - Text( - text = subtitle, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption, + Text( + text = subtitle.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption, + ) + } +} + +@Composable +private fun Buttons(state: NotificationButtonsState?) { + when (state) { + is NotificationButtonsState.SecondaryButtonConfig -> SingleSecondaryButton(config = state) + is NotificationButtonsState.PrimaryButtonConfig -> SinglePrimaryButton(config = state) + is NotificationButtonsState.PairButtonsConfig -> PairButtons(config = state) + null -> Unit + } +} + +@Composable +private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButtonConfig) { + SecondaryButton( + text = config.text.resolveReference(), + onClick = config.onClick, + modifier = Modifier.fillMaxWidth(), + ) +} + +@Composable +private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonConfig) { + if (config.iconResId != null) { + PrimaryButtonIconEnd( + text = config.text.resolveReference(), + iconResId = config.iconResId, + onClick = config.onClick, + modifier = Modifier.fillMaxWidth(), + ) + } else { + PrimaryButton( + text = config.text.resolveReference(), + onClick = config.onClick, + modifier = Modifier.fillMaxWidth(), + ) + } +} + +@Composable +private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) { + Row(horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8)) { + SecondaryButton( + text = config.secondaryText.resolveReference(), + onClick = config.onSecondaryClick, + modifier = Modifier.weight(weight = 1f), + ) + + PrimaryButton( + text = config.primaryText.resolveReference(), + onClick = config.onPrimaryClick, + modifier = Modifier.weight(weight = 1f), + ) + } +} + +@Composable +private fun CloseableIconButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { + AnimatedVisibility(visible = onClick != null, modifier = modifier) { + onClick ?: return@AnimatedVisibility + + /* + * Implement a custom ripple because the design layout doesn't match the Material Design. + * Material Icon has a size 24x24 and Material IconButton has a size 48x48, + * but icon from Figma has a size 16x16. + */ + Box( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(shape = CircleShape) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = LocalIndication.current, + role = Role.Button, + onClick = onClick, + ), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + modifier = Modifier + .size(size = TangemTheme.dimens.size16) + .align(alignment = Alignment.Center), + tint = TangemTheme.colors.icon.inactive, ) } } @@ -139,72 +251,83 @@ private fun NotificationInfoBlock(title: String, subtitle: String?, modifier: Mo @Preview @Composable -private fun Preview_WarningNotification_Light( - @PreviewParameter(NotificationStateProvider::class) - state: NotificationState, +private fun Preview_Notification_Light( + @PreviewParameter(NotificationConfigProvider::class) + config: NotificationConfig, ) { TangemTheme(isDark = false) { - Notification(state) + Notification(config) } } @Preview @Composable -private fun Preview_WarningNotification_Dark( - @PreviewParameter(NotificationStateProvider::class) - state: NotificationState, +private fun Preview_Notification_Dark( + @PreviewParameter(NotificationConfigProvider::class) config: NotificationConfig, ) { TangemTheme(isDark = true) { - Notification(state) + Notification(config) } } -private class NotificationStateProvider : CollectionPreviewParameterProvider( +private class NotificationConfigProvider : CollectionPreviewParameterProvider( collection = listOf( - NotificationState.Simple( - title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + NotificationConfig( + title = TextReference.Str(value = "Development card"), subtitle = TextReference.Str( - value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + - "ut labore et...", + value = "The card you scanned is a development card.\nDon’t accept it as a payment.", ), - iconResId = R.drawable.img_attention_20, - ), - NotificationState.Simple( - title = TextReference.Str("Your wallet hasn’t been backed up"), - subtitle = null, iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, ), - NotificationState.Clickable( - title = TextReference.Str(value = "Your wallet hasn’t been backed up"), - subtitle = TextReference.Str( - value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + - "ut labore et...", - ), + NotificationConfig( + title = TextReference.Str(value = "Some networks are unreachable"), + subtitle = TextReference.Str(value = "Check your network connection"), iconResId = R.drawable.img_attention_20, + ), + NotificationConfig( + title = TextReference.Str(value = "Used card"), + subtitle = TextReference.Str(value = "The card signed transactions in the past"), + iconResId = R.drawable.ic_alert_circle_24, onClick = {}, ), - NotificationState.Clickable( + NotificationConfig( title = TextReference.Str(value = "Your wallet hasn’t been backed up"), - subtitle = null, - iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, - onClick = {}, - ), - NotificationState.Closable( - title = TextReference.Str(value = "Your wallet hasn’t been backed up"), - subtitle = TextReference.Str( - value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + - "ut labore et...", - ), + subtitle = TextReference.Str(value = "To protect your assets, we advise you to carry out this procedure"), iconResId = R.drawable.img_attention_20, - onCloseClick = {}, + buttonsState = NotificationButtonsState.SecondaryButtonConfig( + text = TextReference.Str(value = "Start backup process"), + onClick = {}, + ), ), - NotificationState.Closable( - title = TextReference.Str(value = "Your wallet hasn’t been backed up"), - subtitle = null, + NotificationConfig( + title = TextReference.Str(value = "Some addresses are missing"), + subtitle = TextReference.Str(value = "Generate addresses for 2 new networks using your card"), iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, + buttonsState = NotificationButtonsState.PrimaryButtonConfig( + text = TextReference.Str(value = "Generate addresses"), + iconResId = R.drawable.ic_tangem_24, + onClick = {}, + ), + ), + NotificationConfig( + title = TextReference.Str(value = "Rate the app"), + subtitle = TextReference.Str(value = "How do you like Tangem?"), + iconResId = R.drawable.img_attention_20, + buttonsState = NotificationButtonsState.PairButtonsConfig( + primaryText = TextReference.Str(value = "Love it!"), + onPrimaryClick = {}, + secondaryText = TextReference.Str(value = "Can be better"), + onSecondaryClick = {}, + ), + ), + NotificationConfig( + title = TextReference.Str(value = "Note top up"), + subtitle = TextReference.Str(value = "To activate card top up it with at least 1 XLM"), + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = NotificationButtonsState.SecondaryButtonConfig( + text = TextReference.Str(value = "Top up card"), + onClick = {}, + ), onCloseClick = {}, ), ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt new file mode 100644 index 0000000000..c5b35a183e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -0,0 +1,44 @@ +package com.tangem.core.ui.components.notifications + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +/** + * Notification component state + * + * @property title title + * @property subtitle subtitle + * @property iconResId icon resource id + * @property buttonsState buttons state + * @property onClick lambda be invoked when notification is clicked + * @property onCloseClick lambda be invoked when close button is clicked + * +[REDACTED_AUTHOR] + */ +data class NotificationConfig( + val title: TextReference, + val subtitle: TextReference, + @DrawableRes val iconResId: Int, + val buttonsState: ButtonsState? = null, + val onClick: (() -> Unit)? = null, + val onCloseClick: (() -> Unit)? = null, +) { + + sealed class ButtonsState { + + data class PrimaryButtonConfig( + val text: TextReference, + @DrawableRes val iconResId: Int? = null, + val onClick: () -> Unit, + ) : ButtonsState() + + data class SecondaryButtonConfig(val text: TextReference, val onClick: () -> Unit) : ButtonsState() + + data class PairButtonsConfig( + val primaryText: TextReference, + val onPrimaryClick: () -> Unit, + val secondaryText: TextReference, + val onSecondaryClick: () -> Unit, + ) : ButtonsState() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt deleted file mode 100644 index f787e62a66..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.core.ui.components.notifications - -import androidx.annotation.DrawableRes -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.extensions.TextReference - -/** - * Notification component state - * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property tint icon tint - * -[REDACTED_AUTHOR] - */ -sealed class NotificationState( - open val title: TextReference, - open val subtitle: TextReference? = null, - @DrawableRes open val iconResId: Int, - open val tint: Color? = null, -) { - - /** - * Simple notification state. Non clickable. - * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property tint icon tint - */ - data class Simple( - override val title: TextReference, - override val subtitle: TextReference? = null, - @DrawableRes override val iconResId: Int, - override val tint: Color? = null, - ) : NotificationState(title, subtitle, iconResId, tint) - - /** - * Clickable notification state - * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property tint icon tint - * @property onClick lambda be invoked when notification component is clicked - */ - data class Clickable( - override val title: TextReference, - override val subtitle: TextReference? = null, - @DrawableRes override val iconResId: Int, - override val tint: Color? = null, - val onClick: () -> Unit, - ) : NotificationState(title, subtitle, iconResId, tint) - - /** - * Closable notification state - * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property tint icon tint - * @property onCloseClick lambda be invoked when close button is clicked - */ - data class Closable( - override val title: TextReference, - override val subtitle: TextReference? = null, - @DrawableRes override val iconResId: Int, - override val tint: Color? = null, - val onCloseClick: (() -> Unit)? = null, - ) : NotificationState(title, subtitle, iconResId, tint) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 93a6d16058..f28a081419 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -35,10 +35,7 @@ fun LazyListScope.txHistoryItems( ) } is TxHistoryState.Empty -> { - nonContentItem( - state = EmptyTransactionsBlockState.Empty(onClick = state.onBuyClick), - modifier = modifier, - ) + nonContentItem(state = EmptyTransactionsBlockState.Empty, modifier = modifier) } is TxHistoryState.Error -> { nonContentItem( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt index 4045bf505e..7e5efdd446 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt @@ -47,7 +47,9 @@ fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier color = TangemTheme.colors.text.secondary, ) - ActionButton(config = state.actionButtonConfig) + state.actionButtonConfig?.let { + ActionButton(config = it) + } } } @@ -73,7 +75,7 @@ private fun EmptyTransactionBlock_Dark( private class EmptyTransactionBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( - EmptyTransactionsBlockState.Empty(onClick = {}), + EmptyTransactionsBlockState.Empty, EmptyTransactionsBlockState.FailedToLoad(onClick = {}), EmptyTransactionsBlockState.NotImplemented(onClick = {}), ), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt index d4c4dad238..6aa8c653c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt @@ -5,12 +5,12 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.extensions.TextReference sealed class EmptyTransactionsBlockState( + val actionButtonConfig: ActionButtonConfig? = null, val iconRes: Int, val text: TextReference, - val actionButtonConfig: ActionButtonConfig, ) { - class FailedToLoad(onClick: () -> Unit) : EmptyTransactionsBlockState( + data class FailedToLoad(val onClick: () -> Unit) : EmptyTransactionsBlockState( actionButtonConfig = ActionButtonConfig( text = TextReference.Res(R.string.common_reload), iconResId = R.drawable.ic_refresh_24, @@ -21,18 +21,12 @@ sealed class EmptyTransactionsBlockState( text = TextReference.Res(R.string.transaction_history_error_failed_to_load), ) - class Empty(onClick: (() -> Unit)?) : EmptyTransactionsBlockState( - actionButtonConfig = ActionButtonConfig( - text = TextReference.Res(R.string.common_buy), - iconResId = R.drawable.ic_plus_24, - onClick = onClick ?: {}, - enabled = onClick != null, - ), - iconRes = R.drawable.img_coin_64, + object Empty : EmptyTransactionsBlockState( + iconRes = R.drawable.ic_empty_token_64, text = TextReference.Res(R.string.transaction_history_empty_transactions), ) - class NotImplemented(onClick: () -> Unit) : EmptyTransactionsBlockState( + data class NotImplemented(val onClick: () -> Unit) : EmptyTransactionsBlockState( actionButtonConfig = ActionButtonConfig( text = TextReference.Res(R.string.common_explore_transaction_history), iconResId = R.drawable.ic_arrow_top_right_24, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt deleted file mode 100644 index 4abce062be..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/intents/TxHistoryClickIntents.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.core.ui.components.transactions.intents - -interface TxHistoryClickIntents { - - fun onBuyClick() - - fun onReloadClick() - - fun onExploreClick() -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt index a6035f48dc..c6a42aacbb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TxHistoryState.kt @@ -13,12 +13,8 @@ sealed interface TxHistoryState { */ data class Content(val contentItems: MutableStateFlow>) : TxHistoryState - /** - * Empty state - * - * @property onBuyClick lambda be invoke when buy button was clicked - */ - data class Empty(val onBuyClick: () -> Unit) : TxHistoryState + /** Empty state */ + object Empty : TxHistoryState /** * Not supported tx history state diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt index 49abda9299..e6f3ad0d84 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/event/EventEffect.kt @@ -13,11 +13,12 @@ import androidx.compose.runtime.NonRestartableComposable */ @Composable @NonRestartableComposable -fun EventEffect(event: StateEvent, onTrigger: suspend () -> Unit) { +@Suppress("UnnecessaryEventHandlerParameter") +fun EventEffect(event: StateEvent, onTrigger: suspend (data: A) -> Unit) { LaunchedEffect(event) { - if (event is StateEvent.Triggered) { - onTrigger() - event.consume() + if (event is StateEvent.Triggered) { + onTrigger(event.data) + event.onConsume() } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt index d0f495b944..1860808b1d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/event/StateEvent.kt @@ -9,32 +9,34 @@ import androidx.compose.runtime.Immutable * re-triggered on recompositions or state changes. */ @Immutable -sealed class StateEvent { - - /** Defines the action to be executed when the event is consumed. */ - protected abstract val onConsume: () -> Unit +sealed class StateEvent { /** * Represents an already consumed state event. * Events of this type will not trigger any further actions. */ - object Consumed : StateEvent() { - override val onConsume: () -> Unit = {} + class Consumed : StateEvent() { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Consumed<*> + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } } /** * Represents a state event that has been triggered but not yet consumed. * + * @property data The data provided by the event. * @property onConsume The action to be executed when the event is consumed. */ - data class Triggered(override val onConsume: () -> Unit) : StateEvent() - - /** - * Consumes the event, triggering any associated action. - */ - fun consume() { - onConsume() - } + data class Triggered( + val data: A, + internal val onConsume: () -> Unit, + ) : StateEvent() } /** @@ -43,9 +45,11 @@ sealed class StateEvent { * @param onConsume The action to be executed when the event is consumed. * @return A triggered state event. */ -fun triggered(onConsume: () -> Unit): StateEvent.Triggered = StateEvent.Triggered(onConsume) +fun triggeredEvent(data: A, onConsume: () -> Unit): StateEvent = StateEvent.Triggered(data, onConsume) /** - * Represents a statically defined [StateEvent.Consumed] event. + * Creates a [StateEvent.Consumed] instance. + * + * @return A consumed state event. */ -val consumed: StateEvent.Consumed = StateEvent.Consumed \ No newline at end of file +fun consumedEvent(): StateEvent = StateEvent.Consumed() \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Context.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Context.kt new file mode 100644 index 0000000000..17439269f9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/Context.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.extensions + +import android.content.Context +import android.content.Intent +import androidx.core.content.ContextCompat + +fun Context.shareText(text: String) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + ContextCompat.startActivity(this, shareIntent, null) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt index 7c32f55014..c30323a1b2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt @@ -1,45 +1,52 @@ package com.tangem.core.ui.extensions import androidx.annotation.DrawableRes -import com.tangem.core.ui.R +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.core.graphics.toColorInt +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.tokens.models.CryptoCurrency +private const val LIGHT_LUMINANCE = 0.5f +private const val COLOR_HEX_START_INDEX = 2 +private const val COLOR_HEX_END_INDEX = 7 + /** - * Retrieves the resource ID for the network badge of a [CryptoCurrency]. + * Retrieves the resource ID for the network of a [CryptoCurrency]. * - * This property provides a way to fetch the appropriate drawable resource ID - * for the network badge of a given cryptocurrency. For coins, this will typically - * return null as they do not have network badges, while tokens will fetch the icon - * based on their associated network ID. - * - * @return Drawable resource ID for the network badge or null if the cryptocurrency is a coin. + * @return Drawable resource ID for the network. */ @get:DrawableRes -val CryptoCurrency.networkBadgeIconResId: Int? - get() = when (this) { - is CryptoCurrency.Coin -> null - is CryptoCurrency.Token -> getActiveIconRes(network.id.value) +val CryptoCurrency.networkIconResId: Int + get() = getActiveIconRes(network.id.value) + +/** + * Tries to extract a background color from the contract address of a token. + * + * @param fallbackColor The color to use as a fallback. + * @return The extracted background color or the fallback color if extraction fails or if it is a test network token. + */ +fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon( + isGrayscale: Boolean, + fallbackColor: Color = TangemColorPalette.Black, +): Color { + if (isGrayscale) return TangemColorPalette.Dark2 + + return try { + val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX) + Color(colorHex.toColorInt()) + } catch (exception: Exception) { + fallbackColor } +} /** - * Retrieves the resource ID for the icon of a [CryptoCurrency]. + * Determines the tint color to be used for a token icon based on its background color. + * If the icon's background color is light, a dark tint is chosen; otherwise, a light tint is chosen. * - * This property provides a way to fetch the appropriate drawable resource ID - * for the icon of a given cryptocurrency. - * - * @return Drawable resource ID for the cryptocurrency icon. + * @param iconBackground The background color of the custom token icon. + * @return The tint color to be used for the icon. */ -@get:DrawableRes -val CryptoCurrency.iconResId: Int - get() = when (this) { - is CryptoCurrency.Coin -> { - val rawCoinId = id.rawCurrencyId - - if (rawCoinId != null) { - getActiveIconResByCoinId(rawCoinId, network.id.value) - } else { - R.drawable.ic_alert_24 - } - } - is CryptoCurrency.Token -> R.drawable.ic_alert_24 - } \ No newline at end of file +fun getTintForTokenIcon(iconBackground: Color): Color { + return if (iconBackground.luminance() > LIGHT_LUMINANCE) TangemColorPalette.Black else TangemColorPalette.White +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt new file mode 100644 index 0000000000..f5cc3cb971 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/String.kt @@ -0,0 +1,32 @@ +package com.tangem.core.ui.extensions + +import android.graphics.Bitmap +import android.graphics.Color +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.qrcode.QRCodeWriter +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import java.util.Hashtable + +@Suppress("MagicNumber") +fun String.toQrCode(sizePx: Int = 256, paddingPx: Int = 0): Bitmap { + val hintMap = Hashtable() + hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage + hintMap[EncodeHintType.MARGIN] = paddingPx + + val qrCodeWriter = QRCodeWriter() + + val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, sizePx, sizePx, hintMap) + val width = bitMatrix.width + val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565) + for (x in 0 until width) { + for (y in 0 until width) { + bmp.setPixel( + y, + x, + if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE, + ) + } + } + return bmp +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index bc8107d5f5..d44b32ecdf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.extensions +import android.content.res.Resources import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.compose.runtime.Composable @@ -48,6 +49,59 @@ sealed interface TextReference { * @see [TextReference.plus] method */ data class Combined(val refs: WrappedList) : TextReference + + companion object { + + /** Empty string as [TextReference] */ + val EMPTY: TextReference by lazy(mode = LazyThreadSafetyMode.NONE) { Str(value = "") } + } +} + +/** + * Creates a [TextReference] using a string resource ID with optional format arguments. + * + * @param id The resource ID of the string. + * @param formatArgs A list of format arguments to be applied to the string resource. + * @return A [TextReference] representing the string resource with format arguments. + */ +fun resourceReference(@StringRes id: Int, formatArgs: WrappedList = WrappedList(emptyList())): TextReference { + return TextReference.Res(id, formatArgs) +} + +/** + * Creates a [TextReference] using a plain string value. + * + * @param value The plain string value. + * @return A [TextReference] representing the provided string value. + */ +fun stringReference(value: String): TextReference { + return TextReference.Str(value) +} + +/** + * Creates a [TextReference] using a plural string resource ID with count and optional format arguments. + * + * @param id The resource ID of the plural string. + * @param count The count value to determine the plural form. + * @param formatArgs A list of format arguments to be applied to the plural string resource. + * @return A [TextReference] representing the plural string resource with count and format arguments. + */ +fun pluralReference( + @PluralsRes id: Int, + count: Int, + formatArgs: WrappedList = WrappedList(emptyList()), +): TextReference { + return TextReference.PluralRes(id, count, formatArgs) +} + +/** + * Combines multiple [TextReference] instances into a single [TextReference]. + * + * @param refs A list of [TextReference] instances to be combined. + * @return A [TextReference] representing the combined text references. + */ +fun combinedReference(refs: WrappedList): TextReference { + return TextReference.Combined(refs) } /** Resolve [TextReference] as [String] */ @@ -55,7 +109,13 @@ sealed interface TextReference { @ReadOnlyComposable fun TextReference.resolveReference(): String { return when (this) { - is TextReference.Res -> stringResource(id, *formatArgs.toTypedArray()) + is TextReference.Res -> { + val args = formatArgs + .map { if (it is TextReference) it.resolveReference() else it } + .toTypedArray() + + stringResource(id = id, *args) + } is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray()) is TextReference.Str -> value is TextReference.Combined -> { @@ -68,6 +128,28 @@ fun TextReference.resolveReference(): String { } } +/** Resolve [TextReference] as [String] using [resources] (non-composable context) */ +fun TextReference.resolveReference(resources: Resources): String { + return when (this) { + is TextReference.Res -> { + val args = formatArgs + .map { if (it is TextReference) it.resolveReference(resources) else it } + .toTypedArray() + + resources.getString(id, *args) + } + is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray()) + is TextReference.Str -> value + is TextReference.Combined -> { + buildString { + refs.forEach { + append(it.resolveReference(resources)) + } + } + } + } +} + /** Concatenate [this] reference with [ref] */ operator fun TextReference.plus(ref: TextReference): TextReference { return when (this) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt b/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt index f009843950..bcfec7a2b5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/IconColorType.kt @@ -4,6 +4,7 @@ import androidx.compose.material.Colors import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.tangem.core.ui.res.TangemColorPalette.Amaranth +import com.tangem.core.ui.res.TangemColorPalette.Azure import com.tangem.core.ui.res.TangemColorPalette.Black import com.tangem.core.ui.res.TangemColorPalette.Dark1 import com.tangem.core.ui.res.TangemColorPalette.Dark2 @@ -11,7 +12,6 @@ import com.tangem.core.ui.res.TangemColorPalette.Dark4 import com.tangem.core.ui.res.TangemColorPalette.Dark6 import com.tangem.core.ui.res.TangemColorPalette.Light4 import com.tangem.core.ui.res.TangemColorPalette.Light5 -import com.tangem.core.ui.res.TangemColorPalette.Meadow import com.tangem.core.ui.res.TangemColorPalette.Tangerine import com.tangem.core.ui.res.TangemColorPalette.White @@ -22,7 +22,7 @@ enum class IconColorType(val lightColor: Color, val darkColor: Color) { SECONDARY(lightColor = Dark2, darkColor = Dark1), INFORMATIVE(lightColor = Light5, darkColor = Dark2), INACTIVE(lightColor = Light4, darkColor = Dark4), - ACCENT(lightColor = Meadow, darkColor = Meadow), + ACCENT(lightColor = Azure, darkColor = Azure), WARNING(lightColor = Amaranth, darkColor = Amaranth), ATTENTION(lightColor = Tangerine, darkColor = Tangerine), } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 5f9617639f..6c0bd9671f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -33,16 +33,16 @@ object TangemColorPalette { // endregion Green // region Blue - val Cobalt = Color(0xFF0029FF) - val Azure = Color(0xFF007AFF) + val Azure = Color(0xFF0099FF) // endregion Blue // region Red - val Amaranth = Color(0xFFDE1010) - val Flamingo = Color(0xFFED7979) + val Amaranth = Color(0xFFFF3333) + val Flamingo = Color(0xFFFF5B5B) // endregion Red // region Yellow val Tangerine = Color(0xFFFFB71B) + val Mustard = Color(0xFFFDDE55) // endregion Yellow } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt index 8dc45fd453..f8ad2530e8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors.kt @@ -39,9 +39,9 @@ class TangemColors internal constructor( secondary: Color, tertiary: Color, disabled: Color, - accent: Color = TangemColorPalette.Meadow, - warning: Color = TangemColorPalette.Amaranth, - attention: Color = TangemColorPalette.Tangerine, + warning: Color, + attention: Color, + accent: Color = TangemColorPalette.Azure, constantWhite: Color = TangemColorPalette.White, ) { var primary1 by mutableStateOf(primary1) @@ -80,9 +80,9 @@ class TangemColors internal constructor( secondary: Color, informative: Color, inactive: Color, - accent: Color = TangemColorPalette.Meadow, - warning: Color = TangemColorPalette.Amaranth, - attention: Color = TangemColorPalette.Tangerine, + warning: Color, + attention: Color, + accent: Color = TangemColorPalette.Azure, ) { var primary1 by mutableStateOf(primary1) private set 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 b4fd692f7e..3f80bcbc71 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 @@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -@Suppress("ConstructorParameterNaming") +@Suppress("ConstructorParameterNaming", "MagicNumber") @Immutable data class TangemDimens internal constructor( // region Elevation @@ -81,6 +81,7 @@ data class TangemDimens internal constructor( val size158: Dp = 158.dp, val size164: Dp = 164.dp, val size200: Dp = 200.dp, + val size248: Dp = 248.dp, // endregion Size // region Spacing val spacing0: Dp = 0.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt index a3802221fb..8bf99452ed 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt @@ -13,6 +13,7 @@ data class TangemShapes internal constructor( val roundedCornersXMedium: Shape, val roundedCornersLarge: Shape, val bottomSheet: Shape, + val bottomSheetLarge: Shape, ) { constructor(dimens: TangemDimens) : this( roundedCornersSmall = RoundedCornerShape(size = dimens.radius2), @@ -25,5 +26,9 @@ data class TangemShapes internal constructor( topStart = dimens.radius16, topEnd = dimens.radius16, ), + bottomSheetLarge = RoundedCornerShape( + topStart = dimens.radius24, + topEnd = dimens.radius24, + ), ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 139dc810c6..96cc2f168a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -36,6 +36,7 @@ fun TangemTheme( LocalTangemTypography provides typography, LocalTangemDimens provides dimens, LocalTangemShapes provides shapes, + LocalIsInDarkTheme provides isDark, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -88,6 +89,7 @@ private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors { } @Composable +@ReadOnlyComposable private fun lightThemeColors(): TangemColors { return TangemColors( text = TangemColors.Text( @@ -96,6 +98,8 @@ private fun lightThemeColors(): TangemColors { secondary = TangemColorPalette.Dark2, tertiary = TangemColorPalette.Dark1, disabled = TangemColorPalette.Light4, + warning = TangemColorPalette.Amaranth, + attention = TangemColorPalette.Tangerine, ), icon = TangemColors.Icon( primary1 = TangemColorPalette.Black, @@ -103,6 +107,8 @@ private fun lightThemeColors(): TangemColors { secondary = TangemColorPalette.Dark2, informative = TangemColorPalette.Light5, inactive = TangemColorPalette.Light4, + warning = TangemColorPalette.Amaranth, + attention = TangemColorPalette.Tangerine, ), button = TangemColors.Button( primary = TangemColorPalette.Dark6, @@ -135,6 +141,7 @@ private fun lightThemeColors(): TangemColors { } @Composable +@ReadOnlyComposable private fun darkThemeColors(): TangemColors { return TangemColors( text = TangemColors.Text( @@ -143,6 +150,8 @@ private fun darkThemeColors(): TangemColors { secondary = TangemColorPalette.Light5, tertiary = TangemColorPalette.Dark1, disabled = TangemColorPalette.Dark3, + warning = TangemColorPalette.Flamingo, + attention = TangemColorPalette.Mustard, ), icon = TangemColors.Icon( primary1 = TangemColorPalette.White, @@ -150,6 +159,8 @@ private fun darkThemeColors(): TangemColors { secondary = TangemColorPalette.Dark1, informative = TangemColorPalette.Dark2, inactive = TangemColorPalette.Dark4, + warning = TangemColorPalette.Flamingo, + attention = TangemColorPalette.Mustard, ), button = TangemColors.Button( primary = TangemColorPalette.Light1, @@ -195,4 +206,6 @@ private val LocalTangemDimens = staticCompositionLocalOf { private val LocalTangemShapes = staticCompositionLocalOf { error("No TangemShapes provided") -} \ No newline at end of file +} + +val LocalIsInDarkTheme = staticCompositionLocalOf { false } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_arrow_down_8.xml b/core/ui/src/main/res/drawable/ic_arrow_down_8.xml new file mode 100644 index 0000000000..94ff984c79 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_arrow_down_8.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_arrow_up_8.xml b/core/ui/src/main/res/drawable/ic_arrow_up_8.xml new file mode 100644 index 0000000000..0087a3703c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_arrow_up_8.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_currency_24.xml b/core/ui/src/main/res/drawable/ic_currency_24.xml index bee87c2b68..1b8eb1b684 100644 --- a/core/ui/src/main/res/drawable/ic_currency_24.xml +++ b/core/ui/src/main/res/drawable/ic_currency_24.xml @@ -5,5 +5,5 @@ android:viewportHeight="24"> + android:fillColor="#000000" /> diff --git a/core/ui/src/main/res/drawable/ic_custom_token_44.xml b/core/ui/src/main/res/drawable/ic_custom_token_44.xml new file mode 100644 index 0000000000..b1d31ab0bd --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_custom_token_44.xml @@ -0,0 +1,10 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_empty_token_64.xml b/core/ui/src/main/res/drawable/ic_empty_token_64.xml new file mode 100644 index 0000000000..51821ac760 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_empty_token_64.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_exchange_horizontal_24.xml b/core/ui/src/main/res/drawable/ic_exchange_horizontal_24.xml new file mode 100644 index 0000000000..ab4c32c060 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_exchange_horizontal_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_eye_off_24.xml b/core/ui/src/main/res/drawable/ic_eye_off_24.xml deleted file mode 100644 index 7454717d84..0000000000 --- a/core/ui/src/main/res/drawable/ic_eye_off_24.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/ui/src/main/res/drawable/img_arrow_down_8.xml b/core/ui/src/main/res/drawable/img_arrow_down_8.xml deleted file mode 100644 index 355357c8c3..0000000000 --- a/core/ui/src/main/res/drawable/img_arrow_down_8.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/ui/src/main/res/drawable/img_arrow_up_8.xml b/core/ui/src/main/res/drawable/img_arrow_up_8.xml deleted file mode 100644 index 0d99c9d789..0000000000 --- a/core/ui/src/main/res/drawable/img_arrow_up_8.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt index ee1c005d8c..6ecaef28b8 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt @@ -3,6 +3,8 @@ package com.tangem.utils.extensions /** * Removes an element from the collection based on the provided predicate. * + * !!!This function is not thread-safe!!! + * * @param predicate The condition to remove an element. * @return [Boolean] indicating whether an element was removed. */ @@ -33,7 +35,8 @@ inline fun MutableList.replaceBy(item: T, predicate: (T) -> Boolean): Boo /** * Adds the specified element to the list or replaces an existing element. - * The predicate defines the condition to replace the existing element. + * + * !!!This function is not thread-safe!!! * * @param item The element to be added or replace the existing one. * @param predicate The condition to replace an existing element. diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt new file mode 100644 index 0000000000..f5c0cf1eb7 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt @@ -0,0 +1,39 @@ +package com.tangem.utils.extensions + +/** + * Replaces an element in the set with the provided item based on the predicate. + * + * !!!This function is not thread-safe!!! + * + * @param item The element to replace the existing one. + * @param predicate The condition to replace an existing element. + * @return [Boolean] indicating whether an element was replaced. + */ +inline fun MutableSet.replaceBy(item: T, predicate: (T) -> Boolean): Boolean { + val foundItem = firstOrNull(predicate) ?: return false + + remove(foundItem) + add(item) + + return true +} + +/** + * Adds the specified element to the set or replaces an existing element. + * + * !!!This function is not thread-safe!!! + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + * @return The modified [Set] after adding or replacing the element. + */ +inline fun Set.addOrReplace(item: T, predicate: (T) -> Boolean): Set { + val mutableList = this.toMutableSet() + val isReplaced = mutableList.replaceBy(item, predicate) + + if (!isReplaced) { + mutableList.add(item) + } + + return mutableList +} \ 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 c2fe02a2b5..a20bd2309b 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 @@ -88,6 +88,8 @@ internal class DefaultAppCurrencyRepository( Timber.e(e, "Unable to fetch available currencies") availableAppCurrenciesStore.store(getDefaultCurrenciesResponse()) + + throw e } } diff --git a/data/app-theme/build.gradle.kts b/data/app-theme/build.gradle.kts index b0f1f17a6a..7996a21c65 100644 --- a/data/app-theme/build.gradle.kts +++ b/data/app-theme/build.gradle.kts @@ -12,13 +12,11 @@ android { dependencies { /** Project - Domain */ - implementation(projects.domain.core) implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) /** Project - Data */ implementation(projects.core.datasource) - implementation(projects.data.common) /** Project - Utils */ implementation(projects.core.utils) @@ -29,6 +27,4 @@ dependencies { /** Other */ implementation(deps.kotlin.coroutines) - implementation(deps.timber) - implementation(deps.jodatime) } \ 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 new file mode 100644 index 0000000000..4d8ed9cf4c --- /dev/null +++ b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/DefaultAppThemeModeRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.data.apptheme + +import com.tangem.datasource.local.apptheme.AppThemeModeStore +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 + +internal class DefaultAppThemeModeRepository( + private val appThemeModeStore: AppThemeModeStore, + private val dispatchers: CoroutineDispatcherProvider, +) : AppThemeModeRepository { + + override fun getAppThemeMode(): Flow { + return channelFlow { + launch(dispatchers.io) { + if (appThemeModeStore.isEmpty()) { + appThemeModeStore.store(AppThemeMode.DEFAULT) + } + } + + launch(dispatchers.io) { + appThemeModeStore.get().collect(::send) + } + } + } + + override suspend fun changeAppThemeMode(mode: AppThemeMode) { + withContext(dispatchers.io) { + appThemeModeStore.store(mode) + } + } +} \ No newline at end of file diff --git a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt b/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt deleted file mode 100644 index a4a885aab2..0000000000 --- a/data/app-theme/src/main/kotlin/com/tangem/data/apptheme/MockAppThemeModeRepository.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.data.apptheme - -import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.apptheme.repository.AppThemeModeRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -internal class MockAppThemeModeRepository : AppThemeModeRepository { - - private val appThemeModeFlow = MutableStateFlow(AppThemeMode.DEFAULT) - - override fun getAppThemeMode(): Flow { - return appThemeModeFlow - } - - override suspend fun changeAppThemeMode(mode: AppThemeMode) { - appThemeModeFlow.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 6847fad0b8..a360ba19b6 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,7 +1,9 @@ package com.tangem.data.apptheme.di -import com.tangem.data.apptheme.MockAppThemeModeRepository +import com.tangem.data.apptheme.DefaultAppThemeModeRepository +import com.tangem.datasource.local.apptheme.AppThemeModeStore import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,7 +16,10 @@ internal object AppThemeModeDataModule { @Provides @Singleton - fun provideAppThemeModeRepository(): AppThemeModeRepository { - return MockAppThemeModeRepository() + fun provideAppThemeModeRepository( + appThemeModeStore: AppThemeModeStore, + dispatchers: CoroutineDispatcherProvider, + ): AppThemeModeRepository { + return DefaultAppThemeModeRepository(appThemeModeStore, dispatchers) } } \ No newline at end of file diff --git a/data/balance-hiding/.gitignore b/data/balance-hiding/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/balance-hiding/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/balance-hiding/build.gradle.kts b/data/balance-hiding/build.gradle.kts new file mode 100644 index 0000000000..434d3e0a34 --- /dev/null +++ b/data/balance-hiding/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.balancehiding" +} + +dependencies { + + /** DI */ + implementation(deps.hilt.android) + + kapt(deps.hilt.kapt) + + implementation(deps.kotlin.coroutines) + + implementation(projects.core.utils) + implementation(projects.core.datasource) + + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) +} 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 new file mode 100644 index 0000000000..879bef5f17 --- /dev/null +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultBalanceHidingRepository.kt @@ -0,0 +1,36 @@ +package com.tangem.data.balancehiding + +import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore +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, +) : BalanceHidingRepository { + + override fun getBalanceHidingSettingsFlow(): Flow { + return balanceHidingSettingsStore.get() + .onStart { emit(getBalanceHidingSettings()) } + .flowOn(dispatchers.io) + .distinctUntilChanged() + } + + override suspend fun storeBalanceHidingSettings(balanceHidingSettings: BalanceHidingSettings) { + withContext(dispatchers.io) { + balanceHidingSettingsStore.store(balanceHidingSettings) + } + } + + override suspend fun getBalanceHidingSettings(): BalanceHidingSettings { + return withContext(dispatchers.io) { + balanceHidingSettingsStore.getSyncOrDefault() + } + } +} \ No newline at end of file diff --git a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultDeviceFlipDetector.kt b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultDeviceFlipDetector.kt new file mode 100644 index 0000000000..30b2e97726 --- /dev/null +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultDeviceFlipDetector.kt @@ -0,0 +1,25 @@ +package com.tangem.data.balancehiding + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorManager +import com.tangem.domain.balancehiding.DeviceFlipDetector +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow + +internal class DefaultDeviceFlipDetector(context: Context) : DeviceFlipDetector { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private var gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) + + override fun getDeviceFlipFlow(): Flow = callbackFlow { + val listener = FlipListener { trySend(Unit) } + + gravitySensor?.let { + sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_NORMAL) + } + + awaitClose { sensorManager.unregisterListener(listener) } + } +} \ No newline at end of file diff --git a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt new file mode 100644 index 0000000000..b535797121 --- /dev/null +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt @@ -0,0 +1,39 @@ +package com.tangem.data.balancehiding + +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.os.SystemClock + +internal class FlipListener(private val action: () -> Unit) : SensorEventListener { + + private val zAxisThreshold = -6 + private val throttleTimeMs = 3000 + private var lastTriggerTime = 0L + private var isScreenDown = false + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + /* no-op */ + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + val currentTime = SystemClock.elapsedRealtime() + val zAxisValue = it.values[2] + + if (zAxisValue < zAxisThreshold && !isScreenDown) { + isScreenDown = true + lastTriggerTime = currentTime + // TODO add module logging + // Timber.tag("onSensorChanged").d("screen down") + } else if (zAxisValue >= zAxisThreshold) { + if (isScreenDown && currentTime - lastTriggerTime <= throttleTimeMs) { + // Timber.tag("onSensorChanged").d("screen up!") + lastTriggerTime = currentTime + action.invoke() + } + isScreenDown = false + } + } + } +} \ 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 new file mode 100644 index 0000000000..86b293b9fe --- /dev/null +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/di/BalanceHidingModule.kt @@ -0,0 +1,38 @@ +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.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.components.SingletonComponent +import javax.inject.Singleton +import dagger.hilt.android.qualifiers.ApplicationContext + +@Module +@InstallIn(SingletonComponent::class) +internal object BalanceHidingModule { + + @Provides + @Singleton + fun provideBalanceHidingRepository( + balanceHidingSettingsStore: BalanceHidingSettingsStore, + dispatchers: CoroutineDispatcherProvider, + ): BalanceHidingRepository { + return DefaultBalanceHidingRepository( + balanceHidingSettingsStore = balanceHidingSettingsStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideFlipDetector(@ApplicationContext context: Context): DeviceFlipDetector { + return DefaultDeviceFlipDetector(context = context) + } +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt index 1249de10d7..9c17c4c127 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -19,7 +19,7 @@ import javax.inject.Singleton internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkLifecycleObserver { override val sdk: TangemSdk - get() = requireNotNull(value = _sdk) { "Impossible to get the TangemSdk when activity is destroyed" } + get() = requireNotNull(value = _sdk?.get()) { "Impossible to get the TangemSdk when activity is destroyed" } private var _sdk: TangemSdk? = null diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 1413053d65..e59302acd9 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -14,13 +14,16 @@ dependencies { /** 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/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 6ef7e9a280..6d0736bdf1 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 @@ -20,6 +20,9 @@ internal object SettingsDataModule { preferencesDataSource: PreferencesDataSource, dispatchers: CoroutineDispatcherProvider, ): SettingsRepository { - return DefaultSettingsRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers) + return DefaultSettingsRepository( + preferencesDataSource = preferencesDataSource, + dispatchers = dispatchers, + ) } } \ No newline at end of file diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index a45be27508..d36f42d591 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) /** Project - Data */ @@ -40,4 +41,5 @@ dependencies { implementation(deps.moshi.kotlin) implementation(deps.jodatime) implementation(deps.timber) + implementation(deps.retrofit) // For HttpException } \ No newline at end of file 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 c1229cde5b..cd55cde081 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 @@ -2,14 +2,17 @@ package com.tangem.data.tokens.di import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.repository.DefaultCurrenciesRepository +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.quote.QuotesStore +import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -30,10 +33,18 @@ internal object TokensDataModule { tangemTechApi: TangemTechApi, userTokensStore: UserTokensStore, userWalletsStore: UserWalletsStore, + userMarketCoinsStore: UserMarketCoinsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): CurrenciesRepository { - return DefaultCurrenciesRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers) + return DefaultCurrenciesRepository( + tangemTechApi = tangemTechApi, + userTokensStore = userTokensStore, + userWalletsStore = userWalletsStore, + userMarketCoinsStore = userMarketCoinsStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, + ) } @Provides @@ -46,11 +57,11 @@ internal object TokensDataModule { dispatchers: CoroutineDispatcherProvider, ): QuotesRepository { return DefaultQuotesRepository( - tangemTechApi, - quotesStore, - selectedAppCurrencyStore, - cacheRegistry, - dispatchers, + tangemTechApi = tangemTechApi, + quotesStore = quotesStore, + selectedAppCurrencyStore = selectedAppCurrencyStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, ) } @@ -64,11 +75,19 @@ internal object TokensDataModule { dispatchers: CoroutineDispatcherProvider, ): NetworksRepository { return DefaultNetworksRepository( - walletManagersFacade, - userWalletsStore, - userTokensStore, - cacheRegistry, - dispatchers, + walletManagersFacade = walletManagersFacade, + userWalletsStore = userWalletsStore, + userTokensStore = userTokensStore, + cacheRegistry = cacheRegistry, + dispatchers = dispatchers, ) } + + @Provides + @Singleton + fun provideDefaultMarketCoinsRepository( + userMarketCoinsStore: UserMarketCoinsStore, + ): MarketCryptoCurrencyRepository { + return DefaultMarketCryptoCurrencyRepository(userMarketCoinsStore) + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 0df288c0c0..e29095c31d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -1,17 +1,20 @@ package com.tangem.data.tokens.repository +import com.tangem.blockchain.common.Blockchain import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.utils.CardCurrenciesFactory -import com.tangem.data.tokens.utils.ResponseCurrenciesFactory -import com.tangem.data.tokens.utils.UserTokensResponseFactory +import com.tangem.data.tokens.utils.* import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.token.UserMarketCoinsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.toCoinId +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId @@ -21,19 +24,21 @@ import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import retrofit2.HttpException import timber.log.Timber internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, + private val userMarketCoinsStore: UserMarketCoinsStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { private val demoConfig = DemoConfig() - private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig) - private val cardCurrenciesFactory = CardCurrenciesFactory(demoConfig) + private val responseCurrenciesFactory = ResponseCryptoCurrenciesFactory(demoConfig) + private val cardCurrenciesFactory = CardCryptoCurrenciesFactory(demoConfig) private val userTokensResponseFactory = UserTokensResponseFactory() override suspend fun saveTokens( @@ -53,6 +58,54 @@ internal class DefaultCurrenciesRepository( storeAndPushTokens(userWalletId, response) } + override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) { + return withContext(dispatchers.io) { + val savedCurrencies = requireNotNull( + value = userTokensStore.getSyncOrNull(userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, + ) + + val newCoins = createCoinsForNewTokens( + userWalletId = userWalletId, + newTokens = currencies.filterIsInstance(), + savedCurrencies = savedCurrencies.tokens, + ) + + val newCurrencies = newCoins + currencies + + storeAndPushTokens( + userWalletId = userWalletId, + response = savedCurrencies.copy( + tokens = savedCurrencies.tokens + newCurrencies.map(userTokensResponseFactory::createResponseToken), + ), + ) + } + } + + private suspend fun createCoinsForNewTokens( + userWalletId: UserWalletId, + newTokens: List, + savedCurrencies: List, + ): List { + return newTokens + .filterNot { savedCurrencies.hasCoinForToken(it) } // tokens without coins + .mapNotNull { + CryptoCurrencyFactory().createCoin( + blockchain = getBlockchain(networkId = it.network.id), + extraDerivationPath = it.network.derivationPath.value, + derivationStyleProvider = getUserWallet(userWalletId).scanResponse.derivationStyleProvider, + ) + } + } + + private fun List.hasCoinForToken(token: CryptoCurrency.Token): Boolean { + return any { + val blockchain = getBlockchain(networkId = token.network.id) + + it.id == blockchain.toCoinId() + } + } + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = withContext(dispatchers.io) { val savedCurrencies = requireNotNull( @@ -69,6 +122,23 @@ internal class DefaultCurrenciesRepository( ) } + override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) { + return withContext(dispatchers.io) { + val savedCurrencies = requireNotNull( + value = userTokensStore.getSyncOrNull(userWalletId), + lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" }, + ) + + val tokens = currencies.map(userTokensResponseFactory::createResponseToken) + storeAndPushTokens( + userWalletId = userWalletId, + response = savedCurrencies.copy( + tokens = savedCurrencies.tokens.filterNot(tokens::contains), + ), + ) + } + } + override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { return withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) @@ -106,10 +176,7 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - return responseCurrenciesFactory.createCurrencies( - response = storedTokens, - card = userWallet.scanResponse.card, - ) + return responseCurrenciesFactory.createCurrencies(storedTokens, userWallet.scanResponse) } override suspend fun getMultiCurrencyWalletCurrency( @@ -123,7 +190,25 @@ internal class DefaultCurrenciesRepository( "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse.card) + responseCurrenciesFactory.createCurrency(id, response, userWallet.scanResponse) + } + + override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { + val userWallet = getUserWallet(userWalletId) + ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) + + fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) + + val storedTokens = requireNotNull(userTokensStore.getSyncOrNull(userWallet.walletId)) { + "Unable to find tokens response for user wallet with provided ID: $userWalletId" + } + + val storedCoin = storedTokens.tokens.find { it.networkId == Blockchain.fromId(networkId.value).toNetworkId() } + ?: error("Coin in this network $networkId not found") + + val coin = responseCurrenciesFactory.createCurrency(storedCoin, userWallet.scanResponse) + + return coin as? CryptoCurrency.Coin ?: error("Unable to create currency") } override fun isTokensGrouped(userWalletId: UserWalletId): Flow { @@ -154,7 +239,7 @@ internal class DefaultCurrenciesRepository( return userTokensStore.get(userWallet.walletId).map { storedTokens -> responseCurrenciesFactory.createCurrencies( response = storedTokens, - card = userWallet.scanResponse.card, + scanResponse = userWallet.scanResponse, ) } } @@ -168,13 +253,19 @@ internal class DefaultCurrenciesRepository( } private suspend fun fetchTokens(userWallet: UserWallet) { - try { - val response = tangemTechApi.getUserTokens(userWallet.walletId.stringValue) + val userWalletId = userWallet.walletId - userTokensStore.store(userWallet.walletId, response) - } catch (e: Throwable) { - handleFetchTokensErrorOrThrow(userWallet, e) + val response = try { + with(tangemTechApi.getUserTokens(userWalletId.stringValue)) { + // The response may contain repeated tokens + copy(tokens = tokens.distinct()) + } + } catch (e: HttpException) { + handleCurrenciesNotFoundOrThrow(userWallet, e) } + + userTokensStore.store(userWallet.walletId, response) + fetchUserMarketCoinsByIds(userWalletId, response) } private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { @@ -182,26 +273,42 @@ internal class DefaultCurrenciesRepository( tangemTechApi.saveUserTokens(userWalletId.stringValue, response) } - private suspend fun handleFetchTokensErrorOrThrow(userWallet: UserWallet, error: Throwable) { - val errorMessage = error.message ?: throw error + private suspend fun fetchUserMarketCoinsByIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + try { + val networkIds = userTokens.tokens.joinToString(separator = ",") { it.networkId } + val response = tangemTechApi.getCoins(networkIds) - if (NOT_FOUND_HTTP_CODE in errorMessage) { - val response = userTokensStore.getSyncOrNull(userWallet.walletId) - ?: userTokensResponseFactory.createUserTokensResponse( - currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard( - card = userWallet.scanResponse.card, - derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, - ), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response) - } else { - Timber.e(error, "Unable to fetch currencies for: ${userWallet.walletId}") + userMarketCoinsStore.store(userWalletId, response) + } catch (e: Throwable) { + Timber.e(e, "Unable to fetch user market coins for: ${userWalletId.stringValue}") } } + private suspend fun handleCurrenciesNotFoundOrThrow( + userWallet: UserWallet, + httpException: HttpException, + ): UserTokensResponse { + val userWalletId = userWallet.walletId + + if (httpException.code() != NOT_FOUND_HTTP_CODE) { + Timber.e(httpException, "Unable to fetch currencies for: $userWalletId") + throw httpException + } + + Timber.d("Requested currencies could not be found in the remote store for: $userWalletId") + + val response = userTokensStore.getSyncOrNull(userWalletId) + ?: userTokensResponseFactory.createUserTokensResponse( + currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard(userWallet.scanResponse), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + tangemTechApi.saveUserTokens(userWalletId.stringValue, response) + + return response + } + private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find a user wallet with provided ID: $userWalletId" @@ -238,6 +345,6 @@ internal class DefaultCurrenciesRepository( private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" private companion object { - const val NOT_FOUND_HTTP_CODE = "404" + const val NOT_FOUND_HTTP_CODE = 404 } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt new file mode 100644 index 0000000000..891a7c14d2 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.data.tokens.repository + +import com.tangem.datasource.local.token.UserMarketCoinsStore +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository +import com.tangem.domain.wallets.models.UserWalletId + +class DefaultMarketCryptoCurrencyRepository( + private val userMarketCoinsStore: UserMarketCoinsStore, +) : MarketCryptoCurrencyRepository { + + override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean { + return userMarketCoinsStore.getSyncOrNull(userWalletId)?.coins + ?.firstOrNull { it.id == cryptoCurrencyId.rawCurrencyId } + ?.networks + ?.firstOrNull { it.networkId == cryptoCurrencyId.rawNetworkId }?.exchangeable ?: false + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 5ac3840c7f..667bf5c512 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,10 +1,9 @@ package com.tangem.data.tokens.repository import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.tokens.utils.CardCurrenciesFactory -import com.tangem.data.tokens.utils.NetworkConverter +import com.tangem.data.tokens.utils.CardCryptoCurrenciesFactory import com.tangem.data.tokens.utils.NetworkStatusFactory -import com.tangem.data.tokens.utils.ResponseCurrenciesFactory +import com.tangem.data.tokens.utils.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.demo.DemoConfig @@ -28,25 +27,18 @@ internal class DefaultNetworksRepository( ) : NetworksRepository { private val demoConfig by lazy { DemoConfig() } - private val networkConverter by lazy { NetworkConverter() } - private val cardCurrenciesFactory by lazy { CardCurrenciesFactory(demoConfig) } - private val responseCurrenciesFactory by lazy { ResponseCurrenciesFactory(demoConfig) } + private val cardCurrenciesFactory by lazy { CardCryptoCurrenciesFactory(demoConfig) } + private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory(demoConfig) } private val networkStatusFactory by lazy { NetworkStatusFactory() } - private val networksStatuses: MutableStateFlow> = MutableStateFlow(emptyList()) - - override fun getNetworks(networksIds: Set): Set { - return networkConverter.convertSet(networksIds) - } + private val networksStatuses: MutableStateFlow> = MutableStateFlow(hashSetOf()) override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, - networks: Set, + networks: Set, ): Flow> = channelFlow { launch(dispatchers.io) { - networksStatuses.collect { - send(it.toSet()) - } + networksStatuses.collect(::send) } launch(dispatchers.io) { @@ -56,7 +48,7 @@ internal class DefaultNetworksRepository( override suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, - networks: Set, + networks: Set, refresh: Boolean, ): Set = withContext(dispatchers.io) { fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh) @@ -65,66 +57,86 @@ internal class DefaultNetworksRepository( private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, - networks: Set, + networks: Set, refresh: Boolean, ) { - cacheRegistry.invokeOnExpire( - key = getNetworksStatusesCacheKey(userWalletId), - skipCache = refresh, - block = { fetchNetworksStatuses(userWalletId, networks) }, - ) - } - - private suspend fun fetchNetworksStatuses(userWalletId: UserWalletId, networks: Set) { coroutineScope { networks - .map { networkId -> + .map { network -> async { - fetchNetworkStatus(userWalletId, networkId) + fetchNetworkStatusIfCacheExpired(userWalletId, network, refresh) } } .awaitAll() } } - private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, networkId: Network.ID) { - val currencies = getCurrencies(userWalletId) - .asSequence() - .filter { it.network.id == networkId } + private suspend fun fetchNetworkStatusIfCacheExpired( + userWalletId: UserWalletId, + network: Network, + refresh: Boolean, + ) { + cacheRegistry.invokeOnExpire( + key = getNetworksStatusesCacheKey(userWalletId, network), + skipCache = refresh, + block = { fetchNetworkStatus(userWalletId, network) }, + ) + } + + private suspend fun fetchNetworkStatus(userWalletId: UserWalletId, network: Network) { + val currencies = getCurrencies(userWalletId, network) val result = walletManagersFacade.update( userWalletId = userWalletId, - networkId = networkId, + network = network, extraTokens = currencies.filterIsInstance().toSet(), ) + val networkStatus = networkStatusFactory.createNetworkStatus( - networkId = networkId, + network = network, result = result, currencies = currencies.toSet(), ) networksStatuses.update { statuses -> - statuses.addOrReplace(networkStatus) { it.networkId == networkStatus.networkId } + statuses.addOrReplace(networkStatus) { it.network == networkStatus.network } } + + invalidateCacheKeyIfNeeded(userWalletId, networkStatus) } - private suspend fun getCurrencies(userWalletId: UserWalletId): List { + private suspend fun getCurrencies(userWalletId: UserWalletId, network: Network): Sequence { val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find user wallet with provided ID: $userWalletId" } - return if (userWallet.isMultiCurrency) { + val currencies = if (userWallet.isMultiCurrency) { val response = requireNotNull(userTokensStore.getSyncOrNull(userWalletId)) { "Unable to find tokens response for user wallet with provided ID: $userWalletId" } - responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse.card) + responseCurrenciesFactory.createCurrencies(response, userWallet.scanResponse).asSequence() } else { val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) - listOf(currency) + sequenceOf(currency) + } + + return currencies.filter { it.network == network } + } + + private suspend fun invalidateCacheKeyIfNeeded(userWalletId: UserWalletId, networkStatus: NetworkStatus) { + when (networkStatus.value) { + is NetworkStatus.Verified, + is NetworkStatus.NoAccount, + -> Unit + is NetworkStatus.Unreachable, + is NetworkStatus.MissedDerivation, + -> cacheRegistry.invalidate(getNetworksStatusesCacheKey(userWalletId, networkStatus.network)) } } - private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId): String = "network_status_$userWalletId" + private fun getNetworksStatusesCacheKey(userWalletId: UserWalletId, network: Network): String { + return "network_status_${userWalletId}_${network.id}_${network.derivationPath.value}" + } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt new file mode 100644 index 0000000000..aa96c7a9b5 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCryptoCurrenciesFactory.kt @@ -0,0 +1,61 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.models.CryptoCurrency + +internal class CardCryptoCurrenciesFactory(private val demoConfig: DemoConfig) { + + private val cryptoCurrencyFactory = CryptoCurrencyFactory() + + fun createDefaultCoinsForMultiCurrencyCard(scanResponse: ScanResponse): List { + val cardDerivationStyleProvider = scanResponse.derivationStyleProvider + val card = scanResponse.card + + var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { + demoConfig.demoBlockchains + } else { + listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } + + if (card.isTestCard) { + blockchains = blockchains.mapNotNull { it.getTestnetVersion() } + } + + return blockchains.mapNotNull { + cryptoCurrencyFactory.createCoin( + blockchain = it, + extraDerivationPath = null, + derivationStyleProvider = cardDerivationStyleProvider, + ) + } + } + + fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { + val cardDerivationStyleProvider = scanResponse.derivationStyleProvider + val resolver = scanResponse.cardTypesResolver + val blockchain = resolver.getBlockchain() + + val coin = cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = cardDerivationStyleProvider, + ) + requireNotNull(coin) { "Coin for the single currency card cannot be null" } + + val primaryToken = resolver.getPrimaryToken()?.let { token -> + cryptoCurrencyFactory.createToken( + sdkToken = token, + blockchain = blockchain, + extraDerivationPath = null, + derivationStyleProvider = cardDerivationStyleProvider, + ) + } + + return primaryToken ?: coin + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt deleted file mode 100644 index 269c65e9f7..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.models.CryptoCurrency - -internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) { - - private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } - - fun createDefaultCoinsForMultiCurrencyCard( - card: CardDTO, - derivationStyleProvider: DerivationStyleProvider, - ): List { - var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { - demoConfig.demoBlockchains - } else { - listOf(Blockchain.Bitcoin, Blockchain.Ethereum) - } - - if (card.isTestCard) { - blockchains = blockchains.mapNotNull { it.getTestnetVersion() } - } - - return blockchains.mapNotNull { cryptoCurrencyFactory.createCoin(it, derivationStyleProvider) } - } - - fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { - val derivationStyleProvider = scanResponse.derivationStyleProvider - val resolver = scanResponse.cardTypesResolver - val blockchain = resolver.getBlockchain() - - val coin = requireNotNull(cryptoCurrencyFactory.createCoin(blockchain, derivationStyleProvider)) { - "Coin for the single currency card cannot be null" - } - val primaryToken = resolver.getPrimaryToken()?.let { token -> - cryptoCurrencyFactory.createToken(token, blockchain, derivationStyleProvider) - } - - return primaryToken ?: coin - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt index e6fff3176f..e12c65ff37 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CryptoCurrencyFactory.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.DerivationStyleProvider +import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.tokens.models.CryptoCurrency import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken @@ -12,6 +13,7 @@ class CryptoCurrencyFactory { fun createToken( sdkToken: SdkToken, blockchain: Blockchain, + extraDerivationPath: String?, derivationStyleProvider: DerivationStyleProvider, ): CryptoCurrency.Token? { if (blockchain == Blockchain.Unknown) { @@ -19,35 +21,40 @@ class CryptoCurrencyFactory { return null } - val id = getTokenId(blockchain, sdkToken) + val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null + val id = getTokenId(network, sdkToken) return CryptoCurrency.Token( id = id, - network = getNetwork(blockchain) ?: return null, + network = network, name = sdkToken.name, symbol = sdkToken.symbol, iconUrl = getTokenIconUrl(blockchain, sdkToken), decimals = sdkToken.decimals, - isCustom = isCustomToken(id), + isCustom = isCustomToken(id, network), contractAddress = sdkToken.contractAddress, - derivationPath = getDerivationPath(blockchain, derivationStyleProvider), ) } - fun createCoin(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): CryptoCurrency.Coin? { + fun createCoin( + blockchain: Blockchain, + extraDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Coin? { if (blockchain == Blockchain.Unknown) { Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } + val network = getNetwork(blockchain, extraDerivationPath, derivationStyleProvider) ?: return null return CryptoCurrency.Coin( - id = getCoinId(blockchain), - network = getNetwork(blockchain) ?: return null, + id = getCoinId(network, blockchain.toCoinId()), + network = network, name = blockchain.fullName, symbol = blockchain.currency, iconUrl = getCoinIconUrl(blockchain), decimals = blockchain.decimals(), - derivationPath = getDerivationPath(blockchain, derivationStyleProvider), + isCustom = isCustomCoin(network), ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt deleted file mode 100644 index 6c57906a38..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkConverter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.data.tokens.utils - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.tokens.models.Network -import com.tangem.utils.converter.Converter - -internal class NetworkConverter : Converter { - - override fun convert(value: Network.ID): Network? { - val blockchain = Blockchain.fromId(value.value) - - return getNetwork(blockchain) - } - - override fun convertList(input: Collection): List { - return input.mapNotNull(::convert) - } - - override fun convertSet(input: Collection): Set { - return input.mapNotNullTo(hashSetOf(), ::convert) - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt index 3a536a46ed..85c223f1cc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt @@ -1,10 +1,19 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.tokens.models.Network import timber.log.Timber -internal fun getNetwork(blockchain: Blockchain): Network? { +internal fun getBlockchain(networkId: Network.ID): Blockchain { + return Blockchain.fromId(networkId.value) +} + +internal fun getNetwork( + blockchain: Blockchain, + extraDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, +): Network? { if (blockchain == Blockchain.Unknown) { Timber.e("Unable to convert Unknown blockchain to the domain network model") return null @@ -14,10 +23,33 @@ internal fun getNetwork(blockchain: Blockchain): Network? { id = Network.ID(blockchain.id), name = blockchain.fullName, isTestnet = blockchain.isTestnet(), + derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider), standardType = getNetworkStandardType(blockchain), ) } +private fun getNetworkDerivationPath( + blockchain: Blockchain, + extraDerivationPath: String?, + cardDerivationStyleProvider: DerivationStyleProvider, +): Network.DerivationPath { + val defaultDerivationPath = getDefaultDerivationPath(blockchain, cardDerivationStyleProvider) + + return if (extraDerivationPath.isNullOrBlank()) { + if (defaultDerivationPath.isNullOrBlank()) { + Network.DerivationPath.None + } else { + Network.DerivationPath.Card(defaultDerivationPath) + } + } else { + if (extraDerivationPath == defaultDerivationPath) { + Network.DerivationPath.Card(defaultDerivationPath) + } else { + Network.DerivationPath.Custom(extraDerivationPath) + } + } +} + private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> Network.StandardType.ERC20 @@ -26,4 +58,11 @@ private fun getNetworkStandardType(blockchain: Blockchain): Network.StandardType Blockchain.Tron, Blockchain.TronTestnet -> Network.StandardType.TRC20 else -> Network.StandardType.Unspecified(blockchain.name) } +} + +private fun getDefaultDerivationPath( + blockchain: Blockchain, + derivationStyleProvider: DerivationStyleProvider, +): String? { + return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 0127ed7330..76f28d82a3 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -2,9 +2,9 @@ package com.tangem.data.tokens.utils import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.PendingTransaction import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult @@ -14,12 +14,12 @@ import java.math.BigDecimal internal class NetworkStatusFactory { fun createNetworkStatus( - networkId: Network.ID, + network: Network, result: UpdateWalletManagerResult, currencies: Set, ): NetworkStatus { return NetworkStatus( - networkId = networkId, + network = network, value = when (result) { is UpdateWalletManagerResult.MissedDerivation -> NetworkStatus.MissedDerivation is UpdateWalletManagerResult.Unreachable -> NetworkStatus.Unreachable @@ -31,7 +31,6 @@ internal class NetworkStatusFactory { address = getNetworkAddress(result.defaultAddress, result.addresses), amounts = formatAmounts(result.currenciesAmounts, currencies), pendingTransactions = formatTransactions( - networksAddresses = result.addresses, transactions = result.currentTransactions, currencies = currencies, ), @@ -67,10 +66,9 @@ internal class NetworkStatusFactory { } private fun formatTransactions( - networksAddresses: Set, transactions: Set, currencies: Set, - ): Map> { + ): Map> { if (transactions.isEmpty()) return emptyMap() return currencies @@ -87,48 +85,13 @@ internal class NetworkStatusFactory { } } - currency.id to createCurrentTransactions(networksAddresses, currencyTransactions) + currency.id to createCurrentTransactions(currencyTransactions) } .toMap() } - private fun createCurrentTransactions( - networksAddresses: Set, - transactions: Set, - ): Set { - return transactions.mapNotNullTo(hashSetOf()) { createCurrentTransaction(networksAddresses, it) } - } - - private fun createCurrentTransaction( - networksAddresses: Set, - transaction: CryptoCurrencyTransaction, - ): PendingTransaction? { - val direction = when { - transaction.toAddress in networksAddresses -> PendingTransaction.Direction.Incoming( - fromAddress = transaction.fromAddress, - ) - transaction.fromAddress in networksAddresses -> PendingTransaction.Direction.Outgoing( - toAddress = transaction.toAddress, - ) - else -> { - Timber.e( - """ - Unable to find transaction direction - |- To address: ${transaction.toAddress} - |- From address: ${transaction.fromAddress} - |- Network addresses: $networksAddresses - """.trimIndent(), - ) - - return null - } - } - - return PendingTransaction( - amount = transaction.amount, - direction = direction, - sentAt = transaction.sentAt, - ) + private fun createCurrentTransactions(transactions: Set): Set { + return transactions.mapTo(hashSetOf()) { it.txHistoryItem } } private fun getNetworkAddress(defaultAddress: String, availableAddresses: Set): NetworkAddress { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt similarity index 52% rename from data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt rename to data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt index f3e7832865..b46fac6953 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCryptoCurrenciesFactory.kt @@ -3,47 +3,61 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.toCoinId +import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.models.CryptoCurrency import timber.log.Timber import com.tangem.blockchain.common.Token as SdkToken -internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { +internal class ResponseCryptoCurrenciesFactory(private val demoConfig: DemoConfig) { - fun createCurrency(currencyId: CryptoCurrency.ID, response: UserTokensResponse, card: CardDTO): CryptoCurrency { + fun createCurrency( + currencyId: CryptoCurrency.ID, + response: UserTokensResponse, + scanResponse: ScanResponse, + ): CryptoCurrency { val responseTokenId = currencyId.rawCurrencyId val token = requireNotNull(response.tokens.firstOrNull { it.id == responseTokenId }) { "Unable find a token with provided ID: $responseTokenId" } - return requireNotNull(createCurrency(token, card)) { + return requireNotNull(createCurrency(token, scanResponse)) { "Unable to create a currency with provided ID: $currencyId" } } - fun createCurrencies(response: UserTokensResponse, card: CardDTO): List { - return response.tokens.mapNotNull { createCurrency(it, card) } + fun createCurrencies(response: UserTokensResponse, scanResponse: ScanResponse): List { + return response.tokens + .asSequence() + .mapNotNull { createCurrency(it, scanResponse) } + .distinctBy { it.id } + .toList() } - private fun createCurrency(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? { + fun createCurrency(responseToken: UserTokensResponse.Token, scanResponse: ScanResponse): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") return null } + val cardDerivationStyleProvider = scanResponse.derivationStyleProvider + val card = scanResponse.card + if (demoConfig.isDemoCardId(card.cardId)) { blockchain = blockchain.getTestnetVersion() ?: blockchain } val sdkToken = createSdkToken(responseToken) return if (sdkToken == null) { - createCoin(blockchain, responseToken) + createCoin(blockchain, responseToken, cardDerivationStyleProvider) } else { - createToken(blockchain, sdkToken, responseToken.derivationPath) + createToken(blockchain, sdkToken, responseToken.derivationPath, cardDerivationStyleProvider) } } @@ -59,31 +73,43 @@ internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { } } - private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin? { + private fun createCoin( + blockchain: Blockchain, + responseToken: UserTokensResponse.Token, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Coin? { + val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null + return CryptoCurrency.Coin( - id = getCoinId(blockchain), - network = getNetwork(blockchain) ?: return null, + id = getCoinId(network, blockchain.toCoinId()), + network = network, name = responseToken.name, symbol = responseToken.symbol, decimals = responseToken.decimals, - derivationPath = responseToken.derivationPath, iconUrl = getCoinIconUrl(blockchain), + isCustom = isCustomCoin(network), ) } - private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token? { - val id = getTokenId(blockchain, sdkToken) + private fun createToken( + blockchain: Blockchain, + sdkToken: Token, + responseDerivationPath: String?, + derivationStyleProvider: DerivationStyleProvider, + ): CryptoCurrency.Token? { + val network = getNetwork(blockchain, responseDerivationPath, derivationStyleProvider) + ?: return null + val id = getTokenId(network, sdkToken) return CryptoCurrency.Token( id = id, - network = getNetwork(blockchain) ?: return null, + network = network, name = sdkToken.name, symbol = sdkToken.symbol, decimals = sdkToken.decimals, - derivationPath = derivationPath, iconUrl = getTokenIconUrl(blockchain, sdkToken), contractAddress = sdkToken.contractAddress, - isCustom = isCustomToken(id), + isCustom = isCustomToken(id, network), ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt index a95d6efa08..5f5e666c48 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -2,14 +2,13 @@ package com.tangem.data.tokens.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil -import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.tokens.models.CryptoCurrency.ID import com.tangem.domain.tokens.models.Network import com.tangem.blockchain.common.Token as SdkToken +import com.tangem.domain.tokens.models.CryptoCurrency.ID.Body as CurrencyIdBody import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.COIN_PREFIX as COIN_ID_PREFIX -import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.CUSTOM_TOKEN_PREFIX as CUSTOM_TOKEN_ID_PREFIX import com.tangem.domain.tokens.models.CryptoCurrency.ID.Prefix.TOKEN_PREFIX as TOKEN_ID_PREFIX import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.ContractAddress as CustomCurrencyIdSuffix import com.tangem.domain.tokens.models.CryptoCurrency.ID.Suffix.RawID as CurrencyIdSuffix @@ -18,24 +17,27 @@ private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws private const val TOKEN_ICON_SIZE = "large" private const val TOKEN_ICON_EXT = "png" -internal fun isCustomToken(tokenId: ID): Boolean { - return tokenId.rawCurrencyId == null +internal fun isCustomToken(tokenId: ID, network: Network): Boolean { + return network.derivationPath is Network.DerivationPath.Custom || tokenId.rawCurrencyId == null } -internal fun getDerivationPath(blockchain: Blockchain, derivationStyleProvider: DerivationStyleProvider): String? { - return blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath +internal fun isCustomCoin(network: Network): Boolean { + return network.derivationPath is Network.DerivationPath.Custom } -internal fun getBlockchain(networkId: Network.ID): Blockchain { - return Blockchain.fromId(networkId.value) +internal fun getCoinId(network: Network, coinId: String): ID { + return ID(COIN_ID_PREFIX, getCurrencyIdBody(network), CurrencyIdSuffix(rawId = coinId)) } -internal fun getCoinId(blockchain: Blockchain): ID { - return getTokenOrCoinId(blockchain, token = null) -} +internal fun getTokenId(network: Network, sdkToken: SdkToken): ID { + val sdkTokenId = sdkToken.id + val suffix = if (sdkTokenId == null) { + CustomCurrencyIdSuffix(contractAddress = sdkToken.contractAddress) + } else { + CurrencyIdSuffix(rawId = sdkTokenId) + } -internal fun getTokenId(blockchain: Blockchain, token: SdkToken): ID { - return getTokenOrCoinId(blockchain, token) + return ID(TOKEN_ID_PREFIX, getCurrencyIdBody(network), suffix) } internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { @@ -58,15 +60,16 @@ internal fun getCoinIconUrl(blockchain: Blockchain): String? { return coinId?.let(::getTokenIconUrlFromDefaultHost) } -private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): ID { - val sdkTokenId = token?.id - val (prefix, suffix) = when { - token == null -> COIN_ID_PREFIX to CurrencyIdSuffix(rawId = blockchain.toCoinId()) - sdkTokenId == null -> CUSTOM_TOKEN_ID_PREFIX to CustomCurrencyIdSuffix(contractAddress = token.contractAddress) - else -> TOKEN_ID_PREFIX to CurrencyIdSuffix(rawId = sdkTokenId) +private fun getCurrencyIdBody(network: Network): CurrencyIdBody { + return when (val path = network.derivationPath) { + is Network.DerivationPath.Custom -> CurrencyIdBody.NetworkIdWithDerivationPath( + rawId = network.id.value, + derivationPath = path.value, + ) + is Network.DerivationPath.Card, + is Network.DerivationPath.None, + -> CurrencyIdBody.NetworkId(network.id.value) } - - return ID(prefix, Network.ID(blockchain.id), suffix) } private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt index c6cbc7f858..c8cb48b178 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -32,7 +32,7 @@ internal class UserTokensResponseFactory { return UserTokensResponse.Token( id = currency.id.rawCurrencyId, networkId = blockchain.toNetworkId(), - derivationPath = currency.derivationPath, + derivationPath = currency.network.derivationPath.value, name = currency.name, symbol = currency.symbol, decimals = currency.decimals, diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index a042400894..7aa4add858 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -19,12 +19,11 @@ class DefaultTxHistoryRepository( private val userWalletsStore: UserWalletsStore, ) : TxHistoryRepository { - override suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int { + override suspend fun getTxHistoryItemsCount(network: Network): Int { val userWallet = getUserWallet() val state = walletManagersFacade.getTxHistoryState( userWalletId = userWallet.walletId, - networkId = networkId, - rawDerivationPath = derivationPath, + network = network, ) return when (state) { is TxHistoryState.Failed.FetchError -> throw TxHistoryStateError.DataError(state.exception) @@ -34,11 +33,7 @@ class DefaultTxHistoryRepository( } } - override fun getTxHistoryItems( - networkId: Network.ID, - derivationPath: String?, - pageSize: Int, - ): Flow> { + override fun getTxHistoryItems(network: Network, pageSize: Int): Flow> { val userWallet = getUserWallet() return Pager( config = PagingConfig( @@ -49,8 +44,7 @@ class DefaultTxHistoryRepository( loadPage = { page: Int, pageSize: Int -> walletManagersFacade.getTxHistoryItems( userWalletId = userWallet.walletId, - networkId = networkId, - rawDerivationPath = derivationPath, + network = network, page = page, pageSize = pageSize, ) diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetAvailableCurrenciesUseCase.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetAvailableCurrenciesUseCase.kt new file mode 100644 index 0000000000..90670a27da --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetAvailableCurrenciesUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.appcurrency + +import arrow.core.Either +import arrow.core.NonEmptyList +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.appcurrency.error.AvailableCurrenciesError +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository + +// TODO: Add tests +class GetAvailableCurrenciesUseCase( + private val appCurrencyRepository: AppCurrencyRepository, +) { + + suspend operator fun invoke(): Either> = either { + val currencies = catch({ appCurrencyRepository.getAvailableAppCurrencies() }) { + raise(AvailableCurrenciesError.DataError(it)) + } + + ensureNotNull(currencies.toNonEmptyListOrNull()) { + AvailableCurrenciesError.CurrenciesIsEmpty + } + } +} \ No newline at end of file diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/SelectAppCurrencyUseCase.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/SelectAppCurrencyUseCase.kt new file mode 100644 index 0000000000..25d7c9c739 --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/SelectAppCurrencyUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.appcurrency + +import arrow.core.Either +import com.tangem.domain.appcurrency.repository.AppCurrencyRepository + +// TODO: Add tests +class SelectAppCurrencyUseCase( + private val appCurrencyRepository: AppCurrencyRepository, +) { + + suspend operator fun invoke(currencyCode: String): Either { + return Either.catch { appCurrencyRepository.changeAppCurrency(currencyCode) } + } +} \ No newline at end of file diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/AvailableCurrenciesError.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/AvailableCurrenciesError.kt new file mode 100644 index 0000000000..a80e0bc920 --- /dev/null +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/error/AvailableCurrenciesError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.appcurrency.error + +sealed class AvailableCurrenciesError { + + object CurrenciesIsEmpty : AvailableCurrenciesError() + + data class DataError(val cause: Throwable) : AvailableCurrenciesError() +} \ No newline at end of file diff --git a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt index f6ff6269f7..dd24287e13 100644 --- a/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt +++ b/domain/app-theme/models/src/main/kotlin/com/tangem/domain/apptheme/model/AppThemeMode.kt @@ -25,6 +25,11 @@ enum class AppThemeMode { /** * The default [AppThemeMode]. */ - val DEFAULT: AppThemeMode = FORCE_LIGHT + val DEFAULT: AppThemeMode = FOLLOW_SYSTEM + + /** + * List of available [AppThemeMode]s. + * */ + val available: List = values().toList() } } \ No newline at end of file diff --git a/domain/balance-hiding/.gitignore b/domain/balance-hiding/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/balance-hiding/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/balance-hiding/build.gradle.kts b/domain/balance-hiding/build.gradle.kts new file mode 100644 index 0000000000..d9554a9615 --- /dev/null +++ b/domain/balance-hiding/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + implementation(deps.kotlin.coroutines) + implementation(projects.domain.settings) + implementation(projects.domain.balanceHiding.models) +} \ No newline at end of file diff --git a/domain/balance-hiding/models/.gitignore b/domain/balance-hiding/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/balance-hiding/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/balance-hiding/models/build.gradle.kts b/domain/balance-hiding/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/balance-hiding/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/balance-hiding/models/src/main/kotlin/com/tangem/domain/balancehiding/BalanceHidingSettings.kt b/domain/balance-hiding/models/src/main/kotlin/com/tangem/domain/balancehiding/BalanceHidingSettings.kt new file mode 100644 index 0000000000..4aac92cad2 --- /dev/null +++ b/domain/balance-hiding/models/src/main/kotlin/com/tangem/domain/balancehiding/BalanceHidingSettings.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.balancehiding + +data class BalanceHidingSettings( + val isHidingEnabledInSettings: Boolean, + val isBalanceHidden: Boolean, +) \ No newline at end of file diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/DeviceFlipDetector.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/DeviceFlipDetector.kt new file mode 100644 index 0000000000..588e4a06e3 --- /dev/null +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/DeviceFlipDetector.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.balancehiding + +import kotlinx.coroutines.flow.Flow + +interface DeviceFlipDetector { + + fun getDeviceFlipFlow(): Flow +} \ No newline at end of file diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/IsBalanceHiddenUseCase.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/IsBalanceHiddenUseCase.kt new file mode 100644 index 0000000000..9d22b8d762 --- /dev/null +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/IsBalanceHiddenUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.balancehiding + +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class IsBalanceHiddenUseCase( + private val balanceHidingRepository: BalanceHidingRepository, +) { + + operator fun invoke(): Flow { + return balanceHidingRepository.getBalanceHidingSettingsFlow().map { + it.isBalanceHidden + } + } +} \ No newline at end of file diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/ListenToFlipsUseCase.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/ListenToFlipsUseCase.kt new file mode 100644 index 0000000000..f4c4eb1562 --- /dev/null +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/ListenToFlipsUseCase.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.balancehiding + +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.onEach + +class ListenToFlipsUseCase( + private val flipDetector: DeviceFlipDetector, + private val balanceHidingRepository: BalanceHidingRepository, +) { + + suspend operator fun invoke(): Flow { + return if (balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings) { + flipDetector.getDeviceFlipFlow().onEach { + val balanceHidingSettings = balanceHidingRepository.getBalanceHidingSettings() + + balanceHidingRepository.storeBalanceHidingSettings( + balanceHidingSettings.copy( + isBalanceHidden = !balanceHidingSettings.isBalanceHidden, + ), + ) + } + } else { + emptyFlow() + } + } +} \ No newline at end of file diff --git a/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/repositories/BalanceHidingRepository.kt b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/repositories/BalanceHidingRepository.kt new file mode 100644 index 0000000000..84161a07d9 --- /dev/null +++ b/domain/balance-hiding/src/main/kotlin/com/tangem/domain/balancehiding/repositories/BalanceHidingRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.balancehiding.repositories + +import com.tangem.domain.balancehiding.BalanceHidingSettings +import kotlinx.coroutines.flow.Flow + +interface BalanceHidingRepository { + + fun getBalanceHidingSettingsFlow(): Flow + + suspend fun storeBalanceHidingSettings(isBalanceHidden: BalanceHidingSettings) + + suspend fun getBalanceHidingSettings(): BalanceHidingSettings +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt deleted file mode 100644 index c1f72a19de..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.domain - -import com.tangem.common.extensions.VoidCallback -import com.tangem.datasource.api.tangemTech.models.CoinsResponse - -/** -[REDACTED_AUTHOR] - */ -sealed interface DomainDialog { - - data class DialogError(val error: DomainModuleError) : DomainDialog - - data class SelectTokenDialog( - val items: List, - val networkIdConverter: (String) -> String, - val onSelect: (CoinsResponse.Coin.Network) -> Unit, - val onClose: VoidCallback = {}, - ) : DomainDialog -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt deleted file mode 100644 index a8a52c2d4e..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain - -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState -import com.tangem.domain.redux.state.ActionStateLoggerImpl - -/** -[REDACTED_AUTHOR] - */ -object DomainLayer { - internal val actionStateLogger = ActionStateLoggerImpl() - - var onInitComplete: ((DomainModuleError?) -> Unit)? = null - - fun init() { - initActionStateLogger() - - onInitComplete?.invoke(null) - } - - private fun initActionStateLogger() { - val factory = actionStateLogger.actionStateConvertersFactory - - factory.addConverter(AddCustomTokenAction::class.java, AddCustomTokenState.Converter()) - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt index 27d7606e7f..d762e94382 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt @@ -1,6 +1,5 @@ package com.tangem.domain -import com.tangem.common.module.FbConsumeException import com.tangem.common.module.ModuleError import com.tangem.common.module.ModuleErrorCode import com.tangem.common.module.ModuleMessage @@ -36,32 +35,14 @@ sealed class AddCustomTokenError( ) { object FieldIsEmpty : AddCustomTokenError() - object FieldIsNotEmpty : AddCustomTokenError() object InvalidContractAddress : AddCustomTokenError() object NetworkIsNotSelected : AddCustomTokenError() object InvalidDecimalsCount : AddCustomTokenError() object InvalidDerivationPath : AddCustomTokenError() - sealed class Network : AddCustomTokenError() { - object CheckAddressRequestError : Network() - } - sealed class Warning : AddCustomTokenError() { object PotentialScamToken : Warning() object TokenAlreadyAdded : Warning() object UnsupportedSolanaToken : Warning() } - - data class SelectTokeNetworkError(val networkId: String) : - AddCustomTokenError( - message = "Unknown network [$networkId] should not be included in the network selection dialog.", - ), - FbConsumeException - - data class UnAppropriateInitialization( - val of: String, - val info: String? = null, - ) : AddCustomTokenError( - message = "The [$of], must be properly initialized. Info [$info]", - ) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt deleted file mode 100644 index 902244560d..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.domain - -/** -[REDACTED_AUTHOR] - * Provides a temporary copies of the app module classes, data structures, etc. - */ -// TODO: refactoring: : after refactoring they should be unwrapped and moved -// to appropriate parts of module -@Deprecated("After refactoring they should be unwrapped and moved to appropriate parts of module") -sealed interface DomainWrapped { - - // Mirror reflection ot the com.tangem.tap.features.wallet.redux.Currency - sealed interface Currency { - val blockchain: com.tangem.blockchain.common.Blockchain - val currencySymbol: String - val derivationPath: String? - - data class Token( - val token: com.tangem.blockchain.common.Token, - override val blockchain: com.tangem.blockchain.common.Blockchain, - override val derivationPath: String?, - ) : Currency { - override val currencySymbol = token.symbol - } - - data class Blockchain( - override val blockchain: com.tangem.blockchain.common.Blockchain, - override val derivationPath: String?, - ) : Currency { - override val currencySymbol: String = blockchain.currency - } - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index 10d2f401bd..c8dd146a5e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -17,7 +17,7 @@ interface CardTypesResolver { fun isStart2Coin(): Boolean - fun isDev(): Boolean + fun isDevKit(): Boolean fun isMultiwalletAllowed(): Boolean @@ -25,8 +25,6 @@ interface CardTypesResolver { fun getPrimaryToken(): Token? - fun getBackupCardsCount(): Int - fun isReleaseFirmwareType(): Boolean fun getRemainingSignatures(): Int? diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index c3053f87b6..6437bc105d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -37,7 +37,7 @@ internal class TangemCardTypesResolver( override fun isStart2Coin(): Boolean = card.isStart2Coin - override fun isDev(): Boolean = card.isTestCard + override fun isDevKit(): Boolean = card.batchId == DEV_KIT_CARD_BATCH_ID override fun isMultiwalletAllowed(): Boolean { return !isTangemTwins() && !card.isStart2Coin && !isTangemNote() && @@ -71,8 +71,6 @@ internal class TangemCardTypesResolver( ) } - override fun getBackupCardsCount(): Int = card.wallets.size - override fun isReleaseFirmwareType(): Boolean = card.firmwareVersion.type == FirmwareVersion.FirmwareType.Release override fun getRemainingSignatures(): Int? = card.wallets.firstOrNull()?.remainingSignatures @@ -111,4 +109,9 @@ internal class TangemCardTypesResolver( } } } + + private companion object { + + const val DEV_KIT_CARD_BATCH_ID = "CB83" + } } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index c8f7e9fe0d..b8583c123d 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -1,7 +1,6 @@ package com.tangem.domain.common import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.CardDTO @@ -71,6 +70,4 @@ object TapWorkarounds { fun isStart2CoinIssuer(cardIssuer: String?): Boolean { return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER } - - fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt deleted file mode 100644 index b406d02f7c..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.domain.common.form - -import com.tangem.common.json.MoshiJsonConverter - -/** -[REDACTED_AUTHOR] - */ -interface DataConverterVisitor { - fun visit(data: Data?) - fun getConvertedData(): Result -} - -interface FieldDataConverter : DataConverterVisitor - -abstract class BaseFieldDataConverter : FieldDataConverter { - private val collectIds: List - get() = getIdToCollect() - - protected val collectedData: MutableMap = mutableMapOf() - - override fun visit(data: Pair>?) { - val id = data?.first ?: return - - if (collectIds.contains(id)) { - collectedData[id] = data.second.value - } - } - - abstract fun getIdToCollect(): List -} - -class FieldToJsonConverter( - private val fieldsToConvert: List = listOf(), - private val jsonConverter: MoshiJsonConverter, -) : BaseFieldDataConverter() { - - override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ") - - override fun getIdToCollect(): List = fieldsToConvert -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt deleted file mode 100644 index 067feb900f..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.tangem.domain.common.form - -import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService -import com.tangem.blockchain.blockchains.solana.SolanaAddressService -import com.tangem.blockchain.blockchains.tron.TronAddressService -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.address.AddressService -import com.tangem.common.Validator -import com.tangem.common.card.EllipticCurve -import com.tangem.domain.AddCustomTokenError -import timber.log.Timber - -/** -[REDACTED_AUTHOR] - */ -interface CustomTokenValidator : Validator - -class StringIsEmptyValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? { - return if (data.isNullOrEmpty()) null else AddCustomTokenError.FieldIsNotEmpty - } -} - -class StringIsNotEmptyValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? { - return if (data.isNullOrEmpty()) AddCustomTokenError.FieldIsEmpty else null - } -} - -class TokenContractAddressValidator : CustomTokenValidator { - - private var blockchain: Blockchain = Blockchain.Unknown - - private val successAddressValidator = object : AddressService() { - override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String { - throw UnsupportedOperationException() - } - - override fun validate(address: String): Boolean = true - } - - fun nextValidationFor(blockchain: Blockchain) { - this.blockchain = blockchain - } - - override fun validate(data: String?): AddCustomTokenError? { - return when { - data.isNullOrEmpty() -> AddCustomTokenError.FieldIsEmpty - getAddressService().validate(data) -> null - else -> AddCustomTokenError.InvalidContractAddress - } - } - - private fun getAddressService(): AddressService { - return when (blockchain) { - Blockchain.Unknown -> successAddressValidator - Blockchain.Binance, Blockchain.BinanceTestnet -> successAddressValidator - Blockchain.Solana, Blockchain.SolanaTestnet -> SolanaAddressService() - Blockchain.Tron, Blockchain.TronTestnet -> TronAddressService() - else -> { - if (blockchain.isEvm()) { - EthereumAddressService() - } else { - Timber.e("Throw for blockchain: ${blockchain.fullName}") - throw UnsupportedOperationException() - } - } - } - } -} - -class TokenNetworkValidator : CustomTokenValidator { - override fun validate(data: Blockchain?): AddCustomTokenError? { - return when (data) { - null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected - else -> null - } - } -} - -class TokenNameValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) -} - -class TokenSymbolValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) -} - -class TokenDecimalsValidator : CustomTokenValidator { - override fun validate(data: String?): AddCustomTokenError? { - val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty - - return if (decimal > INVALID_DECIMALS_COUNT) AddCustomTokenError.InvalidDecimalsCount else null - } - - private companion object { - const val INVALID_DECIMALS_COUNT = 30 - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt deleted file mode 100644 index cca8b72a5f..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.domain.common.form - -/** -[REDACTED_AUTHOR] - */ -class Form( - fieldList: List>, -) { - private val _fieldList: MutableList> = fieldList.toMutableList() - - val fieldList: List> - get() = _fieldList.toList() - - fun getField(id: FieldId): DataField<*>? = fieldList.firstOrNull { it.id == id } - - fun getData(id: FieldId): Pair? = getField(id)?.getData() - - fun setField(field: DataField<*>) { - val oldField = getField(field.id) ?: return - val oldIndexOfField = _fieldList.indexOf(oldField) - if (oldIndexOfField == -1) return - - _fieldList.removeAt(oldIndexOfField) - _fieldList.add(oldIndexOfField, field) - } - - // convert this form data whatever you want - fun visitDataConverter(converter: FieldDataConverter<*>) { - fieldList.forEach { it.visitDataConverter(converter) } - } -} - -interface FieldId - -interface Field { - val id: FieldId - var data: Data - - data class Data( - val value: Data, - val isUserInput: Boolean, - ) -} - -typealias FieldData = Pair> - -interface DataField : Field { - fun getData(): Pair> - fun visitDataConverter(dataConverter: FieldDataConverter<*>) -} - -abstract class BaseDataField( - override val id: FieldId, - override var data: Field.Data, -) : DataField { - - override fun getData(): Pair> = id to data - - override fun visitDataConverter(dataConverter: FieldDataConverter<*>) { - dataConverter.visit(getData()) - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt new file mode 100644 index 0000000000..b6a2e29f06 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.exchange + +import com.tangem.domain.tokens.models.CryptoCurrency + +/** + * Manager that holds info about available actions as Sell and Buy + */ +interface RampStateManager { + fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean + fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt deleted file mode 100644 index 1a5fc65e8c..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.domain.features.addCustomToken - -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -/** -[REDACTED_AUTHOR] - */ -class AddCustomTokenService( - private val tangemTechApi: TangemTechApi, - private val dispatchers: CoroutineDispatcherProvider, - private val supportedTokenNetworkIds: List, -) { - - suspend fun findToken(contractAddress: String, networkId: String?): List { - return withContext(dispatchers.io) { - runCatching { - tangemTechApi.getCoins( - contractAddress = contractAddress, - networkIds = selectNetworksForSearch(networkId), - ) - } - .fold( - onSuccess = { response -> - var coinsList = mutableListOf() - response.coins.forEach { coin -> - val networksWithTheSameAddress = coin.networks - .filter { it.contractAddress != null || it.decimalCount != null } - .filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true } - .filter { supportedTokenNetworkIds.contains(it.networkId) } - if (networksWithTheSameAddress.isNotEmpty()) { - val newToken = coin.copy(networks = networksWithTheSameAddress) - coinsList.add(newToken) - } - } - if (coinsList.size > 1) { - // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679 - coinsList = mutableListOf(coinsList[0]) - } - coinsList - }, - onFailure = { emptyList() }, - ) - } - } - - private fun selectNetworksForSearch(networkId: String?): String { - return networkId ?: supportedTokenNetworkIds.joinToString(",") - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt index 78ff46be86..15debdd88f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt @@ -2,11 +2,7 @@ package com.tangem.domain.features.addCustomToken import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.form.BaseFieldDataConverter -import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState /** [REDACTED_AUTHOR] @@ -16,65 +12,16 @@ sealed class CustomCurrency( val derivationPath: DerivationPath?, ) { + @Deprecated("It will be removed in next releases") class CustomBlockchain( network: Blockchain, derivationPath: DerivationPath?, - ) : CustomCurrency(network, derivationPath) { - - class Converter( - private val derivationStyle: DerivationStyle?, - ) : BaseFieldDataConverter() { - override fun getConvertedData(): CustomBlockchain { - val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain - val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain - val derivationPath = AddCustomTokenState.getDerivationPath( - mainNetwork, - derivationPathNetwork, - derivationStyle, - ) - return CustomBlockchain(mainNetwork, derivationPath) - } - - override fun getIdToCollect(): List = - listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath) - } - } + ) : CustomCurrency(network, derivationPath) + @Deprecated("It will be removed in next releases") class CustomToken( val token: Token, network: Blockchain, derivationPath: DerivationPath?, - ) : CustomCurrency(network, derivationPath) { - - class Converter( - private val tokenId: String?, - private val derivationStyle: DerivationStyle?, - ) : BaseFieldDataConverter() { - - override fun getConvertedData(): CustomToken { - val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain - val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain - val derivationPath = AddCustomTokenState.getDerivationPath( - mainNetwork, - derivationPathNetwork, - derivationStyle, - ) - - val token = Token( - name = collectedData[CustomTokenFieldId.Name] as String, - symbol = collectedData[CustomTokenFieldId.Symbol] as String, - contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String, - decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(), - id = tokenId, - ) - return CustomToken( - token, - collectedData[CustomTokenFieldId.Network] as Blockchain, - derivationPath, - ) - } - - override fun getIdToCollect(): List = CustomTokenFieldId.values().toList() - } - } + ) : CustomCurrency(network, derivationPath) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt deleted file mode 100644 index e9ec16b2a1..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.domain.features.addCustomToken - -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.form.BaseDataField -import com.tangem.domain.common.form.Field -import com.tangem.domain.common.form.FieldId - -/** -[REDACTED_AUTHOR] - */ -enum class CustomTokenFieldId : FieldId { - ContractAddress, - Network, - Name, - Symbol, - Decimals, - DerivationPath, -} - -data class TokenField( - override val id: FieldId, -) : BaseDataField(id, Field.Data("", false)) - -data class TokenBlockchainField( - override val id: FieldId, - val itemList: List, -) : BaseDataField(id, Field.Data(Blockchain.Unknown, false)) - -data class TokenDerivationPathField( - override val id: FieldId, - val itemList: List, -) : BaseDataField(id, Field.Data(Blockchain.Unknown, false)) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt deleted file mode 100644 index c178c4a610..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.domain.features.addCustomToken.redux - -import com.tangem.blockchain.common.Blockchain -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.DomainWrapped -import com.tangem.domain.common.form.Field -import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -sealed class AddCustomTokenAction : Action { - sealed class Init : AddCustomTokenAction() { - data class SetAddedCurrencies(val addedCurrencies: List) : AddCustomTokenAction() - data class SetOnAddTokenCallback(val callback: (CustomCurrency) -> Unit) : AddCustomTokenAction() - } - - object OnCreate : AddCustomTokenAction() - - object OnDestroy : AddCustomTokenAction() - - // from user, ui - data class OnTokenContractAddressChanged(val contractAddress: Field.Data) : AddCustomTokenAction() - data class OnTokenNetworkChanged(val blockchainNetwork: Field.Data) : AddCustomTokenAction() - data class OnTokenNameChanged(val tokenName: Field.Data) : AddCustomTokenAction() - data class OnTokenSymbolChanged(val tokenSymbol: Field.Data) : AddCustomTokenAction() - data class OnTokenDerivationPathChanged( - val blockchainDerivationPath: Field.Data, - ) : AddCustomTokenAction() - - data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data) : AddCustomTokenAction() - object OnAddCustomTokenClicked : AddCustomTokenAction() - - data class SetFoundTokenInfo(val foundToken: CoinsResponse.Coin?) : AddCustomTokenAction() - - // form fields - data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction() - - sealed class FieldError : AddCustomTokenAction() { - data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : FieldError() - data class Remove(val id: CustomTokenFieldId) : FieldError() - } - - // warnings - sealed class Warning : AddCustomTokenAction() { - data class Add(val warnings: Set) : Warning() - data class Remove(val warnings: Set) : Warning() - data class Replace( - val remove: Set, - val add: Set, - ) : Warning() - } - - // To change the screenState - sealed class Screen : AddCustomTokenAction() { - data class UpdateTokenFields(val pairs: List>) : Screen() - data class UpdateAddButton(val addButton: ViewStates.AddButton) : Screen() - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt deleted file mode 100644 index aa12f86b00..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt +++ /dev/null @@ -1,720 +0,0 @@ -package com.tangem.domain.features.addCustomToken.redux - -import android.webkit.ValueCallback -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.guard -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.AddCustomTokenError.Warning.* -import com.tangem.domain.DomainDialog -import com.tangem.domain.DomainWrapped -import com.tangem.domain.common.extensions.canHandleToken -import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.domain.common.form.* -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.features.addCustomToken.* -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.* -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState.Companion.createInitialScreenState -import com.tangem.domain.redux.BaseStoreHub -import com.tangem.domain.redux.DomainState -import com.tangem.domain.redux.ReStoreReducer -import com.tangem.domain.redux.domainStore -import com.tangem.domain.redux.extensions.dispatchOnMain -import com.tangem.domain.redux.global.DomainGlobalAction -import com.tangem.domain.redux.global.DomainGlobalState -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.rekotlin.Action -import timber.log.Timber - -/** -[REDACTED_AUTHOR] - */ -@Suppress("LargeClass") -internal class AddCustomTokenHub : BaseStoreHub("AddCustomTokenHub") { - - private val hubState: AddCustomTokenState - get() = domainStore.state.addCustomTokensState - - override fun getReducer(): ReStoreReducer = AddCustomTokenReducer(globalState) - - override fun getHubState(storeState: DomainState): AddCustomTokenState = hubState - - override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState { - return storeState.copy(addCustomTokensState = newHubState) - } - - @Suppress("ComplexMethod") - override suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback) { - if (action !is AddCustomTokenAction) return - - when (action) { - is OnCreate -> { - hubState.appSavedCurrencies.guard { - return throwUnAppropriateInitialization("addedTokens") - } - } - is OnDestroy -> cancelAll() - is OnTokenContractAddressChanged -> { - validateContractAddressAndNotify(action.contractAddress.value) - } - is OnTokenNetworkChanged -> { - if (!action.blockchainNetwork.isUserInput) return - - validateContractAddressAndNotify(ContractAddress.getFieldValue()) - } - is OnTokenDerivationPathChanged -> { - updateAddButton() - } - is OnTokenNameChanged, is OnTokenSymbolChanged, is OnTokenDecimalsChanged -> { - updateAddButton() - } - is OnAddCustomTokenClicked -> { - val state = hubState - val completeData = when { - state.getCustomTokenType() == CustomTokenType.Token && state.networkIsSelected() -> { - state.gatherUserToken() - } - state.getCustomTokenType() == CustomTokenType.Blockchain && state.networkIsSelected() -> { - state.gatherBlockchain() - } - else -> null - } - - if (completeData == null) { - // normally it can't be, because the AddButton must be blocked - } else { - hubScope.launch(Dispatchers.Main) { - state.onTokenAddCallback?.invoke(completeData) - } - } - } - else -> Unit - } - } - - private suspend fun validateContractAddressAndNotify(contractAddress: String) { - val error = ContractAddress.validateValue(contractAddress) - if (Network.isFilled()) { - when (error) { - null -> { - // valid contract address - ContractAddress.removeError() - findTokenAndUpdateFields(contractAddress) - } - AddCustomTokenError.InvalidContractAddress -> { - ContractAddress.addError(error) - enableDisableTokenDetailFields(hubState.tokensAnyFieldsIsFilled()) - } - AddCustomTokenError.FieldIsEmpty -> { - ContractAddress.removeError() - clearTokenDetailsFields() - disableTokenDetailFields() - } - else -> {} - } - } else { - // is default selection (Blockchain.Unknown) - when (error) { - null -> { - // Blockchain.Unknown has always valid contract address - ContractAddress.removeError() - findTokenAndUpdateFields(contractAddress) - } - else -> { - ContractAddress.removeError() - clearTokenDetailsFields() - disableTokenDetailFields() - } - } - } - updateDerivationPath(Network.getFieldValue()) - updateWarnings() - updateAddButton() - } - - private suspend fun findTokenAndUpdateFields(contractAddress: String) { - val foundTokens = requestInfoAboutToken(contractAddress) - if (foundTokens.isEmpty()) { - // token not found - it's completely custom - dispatchOnMain(SetFoundTokenInfo(null)) - enableTokenDetailFields() - return - } - - // foundToken - contains all info about the token - val foundToken = foundTokens[0] - dispatchOnMain(SetFoundTokenInfo(foundToken)) - when { - foundToken.networks.isEmpty() -> { - Timber.e("Unexpected state -> throw to FB") - } - foundToken.networks.size == 1 -> { - // token with single contract address - val singleTokenContract = foundToken.networks[0] - fillTokenFields(foundToken, singleTokenContract) - disableTokenDetailFields() - } - else -> { - val dialog = DomainDialog.SelectTokenDialog( - items = foundToken.networks, - networkIdConverter = { networkId -> - val blockchain = Blockchain.fromNetworkId(networkId) - if (blockchain == null || blockchain == Blockchain.Unknown) { - throw AddCustomTokenError.SelectTokeNetworkError(networkId) - } - hubState.blockchainToName(blockchain) ?: "" - }, - onSelect = { selectedContract -> - hubScope.launch { - // find how to connect to the upper coroutineContext and dispatch through them - fillTokenFields(foundToken, selectedContract) - disableTokenDetailFields() - } - }, - ) - dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) - } - } - } - - private suspend fun updateDerivationPath(blockchainNetwork: Blockchain) { - val state = hubState - val derivationIsSupportedByNetwork = blockchainNetwork.isEvm() || blockchainNetwork == Blockchain.Unknown - - if (DerivationPath.isFilled() && !derivationIsSupportedByNetwork) { - // reset to default - val derivationField = DerivationPath.getField() - derivationField.data = derivationField.data.copy( - value = Blockchain.Unknown, - isUserInput = false, - ) - state.setField(derivationField) - dispatchOnMain(UpdateForm(hubState)) - } - - if (state.screenState.derivationPath.isEnabled != derivationIsSupportedByNetwork) { - val action = Screen.UpdateTokenFields( - listOf( - DerivationPath to state.screenState.derivationPath.copy( - isEnabled = derivationIsSupportedByNetwork, - ), - ), - ) - dispatchOnMain(action) - } - } - - private suspend fun updateWarnings() { - val state = hubState - val warningsAdd = mutableSetOf() - val warningsRemove = mutableSetOf() - - val tokenIsSupported = tokenIsSupported(Network.getFieldValue()) - val alreadyAdded = isPersistIntoAppSavedTokensList() - when (state.getCustomTokenType()) { - CustomTokenType.Blockchain -> { - warningsRemove.add(UnsupportedSolanaToken) - - if (alreadyAdded) warningsAdd.add(TokenAlreadyAdded) else warningsRemove.add(TokenAlreadyAdded) - - if (state.derivationPathIsSelected()) { - warningsAdd.add(PotentialScamToken) - } else { - warningsRemove.add(PotentialScamToken) - } - } - CustomTokenType.Token -> { - if (tokenIsSupported) { - warningsRemove.add(UnsupportedSolanaToken) - } else { - val validationResult = ContractAddress.validateValue(ContractAddress.getFieldValue()) - if (validationResult == AddCustomTokenError.FieldIsEmpty) { - warningsRemove.add(UnsupportedSolanaToken) - } else { - warningsAdd.add(UnsupportedSolanaToken) - } - } - - if (isPersistIntoAppSavedTokensList()) { - warningsAdd.add(TokenAlreadyAdded) - } else { - warningsRemove.add(TokenAlreadyAdded) - } - - if (state.foundToken == null) { - if (state.tokensAnyFieldsIsFilled()) { - warningsAdd.add(PotentialScamToken) - } else { - warningsRemove.add(PotentialScamToken) - } - } else { - if (state.foundToken.active) { - warningsRemove.add(PotentialScamToken) - } else { - warningsAdd.add(PotentialScamToken) - } - } - } - } - - dispatchOnMain( - Warning.Replace( - remove = warningsRemove, - add = warningsAdd, - ), - ) - } - - private suspend fun updateAddButton() { - if (isPersistIntoAppSavedTokensList()) { - TokenAlreadyAdded.add() - disableAddButton() - return - } else { - TokenAlreadyAdded.remove() - } - - val state = hubState - when { - // token - state.tokensFieldsIsFilled() && state.networkIsSelected() -> { - val error = ContractAddress.validateValue(ContractAddress.getFieldValue()) - val tokenIsSupported = tokenIsSupported(Network.getFieldValue()) - enableDisableAddButton(tokenIsSupported && error == null) - } - // token - state.tokensAnyFieldsIsFilled() -> { - disableAddButton() - } - // blockchain - else -> { - if (state.networkIsSelected()) { - if (isBlockchainPersistIntoAppSavedTokensList()) disableAddButton() else enableAddButton() - } else { - disableAddButton() - } - } - } - } - - private suspend fun requestInfoAboutToken(contractAddress: String): List { - val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager) - dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) - - val field = hubState.getField(Network) - val selectedNetworkId: String? = field.data.value.let { - if (it == Blockchain.Unknown) null else it - }?.toNetworkId() - - // simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress() - // got the result faster than 500ms and the delay would only be the difference between them. - delay(timeMillis = 500) - - val result = tangemTechServiceManager.findToken(contractAddress, selectedNetworkId) - - dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = false)))) - return result - } - - /** - * These are helper functions. - */ - private fun isPersistIntoAppSavedTokensList(): Boolean = when (hubState.getCustomTokenType()) { - CustomTokenType.Blockchain -> isBlockchainPersistIntoAppSavedTokensList() - CustomTokenType.Token -> isTokenPersistIntoAppSavedTokensList() - } - - private fun isTokenPersistIntoAppSavedTokensList(): Boolean { - val savedCurrencies = hubState.appSavedCurrencies ?: return false - - val tokenId = hubState.foundToken?.id - val tokenContractAddress = ContractAddress.getFieldValue() - val tokenNetworkId = Network.getFieldValue().toNetworkId() - val selectedDerivation = DerivationPath.getFieldValue() - - val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation) - savedCurrencies.forEach { wrappedCurrency -> - when (wrappedCurrency) { - is DomainWrapped.Currency.Blockchain -> Unit - is DomainWrapped.Currency.Token -> { - val sameId = tokenId == wrappedCurrency.token.id - val sameAddress = tokenContractAddress == wrappedCurrency.token.contractAddress - val sameBlockchain = Blockchain.fromNetworkId(tokenNetworkId) == wrappedCurrency.blockchain - val sameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath - @Suppress("ComplexCondition") - if (sameId && sameAddress && sameBlockchain && sameDerivationPath) { - return true - } - } - } - } - return false - } - - private fun isBlockchainPersistIntoAppSavedTokensList(): Boolean { - val savedCurrencies = hubState.appSavedCurrencies ?: return false - val selectedNetwork = Network.getFieldValue() - val selectedDerivation = DerivationPath.getFieldValue() - val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation) - - savedCurrencies.forEach { wrappedCurrency -> - when (wrappedCurrency) { - is DomainWrapped.Currency.Blockchain -> { - val isSameBlockchain = selectedNetwork == wrappedCurrency.blockchain - val isSameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath - if (isSameBlockchain && isSameDerivationPath) return true - } - - is DomainWrapped.Currency.Token -> Unit - } - } - return false - } - - private fun getDerivationPathFromSelectedBlockchain( - selectedDerivationBlockchain: Blockchain, - ): com.tangem.crypto.hdWallet.DerivationPath? = AddCustomTokenState.getDerivationPath( - mainNetwork = Network.getFieldValue(), - derivationNetwork = selectedDerivationBlockchain, - derivationStyle = hubState.cardDerivationStyle, - ) - - private suspend fun fillTokenFields(token: CoinsResponse.Coin, coinNetwork: CoinsResponse.Coin.Network) { - val blockchain = Blockchain.fromNetworkId(coinNetwork.networkId) ?: Blockchain.Unknown - Network.setFieldValue(Field.Data(blockchain, false)) - Name.setFieldValue(Field.Data(token.name, false)) - Symbol.setFieldValue(Field.Data(token.symbol, false)) - Decimals.setFieldValue(Field.Data(coinNetwork.decimalCount.toString(), false)) - dispatchOnMain(UpdateForm(hubState)) - } - - private suspend fun clearTokenDetailsFields() { - Name.setFieldValue(Field.Data("", false)) - Symbol.setFieldValue(Field.Data("", false)) - Decimals.setFieldValue(Field.Data("", false)) - dispatchOnMain(UpdateForm(hubState)) - } - - private suspend fun enableTokenDetailFields() { - enableDisableTokenDetailFields(true) - } - - private suspend fun disableTokenDetailFields() { - enableDisableTokenDetailFields(false) - } - - private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean) { - val state = hubState - val action = Screen.UpdateTokenFields( - listOf( - Name to state.screenState.name.copy(isEnabled = isEnabled), - Symbol to state.screenState.symbol.copy(isEnabled = isEnabled), - Decimals to state.screenState.decimals.copy(isEnabled = isEnabled), - ), - ) - dispatchOnMain(action) - } - - private suspend fun enableAddButton() { - enableDisableAddButton(true) - } - - private suspend fun disableAddButton() { - enableDisableAddButton(false) - } - - private suspend fun enableDisableAddButton(isEnabled: Boolean) { - dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(isEnabled))) - } - - private fun tokenIsSupported(blockchain: Blockchain): Boolean = when (blockchain) { - Blockchain.Unknown -> true - else -> { - val scanResponse = globalState.scanResponse - scanResponse?.card?.canHandleToken( - blockchain = blockchain, - cardTypesResolver = scanResponse.cardTypesResolver, - ) ?: false - } - } - - @Throws - private fun throwUnAppropriateInitialization(objName: String) { - throw AddCustomTokenError.UnAppropriateInitialization( - "AddCustomTokenHub", - "$objName must be not NULL", - ) - } - - private suspend fun CustomTokenFieldId.addError(error: AddCustomTokenError) { - dispatchOnMain(FieldError.Add(this, error)) - } - - private suspend fun CustomTokenFieldId.removeError() { - dispatchOnMain(FieldError.Remove(this)) - } - - private inline fun CustomTokenFieldId.getField(): T { - val state = hubState - val value = when (this) { - ContractAddress -> state.getField(this) - Network -> state.getField(this) - Name -> state.getField(this) - Symbol -> state.getField(this) - Decimals -> state.getField(this) - DerivationPath -> state.getField(this) - } - return value as T - } - - private inline fun CustomTokenFieldId.getFieldValue(): T { - val value = when (this) { - ContractAddress -> getField().data.value - Network -> getField().data.value - Name -> getField().data.value - Symbol -> getField().data.value - Decimals -> getField().data.value - DerivationPath -> getField().data.value - } - return value as T - } - - private fun CustomTokenFieldId.setFieldValue(fieldData: Field.Data<*>) { - when (this) { - ContractAddress -> getField().data = fieldData as Field.Data - Network -> getField().data = fieldData as Field.Data - Name -> getField().data = fieldData as Field.Data - Symbol -> getField().data = fieldData as Field.Data - Decimals -> getField().data = fieldData as Field.Data - DerivationPath -> getField().data = fieldData as Field.Data - } - } - - private fun CustomTokenFieldId.validateValue(value: Any): AddCustomTokenError? { - return when (this) { - ContractAddress -> { - val contractAddressValidator: TokenContractAddressValidator = hubState.getValidator(ContractAddress) - contractAddressValidator.nextValidationFor(Network.getFieldValue()) - contractAddressValidator.validate(value as String) - } - Network, DerivationPath -> { - hubState.getValidator(Network).validate(value as Blockchain) - } - Name -> { - hubState.getValidator(Name).validate(value as String) - } - Symbol -> { - hubState.getValidator(Symbol).validate(value as String) - } - Decimals -> { - hubState.getValidator(Decimals).validate(value as String) - } - } - } - - private fun CustomTokenFieldId.isFilled(): Boolean { - return when (this) { - ContractAddress -> getFieldValue().isNotEmpty() - Network -> getFieldValue() != Blockchain.Unknown - Name -> getFieldValue().isNotEmpty() - Symbol -> getFieldValue().isNotEmpty() - Decimals -> getFieldValue().isNotEmpty() - DerivationPath -> getFieldValue() != Blockchain.Unknown - } - } - - private suspend fun AddCustomTokenError.Warning.add() { - dispatchOnMain(Warning.Add(setOf(this))) - } - - private suspend fun AddCustomTokenError.Warning.remove() { - dispatchOnMain(Warning.Remove(setOf(this))) - } -} - -@Suppress("ComplexMethod") -private class AddCustomTokenReducer( - private val globalState: DomainGlobalState, -) : ReStoreReducer { - - @Suppress("LongMethod") - override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState { - return when (action) { - is Init.SetAddedCurrencies -> { - state.copy(appSavedCurrencies = action.addedCurrencies) - } - is Init.SetOnAddTokenCallback -> { - state.copy(onTokenAddCallback = action.callback) - } - is OnCreate -> { - val scanResponse = requireNotNull(globalState.scanResponse) - val card = globalState.scanResponse.card - val supportedTokenNetworkIds = card.supportedBlockchains(scanResponse.cardTypesResolver) - .filter(Blockchain::canHandleTokens) - .map(Blockchain::toNetworkId) - - val tangemTechServiceManager = AddCustomTokenService( - tangemTechApi = globalState.networkServices.tangemTechService.api, - dispatchers = AppCoroutineDispatcherProvider(), - supportedTokenNetworkIds = supportedTokenNetworkIds, - ) - - state.copy( - cardDerivationStyle = globalState.scanResponse.derivationStyleProvider.getDerivationStyle(), - form = Form( - AddCustomTokenState.createFormFields( - cardTypesResolver = globalState.scanResponse.cardTypesResolver, - card = card, - type = CustomTokenType.Blockchain, - ), - ), - tangemTechServiceManager = tangemTechServiceManager, - screenState = createInitialScreenState(card.settings.isHDWalletAllowed), - ) - } - is OnDestroy -> { - val scanResponse = requireNotNull(globalState.scanResponse) - val card = scanResponse.card - state.reset(scanResponse.cardTypesResolver, card) - } - is UpdateForm -> { - updateFormState(action.state) - } - is OnTokenContractAddressChanged -> { - val field: TokenField = state.getField(ContractAddress) - field.data = action.contractAddress - updateFormState(state) - } - is OnTokenNetworkChanged -> { - val field: TokenBlockchainField = state.getField(Network) - field.data = action.blockchainNetwork - updateFormState(state) - } - is OnTokenNameChanged -> { - val field: TokenField = state.getField(Name) - field.data = action.tokenName - updateFormState(state) - } - is OnTokenSymbolChanged -> { - val field: TokenField = state.getField(Symbol) - field.data = action.tokenSymbol - updateFormState(state) - } - is OnTokenDecimalsChanged -> { - val field: TokenField = state.getField(Decimals) - field.data = action.tokenDecimals - updateFormState(state) - } - is OnTokenDerivationPathChanged -> { - val field: TokenDerivationPathField = state.getField(DerivationPath) - field.data = action.blockchainDerivationPath - updateFormState(state) - } - is FieldError.Add -> { - val newMap = state.formErrors.toMutableMap().apply { this[action.id] = action.error } - state.copy(formErrors = newMap) - } - is FieldError.Remove -> { - val newMap = state.formErrors.toMutableMap().apply { remove(action.id) } - state.copy(formErrors = newMap) - } - is SetFoundTokenInfo -> { - state.copy(foundToken = action.foundToken) - } - is Warning.Add -> { - val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) } - state.copy(warnings = newList.toSet()) - } - is Warning.Remove -> { - val newList = state.warnings.toMutableSet().apply { removeAll(action.warnings) } - state.copy(warnings = newList.toSet()) - } - is Warning.Replace -> { - val newList = state.warnings.toMutableSet().apply { - removeAll(action.remove) - addAll(action.add) - } - state.copy(warnings = newList.toSet()) - } - is Screen.UpdateTokenFields -> { - var newScreenState = state.screenState - action.pairs.forEach { - newScreenState = when (it.first) { - ContractAddress -> { - if (state.screenState.contractAddressField == it.second) { - newScreenState - } else { - newScreenState.copy(contractAddressField = it.second) - } - } - Network -> { - if (state.screenState.network == it.second) { - newScreenState - } else { - newScreenState.copy(network = it.second) - } - } - Name -> { - if (state.screenState.name == it.second) { - newScreenState - } else { - newScreenState.copy(name = it.second) - } - } - Symbol -> { - if (state.screenState.symbol == it.second) { - newScreenState - } else { - newScreenState.copy(symbol = it.second) - } - } - Decimals -> { - if (state.screenState.decimals == it.second) { - newScreenState - } else { - newScreenState.copy(decimals = it.second) - } - } - DerivationPath -> { - if (state.screenState.derivationPath == it.second) { - newScreenState - } else { - newScreenState.copy(derivationPath = it.second) - } - } - else -> newScreenState - } - } - if (state.screenState == newScreenState) { - state - } else { - state.copy(screenState = newScreenState) - } - } - is Screen.UpdateAddButton -> { - val newScreenState = if (state.screenState.addButton == action.addButton) { - state.screenState - } else { - state.screenState.copy(addButton = action.addButton) - } - if (newScreenState == state.screenState) { - state - } else { - state.copy(screenState = newScreenState) - } - } - else -> state - } - } - - private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState { - return state.copy(form = Form(state.form.fieldList)) - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt deleted file mode 100644 index 6d1b3fd2b0..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ /dev/null @@ -1,311 +0,0 @@ -package com.tangem.domain.features.addCustomToken.redux - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.common.json.MoshiJsonConverter -import com.tangem.datasource.api.tangemTech.models.CoinsResponse -import com.tangem.domain.AddCustomTokenError -import com.tangem.domain.DomainWrapped -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.common.TapWorkarounds.isTestCard -import com.tangem.domain.common.extensions.isSupportedInApp -import com.tangem.domain.common.extensions.supportedBlockchains -import com.tangem.domain.common.extensions.supportedTokens -import com.tangem.domain.common.form.* -import com.tangem.domain.features.addCustomToken.* -import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.redux.DomainState -import com.tangem.domain.redux.state.StringActionStateConverter -import org.rekotlin.Action -import org.rekotlin.StateType - -data class AddCustomTokenState( - val appSavedCurrencies: List? = null, - val onTokenAddCallback: ((CustomCurrency) -> Unit)? = null, - val cardDerivationStyle: DerivationStyle? = null, - val form: Form = Form(listOf()), - val formValidators: Map> = createFormValidators(), - val formErrors: Map = emptyMap(), - val foundToken: CoinsResponse.Coin? = null, - val warnings: Set = emptySet(), - val screenState: ScreenState = createInitialScreenState(), - val tangemTechServiceManager: AddCustomTokenService? = null, -) : StateType { - - inline fun getField(id: FieldId): T = form.getField(id) as T - - fun setField(field: DataField<*>) { - form.setField(field) - } - - inline fun getValidator(id: FieldId): T = formValidators[id] as T - - fun getError(id: FieldId): AddCustomTokenError? = formErrors[id] - - inline fun visitDataConverter(converter: FieldDataConverter): T { - form.visitDataConverter(converter) - return converter.getConvertedData() - } - - fun blockchainToName(blockchain: Blockchain, isDerivationPath: Boolean = false): String? { - return when { - isDerivationPath -> blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath - else -> { - when (blockchain) { - Blockchain.Unknown -> null - else -> blockchain.fullName - } - } - } - } - - // except network - fun tokensFieldsIsFilled(): Boolean { - val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) - val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } - val validator = StringIsNotEmptyValidator() - fieldsToCheck.forEach { field -> - val error = validator.validate(field.data.value?.toString()) - if (error != null) return false - } - return true - } - - // except network - fun tokensAnyFieldsIsFilled(): Boolean { - val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) - val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } - val validator = StringIsEmptyValidator() - val errorsList = fieldsToCheck.mapNotNull { field -> - validator.validate(field.data.value?.toString()) - } - return errorsList.isNotEmpty() - } - - fun networkIsSelected(): Boolean { - val network = getField(Network) - return network.data.value != Blockchain.Unknown - } - - fun derivationPathIsSelected(): Boolean { - val network = getField(DerivationPath) - return network.data.value != Blockchain.Unknown - } - - fun getCustomTokenType(): CustomTokenType { - return if (tokensAnyFieldsIsFilled() || tokensFieldsIsFilled()) { - CustomTokenType.Token - } else { - CustomTokenType.Blockchain - } - } - - fun gatherUserToken(): CustomCurrency.CustomToken? = try { - getToken() - } catch (ex: Exception) { - null - } - - fun gatherBlockchain(): CustomCurrency.CustomBlockchain? = try { - getBlockchain() - } catch (ex: Exception) { - null - } - - fun reset(cardTypesResolver: CardTypesResolver, card: CardDTO): AddCustomTokenState { - return this.copy( - appSavedCurrencies = null, - onTokenAddCallback = null, - cardDerivationStyle = null, - form = Form(createFormFields(cardTypesResolver, card, CustomTokenType.Blockchain)), - formErrors = emptyMap(), - foundToken = null, - warnings = emptySet(), - screenState = createInitialScreenState(card.settings.isHDWalletAllowed), - tangemTechServiceManager = null, - ) - } - - private fun getToken(): CustomCurrency.CustomToken { - return CustomCurrency.CustomToken.Converter(foundToken?.id, cardDerivationStyle) - .apply { visitDataConverter(this) } - .getConvertedData() - } - - private fun getBlockchain(): CustomCurrency.CustomBlockchain { - return CustomCurrency.CustomBlockchain.Converter(cardDerivationStyle) - .apply { visitDataConverter(this) } - .getConvertedData() - } - - companion object { - - /** - * If an user select derivation path (derivationNetwork) as Blockchain.Unknown, - * then we should use a blockchain from the mainNetwork to determine a DerivationPath - */ - internal fun getDerivationPath( - mainNetwork: Blockchain, - derivationNetwork: Blockchain, - derivationStyle: DerivationStyle?, - ): com.tangem.crypto.hdWallet.DerivationPath? { - // If we allow user to select derivations, we need to provide different derivations - // (Legacy style derivations). - // But the mainNetwork derivation depends on whether a user has a card - // with legacy derivations or new style derivations. - val derivationStyleToUse = if (derivationNetwork == Blockchain.Unknown) { - derivationStyle - } else { - DerivationStyle.LEGACY - } - return when (derivationNetwork) { - Blockchain.Unknown -> mainNetwork - else -> derivationNetwork - }.derivationPath(derivationStyleToUse) - } - - internal fun createFormFields( - cardTypesResolver: CardTypesResolver, - card: CardDTO, - type: CustomTokenType, - ): List> { - return listOf( - TokenField(ContractAddress), - TokenBlockchainField(Network, getNetworksList(cardTypesResolver, card, type)), - TokenField(Name), - TokenField(Symbol), - TokenField(Decimals), - TokenDerivationPathField(DerivationPath, getSupportedDerivations(card)), - ) - } - - /** - * Serves to determine the networks (blockchains & tokens) that can be selected by Form.Networks. - * Blockchain.Unknown - is the default selection - */ - private fun getNetworksList( - cardTypesResolver: CardTypesResolver, - card: CardDTO, - type: CustomTokenType, - ): List { - val evmBlockchains = Blockchain.values() - .filter { it.isEvm() } - .filter { card.isTestCard == it.isTestnet() } - - val additionalBlockchains = listOf( - Blockchain.Binance, - Blockchain.BinanceTestnet, - Blockchain.Solana, - Blockchain.SolanaTestnet, - Blockchain.Tron, - Blockchain.TronTestnet, - ) - - val supportedByCard = when (type) { - CustomTokenType.Blockchain -> card.supportedBlockchains(cardTypesResolver) - CustomTokenType.Token -> card.supportedTokens(cardTypesResolver) - } - val typedNetworksList = (evmBlockchains + additionalBlockchains) - .filter { supportedByCard.contains(it) } - .toMutableList() - - val default = Blockchain.Unknown - typedNetworksList.add(0, default) - - return typedNetworksList.sortByName() - } - - private fun createFormValidators(): Map> { - return mapOf( - ContractAddress to TokenContractAddressValidator(), - Network to TokenNetworkValidator(), - Name to TokenNameValidator(), - Symbol to TokenSymbolValidator(), - Decimals to TokenDecimalsValidator(), - ) - } - - private fun getSupportedDerivations(card: CardDTO): List { - val evmBlockchains = Blockchain.values() - .filter { card.isTestCard == it.isTestnet() && it.isEvm() } - .filter { it.isSupportedInApp() } - - return (listOf(Blockchain.Unknown) + evmBlockchains).sortByName() - } - - internal fun createInitialScreenState(showDerivationPathField: Boolean = false): ScreenState { - return ScreenState( - contractAddressField = ViewStates.TokenField(), - network = ViewStates.TokenField(), - name = ViewStates.TokenField(isEnabled = false), - symbol = ViewStates.TokenField(isEnabled = false), - decimals = ViewStates.TokenField(isEnabled = false), - derivationPath = ViewStates.TokenField(isVisible = showDerivationPathField), - addButton = ViewStates.AddButton(isEnabled = false), - ) - } - } - - class Converter : StringActionStateConverter { - private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE - private var builder: StringBuilder = StringBuilder() - - override fun convert(action: Action, stateHolder: DomainState): String? { - if (action !is AddCustomTokenAction) return null - - val state = stateHolder.addCustomTokensState - val fieldConverter = - FieldToJsonConverter( - listOf( - ContractAddress, - Network, - Name, - Symbol, - Decimals, - DerivationPath, - ), - jsonConverter, - ) - state.visitDataConverter(fieldConverter) - val errors = state.formErrors.map { - "${it.key}: ${it.value::class.java.simpleName}" - } - val warnings = state.warnings.map { it::class.java.simpleName } - - printAction(action, state) - printStateValue("fields", fieldConverter.getConvertedData()) - printStateValue("fieldErrors", toJson(errors)) - printStateValue("warnings", toJson(warnings)) - printStateValue("screenState", toJson(state.screenState)) - printMessage("------------------------------------------------------") - - val printed = builder.toString() - builder = StringBuilder() - - return printed - } - - private fun printStateValue(name: String, value: String) { - printMessage("$name: $value") - } - - private fun printAction(action: AddCustomTokenAction, state: AddCustomTokenState) { - printMessage("action: $action, state: ${state::class.java.simpleName}") - } - - private fun toJson(value: Any): String { - return jsonConverter.prettyPrint(value) - } - - private fun printMessage(message: String) { - builder.append("$message\n") - } - } -} - -private fun List.sortByName(): List = this.sortedBy { it.fullName } - -enum class CustomTokenType { - Token, Blockchain -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt deleted file mode 100644 index 444255ef7f..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.domain.features.addCustomToken.redux - -/** -[REDACTED_AUTHOR] - */ -// describes state the screen, except the form fields -data class ScreenState( - val contractAddressField: ViewStates.TokenField, - val network: ViewStates.TokenField, - val name: ViewStates.TokenField, - val symbol: ViewStates.TokenField, - val decimals: ViewStates.TokenField, - val derivationPath: ViewStates.TokenField, - val addButton: ViewStates.AddButton, -) - -sealed class ViewStates { - data class TokenField( - val isLoading: Boolean = false, - val isEnabled: Boolean = true, - val isVisible: Boolean = true, - ) : ViewStates() - - data class AddButton( - val isEnabled: Boolean = true, - ) : ViewStates() -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt index 49cc235412..67be91d2dd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt @@ -1,13 +1,9 @@ package com.tangem.domain.redux -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState import com.tangem.domain.redux.global.DomainGlobalState import org.rekotlin.StateType /** [REDACTED_AUTHOR] */ -data class DomainState( - val globalState: DomainGlobalState = DomainGlobalState(), - val addCustomTokensState: AddCustomTokenState = AddCustomTokenState(), -) : StateType \ No newline at end of file +data class DomainState(val globalState: DomainGlobalState = DomainGlobalState()) : StateType \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt index fbd4fe3735..a141cd5f9a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt @@ -1,7 +1,5 @@ package com.tangem.domain.redux -import com.tangem.domain.DomainLayer -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub import com.tangem.domain.redux.global.DomainGlobalHub import org.rekotlin.Action import org.rekotlin.Store @@ -9,10 +7,7 @@ import org.rekotlin.Store /** [REDACTED_AUTHOR] */ -private val RE_STORE_HUBS: List> = listOf( - DomainGlobalHub(), - AddCustomTokenHub(), -) +private val RE_STORE_HUBS: List> = listOf(DomainGlobalHub()) val domainStore = Store( state = DomainState(), @@ -37,7 +32,6 @@ private fun reduce(action: Action, domainState: DomainState?): DomainState { assembleReducedDomainState } } - DomainLayer.actionStateLogger.log(reducedStatesByAction) return assembleReducedDomainState } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt index 2c9edb34a5..90bf41578e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt @@ -1,6 +1,5 @@ package com.tangem.domain.redux.global -import com.tangem.domain.DomainDialog import com.tangem.domain.models.scan.ScanResponse import org.rekotlin.Action @@ -10,5 +9,4 @@ import org.rekotlin.Action // TODO: refactoring: is alias for the GlobalAction sealed class DomainGlobalAction : Action { data class SaveScanNoteResponse(val scanResponse: ScanResponse) : DomainGlobalAction() - data class ShowDialog(val stateDialog: DomainDialog?) : DomainGlobalAction() } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt index f22a14aa64..e552550257 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt @@ -47,9 +47,6 @@ private class DomainGlobalReducer : ReStoreReducer { ) state.copy(scanResponse = action.scanResponse) } - is DomainGlobalAction.ShowDialog -> { - state.copy(dialog = action.stateDialog) - } else -> state } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt index 36a97859c9..a424fbb683 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt @@ -2,7 +2,6 @@ package com.tangem.domain.redux.global import com.tangem.datasource.api.paymentology.PaymentologyApiService import com.tangem.datasource.api.tangemTech.TangemTechService -import com.tangem.domain.DomainDialog import com.tangem.domain.models.scan.ScanResponse /** @@ -14,7 +13,6 @@ data class DomainGlobalState( val scanResponse: ScanResponse? = null, // val networkServices: NetworkServices = NetworkServices(), - val dialog: DomainDialog? = null, ) data class NetworkServices( diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt deleted file mode 100644 index 8e99e0f538..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.domain.redux.state - -import com.tangem.domain.redux.DomainState -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -interface StringStateConverter { - fun convert(stateHolder: StateHolder): String -} - -interface StringActionStateConverter { - fun convert(action: Action, stateHolder: StateHolder): String? -} - -class ActionStateConvertersFactory { - private val stateConverters = mutableMapOf, StringActionStateConverter>() - - fun addConverter(classOfAction: Class, converter: StringActionStateConverter) { - stateConverters[classOfAction] = converter - } - - fun getConverter(action: Action): StringActionStateConverter? { - val converter = stateConverters.firstNotNullOfOrNull { (classOfAction, converter) -> - if (classOfAction.isAssignableFrom(action::class.java)) { - converter - } else { - null - } - } ?: return null - - return converter - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt deleted file mode 100644 index 576d398ea8..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.domain.redux.state - -import com.tangem.domain.features.BuildConfig -import com.tangem.domain.redux.DomainState -import org.rekotlin.Action -import timber.log.Timber - -/** -[REDACTED_AUTHOR] - * Use it only in debug mode! - */ -internal interface ActionStateLogger { - fun log(reducedSates: List>) -} - -internal class ActionStateLoggerImpl : ActionStateLogger { - - val actionStateConvertersFactory = ActionStateConvertersFactory() - - override fun log(reducedSates: List>) { - if (!BuildConfig.LOG_ENABLED) return - - logStates(reducedSates) - } - - private fun logStates(reducedSates: List>) { - reducedSates.forEach { (action, domainState) -> - val messageToPrint = actionStateConvertersFactory.getConverter(action) - ?.convert(action, domainState) - ?: return@forEach - - Timber.d(messageToPrint) - } - } -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/state/StringStateConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StringStateConverter.kt new file mode 100644 index 0000000000..f0660f284f --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StringStateConverter.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.redux.state + +/** +[REDACTED_AUTHOR] + */ +interface StringStateConverter { + fun convert(stateHolder: StateHolder): String +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt b/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt new file mode 100644 index 0000000000..4dcca75874 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.tokens + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet +import org.rekotlin.Action + +sealed interface TokensAction : Action { + + /** Single way to pass data to the screen */ + sealed interface SetArgs : TokensAction { + object ManageAccess : SetArgs + object ReadAccess : SetArgs + } + + @Deprecated("Action is used for saving data by old way. It will be removed after deleting of legacy wallet screen") + data class LegacySaveChanges( + val currentTokens: List, + val currentBlockchains: List, + val changedTokens: List, + val changedBlockchains: List, + val scanResponse: ScanResponse, + ) : TokensAction + + data class NewSaveChanges( + val currentTokens: List, + val currentCoins: List, + val changedTokens: List, + val changedCoins: List, + val userWallet: UserWallet, + ) : TokensAction +} + +data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt index 565930ae9d..4916db578c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt @@ -27,6 +27,10 @@ class UserWalletBuilder( } } + /** + * DANGEROUS!!! + * [backupCardsIds] will be non-empty list if card is backed up on current device. + */ fun backupCardsIds(backupCardsIds: Set?) = this.apply { if (backupCardsIds != null) { this.backupCardsIds = backupCardsIds 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 deb238337d..90455b7b92 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 @@ -1,26 +1,29 @@ package com.tangem.domain.walletmanager -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.BlockchainSdkError -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider +import com.tangem.blockchain.blockchains.solana.RentProvider +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.extensions.Result import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore -import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.hasDerivation import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.walletmanager.utils.* +import com.tangem.domain.walletmanager.utils.WalletManagerFactory import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import timber.log.Timber +import java.math.BigDecimal // FIXME: Move to its own module and make internal @Deprecated("Inject the WalletManagerFacade interface using DI instead") @@ -39,42 +42,46 @@ class DefaultWalletManagersFacade( override suspend fun update( userWalletId: UserWalletId, - networkId: Network.ID, + network: Network, extraTokens: Set, ): UpdateWalletManagerResult { val userWallet = getUserWallet(userWalletId) - val blockchain = Blockchain.fromId(networkId.value) + val blockchain = Blockchain.fromId(network.id.value) + val derivationPath = network.derivationPath.value - return getAndUpdateWalletManager(userWallet, blockchain, extraTokens) + return getAndUpdateWalletManager(userWallet, blockchain, derivationPath, extraTokens) } - override suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String { + override suspend fun getExploreUrl(userWalletId: UserWalletId, network: Network): String { val userWallet = getUserWallet(userWalletId) + val blockchain = Blockchain.fromId(network.id.value) - val blockchain = Blockchain.fromId(networkId.value) - - return getOrCreateWalletManager( + val walletManager = getOrCreateWalletManager( userWallet = userWallet, blockchain = blockchain, - derivationPath = blockchain - .derivationPath(userWallet.scanResponse.derivationStyleProvider.getDerivationStyle()), + derivationPath = network.derivationPath.value, ) - ?.wallet - ?.getExploreUrl() - .orEmpty() - } - override suspend fun getTxHistoryState( - userWalletId: UserWalletId, - networkId: Network.ID, - rawDerivationPath: String?, - ): TxHistoryState { - val userWallet = getUserWallet(userWalletId) - val blockchain = Blockchain.fromId(networkId.value) - val derivationPath = rawDerivationPath?.let(::DerivationPath) - val walletManager = requireNotNull(getOrCreateWalletManager(userWallet, blockchain, derivationPath)) { + requireNotNull(walletManager) { "Unable to get a wallet manager for blockchain: $blockchain" } + + return walletManager.wallet.getExploreUrl() + } + + override suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState { + val userWallet = getUserWallet(userWalletId) + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + requireNotNull(walletManager) { + "Unable to get a wallet manager for blockchain: $blockchain" + } + return walletManager .getTransactionHistoryState(walletManager.wallet.address) .let(txHistoryStateConverter::convert) @@ -82,17 +89,22 @@ class DefaultWalletManagersFacade( override suspend fun getTxHistoryItems( userWalletId: UserWalletId, - networkId: Network.ID, - rawDerivationPath: String?, + network: Network, page: Int, pageSize: Int, ): PaginationWrapper { val userWallet = getUserWallet(userWalletId) - val blockchain = Blockchain.fromId(networkId.value) - val derivationPath = rawDerivationPath?.let(::DerivationPath) - val walletManager = requireNotNull(getOrCreateWalletManager(userWallet, blockchain, derivationPath)) { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + requireNotNull(walletManager) { "Unable to get a wallet manager for blockchain: $blockchain" } + val itemsResult = walletManager.getTransactionsHistory( address = walletManager.wallet.address, page = page, @@ -118,12 +130,12 @@ class DefaultWalletManagersFacade( private suspend fun getAndUpdateWalletManager( userWallet: UserWallet, blockchain: Blockchain, + derivationPath: String?, extraTokens: Set, ): UpdateWalletManagerResult { val scanResponse = userWallet.scanResponse - val derivationPath = blockchain.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) - if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath)) { + if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath)) { Timber.e("Derivation missed for: $blockchain") return UpdateWalletManagerResult.MissedDerivation } @@ -171,21 +183,21 @@ class DefaultWalletManagersFacade( override suspend fun getOrCreateWalletManager( userWallet: UserWallet, blockchain: Blockchain, - derivationPath: DerivationPath?, + derivationPath: String?, ): WalletManager? { val userWalletId = userWallet.walletId var walletManager = walletManagersStore.getSyncOrNull( userWalletId = userWalletId, blockchain = blockchain, - derivationPath = derivationPath?.rawPath, + derivationPath = derivationPath, ) if (walletManager == null) { walletManager = walletManagerFactory.createWalletManager( scanResponse = userWallet.scanResponse, blockchain = blockchain, - derivationPath = derivationPath, + derivationPath = derivationPath?.let { DerivationPath(rawPath = it) }, ) ?: return null walletManagersStore.store(userWalletId, walletManager) @@ -194,6 +206,64 @@ class DefaultWalletManagersFacade( return walletManager } + override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
{ + val userWallet = getUserWallet(userWalletId) + val blockchain = Blockchain.fromId(network.id.value) + + return getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + ?.wallet + ?.addresses + ?.sortedBy { it.type } + .orEmpty() + } + + override suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): CryptoCurrencyWarning.Rent? { + val userWallet = getUserWallet(userWalletId) + val blockchain = Blockchain.fromId(network.id.value) + val manager = getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + if (manager !is RentProvider) return null + + return when (val result = manager.minimalBalanceForRentExemption()) { + is Result.Success -> { + val balance = manager.wallet.fundsAvailable(AmountType.Coin) + val outgoingTxs = manager.wallet.recentTransactions + .filter { it.sourceAddress == manager.wallet.address && it.amount.type == AmountType.Coin } + + val rentExempt = result.data + val setRent = if (outgoingTxs.isEmpty()) { + balance < rentExempt + } else { + val outgoingAmount = outgoingTxs.sumOf { it.amount.value ?: BigDecimal.ZERO } + val rest = balance.minus(outgoingAmount) + balance < rest + } + + if (setRent) CryptoCurrencyWarning.Rent(manager.rentAmount(), rentExempt) else null + } + is Result.Failure -> null + } + } + + override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? { + val userWallet = getUserWallet(userWalletId) + val blockchain = Blockchain.fromId(network.id.value) + val manager = getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + + return if (manager is ExistentialDepositProvider) manager.getExistentialDeposit() else null + } + 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 4c296b01e4..a958a783aa 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,15 +2,18 @@ package com.tangem.domain.walletmanager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager -import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.blockchain.common.address.Address +import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryState import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal // TODO: Move to its own module /** @@ -22,52 +25,75 @@ interface WalletManagersFacade { * Updates the wallet manager associated with a user's wallet and network. * * @param userWalletId The ID of the user's wallet. - * @param networkId The network ID. + * @param network The network. * @param extraTokens Additional tokens. * @return The result of updating the wallet manager. */ suspend fun update( userWalletId: UserWalletId, - networkId: Network.ID, + network: Network, extraTokens: Set, ): UpdateWalletManagerResult - suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String + /** + * Returns network explorer URL of the wallet manager associated with a user's wallet and network. + * + * @param userWalletId The ID of the user's wallet. + * @param network The network. + * + * @return The network explorer URL, maybe empty if the wallet manager was not found. + * */ + suspend fun getExploreUrl(userWalletId: UserWalletId, network: Network): String /** * Returns transactions count * * @param userWalletId The ID of the user's wallet. - * @param networkId The network ID. - * @param rawDerivationPath Derivation path in raw form. - + * @param network The network. */ - suspend fun getTxHistoryState( - userWalletId: UserWalletId, - networkId: Network.ID, - rawDerivationPath: String?, - ): TxHistoryState + suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState /** * Returns transaction history items wrapped to pagination * * @param userWalletId The ID of the user's wallet. - * @param networkId The network ID. - * @param rawDerivationPath Derivation path in raw form. + * @param network The network. * @param page Pagination page. * @param pageSize Pagination size. */ suspend fun getTxHistoryItems( userWalletId: UserWalletId, - networkId: Network.ID, - rawDerivationPath: String?, + network: Network, page: Int, pageSize: Int, ): PaginationWrapper + // TODO: Remove after refactoring suspend fun getOrCreateWalletManager( userWallet: UserWallet, blockchain: Blockchain, - derivationPath: DerivationPath?, + derivationPath: String?, ): WalletManager? + + /** + * Returns ordered list of addresses for selected wallet for given currency + * + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun getAddress(userWalletId: UserWalletId, network: Network): List
+ + /** + * Returns info about rent if wallet manager implemented [RentProvider], otherwise null + * + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun getRentInfo(userWalletId: UserWalletId, network: Network): CryptoCurrencyWarning.Rent? + + /** + * Returns value which indicates if the account balance drops below the existential deposit value, it will be + * deactivated and any remaining funds will be destroyed. + */ + suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt index c0b69b941f..94c3bba81b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyTransaction.kt @@ -1,28 +1,17 @@ package com.tangem.domain.walletmanager.model -import org.joda.time.DateTime -import java.math.BigDecimal +import com.tangem.domain.txhistory.models.TxHistoryItem +// TODO: [REDACTED_JIRA] move to txhistory module sealed class CryptoCurrencyTransaction { - abstract val amount: BigDecimal - abstract val fromAddress: String? - abstract val toAddress: String? - abstract val sentAt: DateTime + abstract val txHistoryItem: TxHistoryItem - data class Coin( - override val amount: BigDecimal, - override val fromAddress: String?, - override val toAddress: String?, - override val sentAt: DateTime, - ) : CryptoCurrencyTransaction() + data class Coin(override val txHistoryItem: TxHistoryItem) : CryptoCurrencyTransaction() data class Token( val tokenId: String?, val tokenContractAddress: String, - override val amount: BigDecimal, - override val fromAddress: String?, - override val toAddress: String?, - override val sentAt: DateTime, + override val txHistoryItem: TxHistoryItem, ) : CryptoCurrencyTransaction() } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index 4d644e4b7c..effa52cfdb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -3,37 +3,37 @@ package com.tangem.domain.walletmanager.utils import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.domain.common.extensions.amountToCreateAccount +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount import com.tangem.domain.walletmanager.model.CryptoCurrencyTransaction import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.Instant import timber.log.Timber import java.math.BigDecimal -import java.util.Calendar +import java.util.concurrent.TimeUnit internal class UpdateWalletManagerResultFactory { fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified { val wallet = walletManager.wallet + val addresses = getAvailableAddresses(wallet.addresses) return UpdateWalletManagerResult.Verified( defaultAddress = wallet.address, - addresses = getAvailableAddresses(wallet.addresses), + addresses = addresses, currenciesAmounts = getTokensAmounts(wallet.amounts.values.toSet()), - currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()), + currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()), ) } fun getDemoResult(walletManager: WalletManager, demoAmount: Amount): UpdateWalletManagerResult.Verified { val wallet = walletManager.wallet + val addresses = getAvailableAddresses(wallet.addresses) return UpdateWalletManagerResult.Verified( defaultAddress = wallet.address, - addresses = getAvailableAddresses(wallet.addresses), + addresses = addresses, currenciesAmounts = getDemoTokensAmounts(demoAmount, walletManager.cardTokens), - currentTransactions = getCurrentTransactions(wallet.recentTransactions.toSet()), + currentTransactions = getCurrentTransactions(addresses, wallet.recentTransactions.toSet()), ) } @@ -70,12 +70,15 @@ internal class UpdateWalletManagerResultFactory { } } - private fun getCurrentTransactions(recentTransactions: Set): Set { + private fun getCurrentTransactions( + walletAddresses: Set, + recentTransactions: Set, + ): Set { val unconfirmedTransactions = recentTransactions.filter { it.status == TransactionStatus.Unconfirmed } - return unconfirmedTransactions.mapNotNullTo(hashSetOf(), ::createCurrencyTransaction) + return unconfirmedTransactions.mapNotNullTo(hashSetOf()) { createCurrencyTransaction(walletAddresses, it) } } private fun createCurrencyAmount(amount: Amount): CryptoCurrencyAmount? { @@ -92,31 +95,78 @@ internal class UpdateWalletManagerResultFactory { } } - private fun createCurrencyTransaction(data: TransactionData): CryptoCurrencyTransaction? { - val fromAddress = takeAddressIfNotUnknown(data.sourceAddress) - val toAddress = takeAddressIfNotUnknown(data.destinationAddress) - val amount = getTransactionAmountValue(data.amount) ?: return null - val sentAt = getTransactionSentTime(data.date) ?: return null - + private fun createCurrencyTransaction( + walletAddresses: Set, + data: TransactionData, + ): CryptoCurrencyTransaction? { return when (val type = data.amount.type) { - is AmountType.Coin -> CryptoCurrencyTransaction.Coin( - amount = amount, - fromAddress = fromAddress, - toAddress = toAddress, - sentAt = sentAt, - ) - is AmountType.Token -> CryptoCurrencyTransaction.Token( - tokenId = type.token.id, - tokenContractAddress = type.token.contractAddress, - amount = amount, - fromAddress = fromAddress, - toAddress = toAddress, - sentAt = sentAt, - ) + is AmountType.Coin -> { + val txHistoryItem = createTxHistoryItem(walletAddresses, data) ?: return null + CryptoCurrencyTransaction.Coin(txHistoryItem) + } + is AmountType.Token -> { + val txHistoryItem = createTxHistoryItem(walletAddresses, data) ?: return null + CryptoCurrencyTransaction.Token( + tokenId = type.token.id, + tokenContractAddress = type.token.contractAddress, + txHistoryItem = txHistoryItem, + ) + } is AmountType.Reserve -> null } } + private fun createTxHistoryItem(walletAddresses: Set, data: TransactionData): TxHistoryItem? { + val direction = extractDirection(walletAddresses, data) ?: run { + Timber.w("Can not determine address for $data") + return null + } + val hash = data.hash ?: return null + val millis = data.date?.timeInMillis ?: return null + val amount = getTransactionAmountValue(data.amount) ?: return null + + return TxHistoryItem( + txHash = hash, + timestampInMillis = TimeUnit.SECONDS.toMillis(millis), + direction = direction, + status = when (data.status) { + TransactionStatus.Confirmed -> TxHistoryItem.TxStatus.Confirmed + TransactionStatus.Unconfirmed -> TxHistoryItem.TxStatus.Unconfirmed + }, + type = TxHistoryItem.TransactionType.Transfer, + amount = amount, + ) + } + + private fun extractDirection( + walletAddresses: Set, + data: TransactionData, + ): TxHistoryItem.TransactionDirection? { + val fromAddress = data.sourceAddress + val toAddress = data.destinationAddress + + return when { + toAddress in walletAddresses -> { + TxHistoryItem.TransactionDirection.Incoming(TxHistoryItem.Address.Single(fromAddress)) + } + fromAddress in walletAddresses -> { + TxHistoryItem.TransactionDirection.Outgoing(TxHistoryItem.Address.Single(toAddress)) + } + else -> { + Timber.e( + """ + Unable to find transaction direction + |- To address: ${data.destinationAddress} + |- From address: ${data.sourceAddress} + |- Network addresses: $walletAddresses + """.trimIndent(), + ) + + return null + } + } + } + private fun getAvailableAddresses(addresses: Set
): Set { return addresses.mapTo(hashSetOf()) { it.value } } @@ -140,24 +190,4 @@ internal class UpdateWalletManagerResultFactory { return value } - - private fun getTransactionSentTime(date: Calendar?): DateTime? { - if (date == null) { - Timber.e("Transaction date must not be null") - return null - } - - val instant = Instant.ofEpochMilli(date.timeInMillis) - val timeZone = DateTimeZone.forTimeZone(date.timeZone) - - return instant.toDateTime(timeZone) - } - - private fun takeAddressIfNotUnknown(address: String): String? { - return address.takeIf { it.isNotBlank() && it != UNKNOWN_TRANSACTION_ADDRESS } - } - - private companion object { - const val UNKNOWN_TRANSACTION_ADDRESS = "unknown" - } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index 1777aa498a..83dfcafb49 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -46,7 +46,7 @@ data class CardDTO( isAccessCodeSet = card.isAccessCodeSet, isPasscodeSet = card.isPasscodeSet, supportedCurves = card.supportedCurves, - wallets = card.wallets.map { Wallet(it) }, + wallets = card.wallets.map(::Wallet), attestation = card.attestation, backupStatus = BackupStatus.fromSdkStatus(card.backupStatus), ) @@ -234,6 +234,7 @@ data class CardDTO( val hasBackup: Boolean, val derivedKeys: Map, val extendedPublicKey: ExtendedPublicKey?, + val isImported: Boolean, ) { constructor(wallet: CardWallet) : this( publicKey = wallet.publicKey, @@ -246,8 +247,10 @@ data class CardDTO( hasBackup = wallet.hasBackup, derivedKeys = wallet.derivedKeys, extendedPublicKey = wallet.extendedPublicKey, + isImported = wallet.isImported, ) + @Suppress("CyclomaticComplexMethod") override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Wallet) return false @@ -256,13 +259,16 @@ data class CardDTO( if (chainCode != null) { if (other.chainCode == null) return false if (!chainCode.contentEquals(other.chainCode)) return false - } else if (other.chainCode != null) return false + } else { + if (other.chainCode != null) return false + } if (curve != other.curve) return false if (settings != other.settings) return false if (totalSignedHashes != other.totalSignedHashes) return false if (remainingSignatures != other.remainingSignatures) return false if (index != other.index) return false if (hasBackup != other.hasBackup) return false + if (isImported != other.isImported) return false return true } @@ -276,6 +282,7 @@ data class CardDTO( result = 31 * result + (remainingSignatures ?: 0) result = 31 * result + index result = 31 * result + hasBackup.hashCode() + result = 31 * result + isImported.hashCode() return result } } diff --git a/domain/settings/build.gradle.kts b/domain/settings/build.gradle.kts index 7ff7fb7522..00cd0a439e 100644 --- a/domain/settings/build.gradle.kts +++ b/domain/settings/build.gradle.kts @@ -1,4 +1,9 @@ plugins { alias(deps.plugins.kotlin.jvm) id("configuration") +} + +dependencies { + implementation(deps.kotlin.coroutines) + implementation(projects.domain.balanceHiding.models) } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 6fae380259..c146658684 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -1,14 +1,21 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.tokens" +} + dependencies { /** Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.models) + implementation(projects.domain.legacy) implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt index c4346df385..c4f9d25b2b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt @@ -11,8 +11,7 @@ import java.io.Serializable * @property symbol Symbol of the cryptocurrency. * @property decimals Number of decimal places used by the cryptocurrency. * @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found. - * @property derivationPath Optional path used for key derivation. `null` if the wallet does not support the - * [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/) feature. + * @property isCustom Indicates whether the currency is a custom user-added currency or not. */ // FIXME: Remove serialization [REDACTED_JIRA] sealed class CryptoCurrency : Serializable { @@ -23,7 +22,7 @@ sealed class CryptoCurrency : Serializable { abstract val symbol: String abstract val decimals: Int abstract val iconUrl: String? - abstract val derivationPath: String? + abstract val isCustom: Boolean /** * Represents a native coin in the blockchain network. @@ -35,7 +34,7 @@ sealed class CryptoCurrency : Serializable { override val symbol: String, override val decimals: Int, override val iconUrl: String?, - override val derivationPath: String?, + override val isCustom: Boolean, ) : CryptoCurrency() { init { @@ -47,7 +46,6 @@ sealed class CryptoCurrency : Serializable { * Represents a token in the blockchain network, typically a non-native asset. * * @property contractAddress Address of the contract managing the token. - * @property isCustom Indicates whether the token is a custom user-added token or not. */ data class Token( override val id: ID, @@ -56,9 +54,8 @@ sealed class CryptoCurrency : Serializable { override val symbol: String, override val decimals: Int, override val iconUrl: String?, - override val derivationPath: String?, + override val isCustom: Boolean, val contractAddress: String, - val isCustom: Boolean, ) : CryptoCurrency() { init { @@ -80,32 +77,70 @@ sealed class CryptoCurrency : Serializable { // FIXME: Remove serialization [REDACTED_JIRA] data class ID( private val prefix: Prefix, - private val networkId: Network.ID, + private val body: Body, private val suffix: Suffix, ) : Serializable { val value: String = buildString { append(prefix.value) - append(networkId.value) - append(DELIMITER) + append(PREFIX_DELIMITER) + append(body.value) + append(SUFFIX_DELIMITER) append(suffix.value) } + /** Represents a raw cryptocurrency ID. If it is a custom token, the value will be `null`. */ val rawCurrencyId: String? = (suffix as? Suffix.RawID)?.rawId + /** Represents a raw cryptocurrency's network ID. */ + val rawNetworkId: String = when (body) { + is Body.NetworkId -> body.rawId + is Body.NetworkIdWithDerivationPath -> body.rawId + } + /** * Represents the different types of prefixes that can be associated with a cryptocurrency ID. + * * These prefixes can help in quickly categorizing the type of cryptocurrency. */ enum class Prefix(val value: String) { /** Prefix for standard coins. */ - COIN_PREFIX(value = "coin_"), + COIN_PREFIX(value = "coin"), /** Prefix for standard tokens. */ - TOKEN_PREFIX(value = "token_"), + TOKEN_PREFIX(value = "token"), + } - /** Prefix for custom tokens. */ - CUSTOM_TOKEN_PREFIX(value = "custom_"), + /** + * Represents the body part of the cryptocurrency ID. + * + * The body can be either a raw network ID or a raw network ID with a network derivation path. + */ + sealed class Body : Serializable { + + /** The value of the body. */ + abstract val value: String + + /** Represents a raw network ID. */ + data class NetworkId(val rawId: String) : Body() { + override val value: String = rawId + } + + /** + * Represents a raw network ID with a network derivation path. + * + * Should be used for a cryptocurrencies with custom derivation path. + * */ + data class NetworkIdWithDerivationPath( + val rawId: String, + val derivationPath: String, + ) : Body() { + override val value: String = buildString { + append(rawId) + append(DERIVATION_PATH_DELIMITER) + append(derivationPath.hashCode()) + } + } } /** @@ -130,8 +165,14 @@ sealed class CryptoCurrency : Serializable { } } + override fun toString(): String { + return "ID(value='$value')" + } + private companion object { - const val DELIMITER = '#' + const val PREFIX_DELIMITER = '_' + const val SUFFIX_DELIMITER = '#' + const val DERIVATION_PATH_DELIMITER = 'd' } } @@ -140,6 +181,5 @@ sealed class CryptoCurrency : Serializable { require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" } require(iconUrl?.isNotBlank() ?: true) { "Crypto currency icon URL must not be blank" } require(decimals >= 0) { "Crypto currency decimal must not be less then zero, but it is: $decimals" } - require(derivationPath?.isNotBlank() ?: true) { "Crypto currency derivation path must not be blank" } } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt index aaa1cb39d8..c58935ed82 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt @@ -11,6 +11,7 @@ import java.io.Serializable * * @property id The unique identifier of the network. * @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin". + * @property derivationPath The path used to derive keys for this network. * @property isTestnet Indicates whether the network is a test network or a main network. * @property standardType The type of blockchain standard the network adheres to. */ @@ -18,6 +19,7 @@ import java.io.Serializable data class Network( val id: ID, val name: String, + val derivationPath: DerivationPath, val isTestnet: Boolean, val standardType: StandardType, ) : Serializable { @@ -39,6 +41,39 @@ data class Network( } } + /** + * Represents a path used to derive cryptographic keys for a blockchain network. + * + * This class represents such paths in a generic manner, allowing for predefined card-based paths, + * custom paths, or even no derivation path at all. + */ + sealed class DerivationPath : Serializable { + + /** The actual derivation path value, if any. */ + abstract val value: String? + + /** + * Represents a predefined card-based derivation path. + * + * @property value The derivation path string. + */ + data class Card(override val value: String) : DerivationPath() + + /** + * Represents a custom derivation path specified by the user. + * + * @property value The derivation path string. + */ + data class Custom(override val value: String) : DerivationPath() + + /** + * Represents a lack of derivation path. + */ + object None : DerivationPath() { + override val value: String? = null + } + } + /** * Represents the type of blockchain standard that a network adheres to. * diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt index 906f30f1fd..0c6f1a0561 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt @@ -1,8 +1,5 @@ package com.tangem.domain.tokens.models.remove -import com.tangem.domain.tokens.models.CryptoCurrency - sealed class RemoveCurrencyError : Throwable() { - data class HasLinkedTokens(val currency: CryptoCurrency) : RemoveCurrencyError() data class DataError(override val cause: Throwable) : RemoveCurrencyError() } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/warnings/CryptoCurrencyWarning.kt new file mode 100644 index 0000000000..538d306a82 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/warnings/CryptoCurrencyWarning.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.tokens.models.warnings + +import com.tangem.domain.tokens.models.CryptoCurrency +import java.math.BigDecimal + +sealed class CryptoCurrencyWarning { + + data class ExistentialDeposit( + val currencyName: String, + val edStringValueWithSymbol: String, + ) : CryptoCurrencyWarning() + + data class BalanceNotEnoughForFee( + val currency: CryptoCurrency, + val blockchainFullName: String, + val blockchainSymbol: String, + ) : CryptoCurrencyWarning() + + object SomeNetworksUnreachable : CryptoCurrencyWarning() + + /** + * Represents wallet blockchain rent + * @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than + * the [exemptionAmount] + * @param exemptionAmount Amount that should be on the blockchain balance not to pay rent + */ + data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index b10e3633c9..91f7e713a4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -75,7 +75,7 @@ class FetchCurrencyStatusUseCase( refresh: Boolean, ) = coroutineScope { val fetchStatus = async { - fetchNetworkStatus(userWalletId, currency.network.id, refresh) + fetchNetworkStatus(userWalletId, currency.network, refresh) } val fetchQuote = async { fetchQuote(currency.id, refresh) @@ -101,11 +101,11 @@ class FetchCurrencyStatusUseCase( private suspend fun Raise.fetchNetworkStatus( userWalletId: UserWalletId, - networkId: Network.ID, + network: Network, refresh: Boolean, ) { catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(networkId), refresh) }, + block = { networksRepository.getNetworkStatusesSync(userWalletId, setOf(network), refresh) }, ) { raise(CurrencyStatusError.DataError(it)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 7e379288b9..9dc971195a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -48,7 +48,7 @@ class FetchTokenListUseCase( val fetchStatuses = async { fetchNetworksStatuses( userWalletId, - currencies.mapTo(hashSetOf()) { it.network.id }, + currencies.mapTo(hashSetOf()) { it.network }, refresh, ) } @@ -81,11 +81,11 @@ class FetchTokenListUseCase( private suspend fun Raise.fetchNetworksStatuses( userWalletId: UserWalletId, - networksIds: Set, + networks: Set, refresh: Boolean, ) { catch( - block = { networksRepository.getNetworkStatusesSync(userWalletId, networksIds, refresh) }, + block = { networksRepository.getNetworkStatusesSync(userWalletId, networks, refresh) }, ) { raise(TokenListError.DataError(it)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt new file mode 100644 index 0000000000..4595413fa6 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.error.GetCurrenciesError +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class GetCryptoCurrenciesUseCase(private val currenciesRepository: CurrenciesRepository) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + refresh: Boolean = false, + ): Either> { + return either { + catch( + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, refresh) }, + catch = { raise(GetCurrenciesError.DataError(it)) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index d705aacf94..095b4f721e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -1,34 +1,88 @@ package com.tangem.domain.tokens +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository 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 +/** + * Use case to determine which TokenActions are available for a [CryptoCurrency] + * + * @property rampManager Ramp manager to check ramp availability + */ class GetCryptoCurrencyActionsUseCase( + private val rampManager: RampStateManager, + private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, private val dispatchers: CoroutineDispatcherProvider, ) { - operator fun invoke(userWalletId: UserWalletId, tokenId: String): Flow { + operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Flow { return flow { - emit(getMockState(userWalletId, tokenId)) + val actionStates = createTokenActionsState(userWalletId, cryptoCurrencyStatus) + emit(actionStates) }.flowOn(dispatchers.io) } - // TODO replace by real data - private fun getMockState(userWalletId: UserWalletId, tokenId: String): TokenActionsState { + private suspend fun createTokenActionsState( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): TokenActionsState { return TokenActionsState( walletId = userWalletId, - tokenId = tokenId, - states = listOf( - TokenActionsState.ActionState.Buy(true), - TokenActionsState.ActionState.Send(true), - TokenActionsState.ActionState.Receive(true), - TokenActionsState.ActionState.Sell(true), - TokenActionsState.ActionState.Swap(true), - ), + cryptoCurrencyStatus = cryptoCurrencyStatus, + states = createListOfActions(userWalletId, cryptoCurrencyStatus.currency), ) } + + /** + * Creates list of action for expected order + * Actions priority: [Buy Send Receive Sell Swap] + */ + private suspend fun createListOfActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): List { + return buildList { + // todo add check available in swap 1inch etc if backend doen't handle it + if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency.id) && + !isCustomToken(cryptoCurrency) + ) { + addFirst(TokenActionsState.ActionState.Swap(true)) + } else { + add(TokenActionsState.ActionState.Swap(false)) + } + + if (rampManager.availableForSell(cryptoCurrency)) { + addFirst(TokenActionsState.ActionState.Sell(true)) + } else { + add(TokenActionsState.ActionState.Sell(false)) + } + + addFirst(TokenActionsState.ActionState.Receive(true)) + addFirst(TokenActionsState.ActionState.Send(true)) + + if (rampManager.availableForBuy(cryptoCurrency)) { + addFirst(TokenActionsState.ActionState.Buy(true)) + } else { + add(TokenActionsState.ActionState.Buy(false)) + } + } + } + + private fun isCustomToken(currency: CryptoCurrency): Boolean { + return currency is CryptoCurrency.Token && currency.isCustom + } + + private fun MutableList.addFirst(item: T) { + this.add(0, item) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt new file mode 100644 index 0000000000..8e206bdeb7 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -0,0 +1,86 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import java.math.BigDecimal + +class GetCurrencyWarningsUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Flow> { + return combine( + getFeeWarningFlow( + userWalletId = userWalletId, + networkId = currency.network.id, + currencyId = currency.id, + ), + flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), + flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), + ) { maybeFeeWarning, maybeRentWarning, maybeEdWarning -> + setOfNotNull( + maybeRentWarning, + maybeEdWarning?.let { + CryptoCurrencyWarning.ExistentialDeposit( + currencyName = currency.name, + edStringValueWithSymbol = "${it.toPlainString()} ${currency.symbol}", + ) + }, + maybeFeeWarning, + ) + }.flowOn(dispatchers.io) + } + + private suspend fun getFeeWarningFlow( + userWalletId: UserWalletId, + networkId: Network.ID, + currencyId: CryptoCurrency.ID, + ): Flow { + val operations = CurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + ) + + return combine( + operations.getCurrencyStatusFlow(currencyId).map { it.getOrNull() }, + operations.getNetworkCoinFlow(networkId).map { it.getOrNull() }, + ) { tokenStatus, coinStatus -> + when { + tokenStatus != null && coinStatus != null -> { + if (!tokenStatus.value.amount.isZero() && coinStatus.value.amount.isZero()) { + CryptoCurrencyWarning.BalanceNotEnoughForFee( + currency = tokenStatus.currency, + blockchainFullName = coinStatus.currency.name, + blockchainSymbol = coinStatus.currency.symbol, + ) + } else { + null + } + } + else -> CryptoCurrencyWarning.SomeNetworksUnreachable + } + } + } + + private fun BigDecimal?.isZero(): Boolean { + return this?.compareTo(BigDecimal.ZERO) == 0 + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt new file mode 100644 index 0000000000..23ecf06a99 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -0,0 +1,53 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.error.mapper.mapToCurrencyError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* + +class GetNetworkCoinStatusUseCase( + private val currenciesRepository: CurrenciesRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke( + userWalletId: UserWalletId, + networkId: Network.ID, + ): Flow> { + return flow { + emitAll( + flow = getCurrency( + userWalletId = userWalletId, + networkId = networkId, + ), + ) + } + .flowOn(dispatchers.io) + } + + private suspend fun getCurrency( + userWalletId: UserWalletId, + networkId: Network.ID, + ): Flow> { + val operations = CurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + ) + + return operations.getNetworkCoinFlow(networkId).map { maybeCurrency -> + maybeCurrency.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt index 71dc767ed2..e2a908df3b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt @@ -3,7 +3,6 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either -import arrow.core.raise.ensure import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.remove.RemoveCurrencyError import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -20,10 +19,6 @@ class RemoveCurrencyUseCase( currency: CryptoCurrency, ): Either { return either { - ensure( - condition = !currency.hasLinkedTokens(userWalletId), - raise = { RemoveCurrencyError.HasLinkedTokens(currency) }, - ) catch( block = { currenciesRepository.removeCurrency(userWalletId, currency) }, catch = { raise(RemoveCurrencyError.DataError(it)) }, @@ -31,10 +26,17 @@ class RemoveCurrencyUseCase( } } - private suspend fun CryptoCurrency.hasLinkedTokens(userWalletId: UserWalletId): Boolean { - val walletCurrencies = currenciesRepository - .getMultiCurrencyWalletCurrenciesSync(userWalletId = userWalletId, refresh = false) + suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { + return when (currency) { + is CryptoCurrency.Coin -> { + val walletCurrencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync( + userWalletId = userWalletId, + refresh = false, + ) - return this is CryptoCurrency.Coin && walletCurrencies.any { it != this && it.network.id == this.network.id } + walletCurrencies.any { it is CryptoCurrency.Token && it.network == currency.network } + } + is CryptoCurrency.Token -> false + } } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/GetCurrenciesError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/GetCurrenciesError.kt new file mode 100644 index 0000000000..0e68977605 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/GetCurrenciesError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.tokens.error + +sealed class GetCurrenciesError { + + data class DataError(val cause: Throwable) : GetCurrenciesError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index 3ef7c99d04..e9c3430207 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWallet import org.rekotlin.Action +import java.math.BigDecimal sealed class TradeCryptoAction : Action { @@ -36,7 +37,13 @@ sealed class TradeCryptoAction : Action { val appCurrencyCode: String, ) : New() - object Send : New() + data class SendToken( + val userWallet: UserWallet, + val tokenStatus: CryptoCurrencyStatus, + val coinFiatRate: BigDecimal?, + ) : New() + + data class SendCoin(val userWallet: UserWallet, val coinStatus: CryptoCurrencyStatus) : New() data class Swap(val cryptoCurrency: CryptoCurrency) : New() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index b71f6496b5..603ffb73f7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -1,12 +1,13 @@ package com.tangem.domain.tokens.model import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal /** * Represents the status of a cryptocurrency asset within a network. * - * This class encapsulates the details of a specific cryptocurrency, either a coin or token, + * This class encapsulates the details of a specific cryptocurrency, either a coin or cryptocurrency, * along with its current status within the blockchain network. The status can include various states * like Loading, Unreachable, Loaded, etc. * @@ -19,51 +20,56 @@ data class CryptoCurrencyStatus( ) { /** - * Represents the various states a token can have, encapsulating different information based on the state. + * Represents the various states a cryptocurrency can have, encapsulating different information based on the state. + * + * @property isError Indicates whether this status represents an error status. */ - sealed class Status { + sealed class Status(val isError: Boolean) { - /** The amount of the token. */ + /** The amount of the cryptocurrency. */ open val amount: BigDecimal? = null - /** The fiat equivalent of the token's amount. */ + /** The fiat equivalent of the cryptocurrency's amount. */ open val fiatAmount: BigDecimal? = null - /** The exchange rate used for converting the token amount to fiat. */ + /** The exchange rate used for converting the cryptocurrency amount to fiat. */ open val fiatRate: BigDecimal? = null - /** The change in price of the token. */ + /** The change in price of the cryptocurrency. */ open val priceChange: BigDecimal? = null /** Indicates if there are any transactions in progress related to the cryptocurrency network. */ open val hasCurrentNetworkTransactions: Boolean = false /** The pending cryptocurrency transactions. */ - open val pendingTransactions: Set = emptySet() + open val pendingTransactions: Set = emptySet() /** The network address */ open val networkAddress: NetworkAddress? = null } - /** Represents the Loading state of a token, typically while fetching its details. */ - object Loading : Status() + /** Represents the Loading state of a cryptocurrency, typically while fetching its details. */ + object Loading : Status(isError = false) - /** Represents a state where the token is not reachable. */ - object Unreachable : Status() + /** Represents a state where the cryptocurrency is not reachable. */ + object Unreachable : Status(isError = true) - /** Represents a state where the token's derivation is missed. */ - object MissedDerivation : Status() + /** Represents a state where the cryptocurrency's network amount not found. */ + object NoAmount : Status(isError = true) - /** Represents a state where there is no account associated with the token. */ - object NoAccount : Status() + /** Represents a state where the cryptocurrency's derivation is missed. */ + object MissedDerivation : Status(isError = true) + + /** Represents a state where there is no account associated with the cryptocurrency. */ + object NoAccount : Status(isError = false) /** - * Represents a Loaded state of a token with complete information. + * Represents a Loaded state of a cryptocurrency with complete information. * - * @property amount The amount of the token. - * @property fiatAmount The fiat equivalent of the token's amount. - * @property fiatRate The exchange rate used for converting the token amount to fiat. - * @property priceChange The change in price of the token. + * @property amount The amount of the cryptocurrency. + * @property fiatAmount The fiat equivalent of the cryptocurrency's amount. + * @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat. + * @property priceChange The change in price of the cryptocurrency. * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. @@ -74,17 +80,17 @@ data class CryptoCurrencyStatus( override val fiatRate: BigDecimal, override val priceChange: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, - override val pendingTransactions: Set, + override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, - ) : Status() + ) : Status(isError = false) /** - * Represents a Custom state of a token, typically used for user-defined tokens. + * Represents a Custom state of a cryptocurrency, typically used for user-defined tokens. * - * @property amount The amount of the token. - * @property fiatAmount The fiat equivalent of the token's amount (optional). - * @property fiatRate The exchange rate used for converting the token amount to fiat (optional). - * @property priceChange The change in price of the token (optional). + * @property amount The amount of the cryptocurrency. + * @property fiatAmount The fiat equivalent of the cryptocurrency's amount (optional). + * @property fiatRate The exchange rate used for converting the cryptocurrency amount to fiat (optional). + * @property priceChange The change in price of the cryptocurrency (optional). * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. @@ -95,14 +101,14 @@ data class CryptoCurrencyStatus( override val fiatRate: BigDecimal?, override val priceChange: BigDecimal?, override val hasCurrentNetworkTransactions: Boolean, - override val pendingTransactions: Set, + override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, - ) : Status() + ) : Status(isError = false) /** - * Represents a state where the token is available, but there is no current quote available for it. + * Represents a state where the cryptocurrency is available, but there is no current quote available for it. * - * @property amount The amount of the token. + * @property amount The amount of the cryptocurrency. * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. @@ -110,7 +116,7 @@ data class CryptoCurrencyStatus( data class NoQuote( override val amount: BigDecimal, override val hasCurrentNetworkTransactions: Boolean, - override val pendingTransactions: Set, + override val pendingTransactions: Set, override val networkAddress: NetworkAddress?, - ) : Status() + ) : Status(isError = false) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt index e9d5ec7405..ef1c54b04f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -2,16 +2,17 @@ package com.tangem.domain.tokens.model import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal /** * Represents the status of a specific blockchain network. * - * @property networkId The unique identifier of the network for which the status is provided. + * @property network The network for which the status is provided. * @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network. */ data class NetworkStatus( - val networkId: Network.ID, + val network: Network, val value: Status, ) { @@ -44,7 +45,7 @@ data class NetworkStatus( data class Verified( val address: NetworkAddress, val amounts: Map, - val pendingTransactions: Map>, + val pendingTransactions: Map>, ) : Status() /** diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index b2d6e6e71b..d755f0c68b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -4,7 +4,7 @@ import com.tangem.domain.wallets.models.UserWalletId data class TokenActionsState( val walletId: UserWalletId, - val tokenId: String, + val cryptoCurrencyStatus: CryptoCurrencyStatus, val states: List, ) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index cb8885e6b5..07a72dbee4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -58,11 +58,11 @@ internal class CurrenciesStatusesOperations( emit(maybeLoadingCurrenciesStatuses) - val (networksIds, currenciesIds) = getIds(nonEmptyCurrencies) + val (networks, currenciesIds) = getIds(nonEmptyCurrencies) val currenciesFlow = combine( getQuotes(currenciesIds), - getNetworksStatuses(networksIds), + getNetworksStatuses(networks), ) { maybeQuotes, maybeNetworksStatuses -> createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) } @@ -80,6 +80,15 @@ internal class CurrenciesStatusesOperations( return getCurrencyStatusFlow(currency) } + suspend fun getNetworkCoinFlow(networkId: Network.ID): Flow> { + val currency = recover( + block = { getNetworkCoin(networkId) }, + recover = { return flowOf(it.left()) }, + ) + + return getCurrencyStatusFlow(currency) + } + suspend fun getPrimaryCurrencyStatusFlow(): Flow> { val currency = recover( block = { getPrimaryCurrency() }, @@ -90,7 +99,7 @@ internal class CurrenciesStatusesOperations( } private fun getCurrencyStatusFlow(currency: CryptoCurrency): Flow> { - val (networksIds, currenciesIds) = getIds(nonEmptyListOf(currency)) + val (networks, currenciesIds) = getIds(nonEmptyListOf(currency)) val quoteFlow = getQuotes(currenciesIds) .map { maybeQuotes -> @@ -99,15 +108,15 @@ internal class CurrenciesStatusesOperations( } } - val statusFlow = getNetworksStatuses(networksIds) + val statusFlow = getNetworksStatuses(networks) .map { maybeStatuses -> maybeStatuses.map { statuses -> - statuses.singleOrNull { it.networkId == currency.network.id } + statuses.singleOrNull { it.network == currency.network } } } return combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus -> - createStatus(currency, maybeQuote, maybeNetworkStatus) + createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus) } } @@ -126,13 +135,13 @@ internal class CurrenciesStatusesOperations( currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.networkId == currency.network.id } + val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - createStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) + createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) } } - private fun createStatus( + private fun createCurrencyStatus( currency: CryptoCurrency, maybeQuote: Either, maybeNetworkStatus: Either, @@ -145,10 +154,10 @@ internal class CurrenciesStatusesOperations( null } - createStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed) + createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed) } - private fun createStatus( + private fun createCurrencyStatus( currency: CryptoCurrency, quote: Quote?, networkStatus: NetworkStatus?, @@ -177,6 +186,12 @@ internal class CurrenciesStatusesOperations( .bind() } + private suspend fun Raise.getNetworkCoin(networkId: Network.ID): CryptoCurrency { + return Either.catch { currenciesRepository.getNetworkCoin(userWalletId, networkId) } + .mapLeft { Error.DataError(it) } + .bind() + } + private suspend fun Raise.getPrimaryCurrency(): CryptoCurrency { return catch( block = { currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }, @@ -191,7 +206,7 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyQuotes.left()) } } - private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { + private fun getNetworksStatuses(networks: NonEmptySet): Flow>> { return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) .map, Either>> { it.right() } .catch { emit(Error.DataError(it).left()) } @@ -200,17 +215,17 @@ internal class CurrenciesStatusesOperations( private fun getIds( currencies: NonEmptyList, - ): Pair, NonEmptySet> { + ): Pair, NonEmptySet> { val currencyIdToNetworkId = currencies.associate { currency -> - currency.id to currency.network.id + currency.id to currency.network } val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() - val networksIds = currencyIdToNetworkId.values.toNonEmptySetOrNull() + val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } - requireNotNull(networksIds) { "Networks IDs cannot be empty" } + requireNotNull(networks) { "Networks IDs cannot be empty" } - return networksIds to currenciesIds + return networks to currenciesIds } sealed class Error { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 2082010b8d..4e043eaa52 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -26,7 +26,7 @@ internal class CurrencyStatusOperations( } private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { - val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.Unreachable + val amount = status.amounts[currency.id] ?: return CryptoCurrencyStatus.NoAmount val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index ec63a9fa0c..326fceb11f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -22,6 +22,7 @@ internal class TokenListFiatBalanceOperations( } is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, -> { fiatBalance = TokenList.FiatBalance.Failed break diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 97cbf78ede..ce523cb34a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -27,6 +28,16 @@ interface CurrenciesRepository { isSortedByBalance: Boolean, ) + /** + * Add currencies to a specific user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param currencies The currencies which must be added. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) + /** * Removes currency from a specific user wallet. * @@ -37,6 +48,16 @@ interface CurrenciesRepository { */ suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) + /** + * Removes currencies from a specific user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param currencies The currencies which must be removed. + * @throws com.tangem.domain.core.error.DataError.UserWalletError.WrongUserWallet If single-currency user wallet + * ID provided. + */ + suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) + /** * Retrieves the primary cryptocurrency for a specific single-currency user wallet. * @@ -83,6 +104,14 @@ interface CurrenciesRepository { */ suspend fun getMultiCurrencyWalletCurrency(userWalletId: UserWalletId, id: CryptoCurrency.ID): CryptoCurrency + /** + * Get the coin for a specific network. + * + * @param userWalletId The unique identifier of the user wallet. + * @param networkId The unique identifier of the network. + */ + suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin + /** * Determines whether the tokens within a specific multi-currency user wallet are grouped. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt new file mode 100644 index 0000000000..a8c12b480d --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/MarketCryptoCurrencyRepository.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * MarketCryptoCurrencyRepository works with data from Tangem coins backend, CoinMarketCap etc + */ +interface MarketCryptoCurrencyRepository { + + suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Boolean +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index b019c7f3f5..a69acb7bec 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -9,25 +9,16 @@ import kotlinx.coroutines.flow.Flow * Repository for everything related to the blockchain networks * */ interface NetworksRepository { - - /** - * Retrieves the details of the specified blockchain networks, identified by their unique IDs. - * - * @param networksIds The unique identifiers of the networks to be retrieved. - * @return A set of [Network] objects corresponding to the specified network IDs. - */ - fun getNetworks(networksIds: Set): Set - /** * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. * * Loads remote network statuses if they have expired. * * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network IDs which statuses are to be retrieved. + * @param networks A set of network which statuses are to be retrieved. * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. */ - fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> + fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> /** * Retrieves network statuses of specified blockchain networks for a specific user wallet. @@ -35,13 +26,13 @@ interface NetworksRepository { * Loads remote network statuses if they have expired or if [refresh] is `true`. * * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network IDs which statuses are to be retrieved. + * @param networks A set of network which statuses are to be retrieved. * @param refresh A boolean flag indicating whether the data should be refreshed. * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. */ suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, - networks: Set, + networks: Set, refresh: Boolean, ): Set } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index 41b1dda67b..a1f9e62bf9 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -164,6 +164,6 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { isSortedByBalance = flowOf(), ), quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses), + networksRepository = MockNetworksRepository(statuses), ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index b3a5e61a2e..0affccbec6 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -314,6 +314,6 @@ internal class GetTokenListUseCaseTest { isSortedByBalance = isSortedByBalance, ), quotesRepository = MockQuotesRepository(quotes), - networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses), + networksRepository = MockNetworksRepository(statuses), ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 831d0c308b..f29e8b3a74 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -17,6 +17,7 @@ internal object MockNetworks { name = "Network One", isTestnet = false, standardType = Network.StandardType.ERC20, + derivationPath = Network.DerivationPath.None, ) val network2 = Network( @@ -24,6 +25,7 @@ internal object MockNetworks { name = "Network Two", isTestnet = false, standardType = Network.StandardType.ERC20, + derivationPath = Network.DerivationPath.None, ) val network3 = Network( @@ -31,22 +33,21 @@ internal object MockNetworks { name = "Network Three", isTestnet = false, standardType = Network.StandardType.ERC20, + derivationPath = Network.DerivationPath.None, ) - val networks = nonEmptySetOf(network1, network2, network3) - val networkStatus1 = NetworkStatus( - networkId = network1.id, + network = network1, value = NetworkStatus.Unreachable, ) val networkStatus2 = NetworkStatus( - networkId = network2.id, + network = network2, value = NetworkStatus.MissedDerivation, ) val networkStatus3 = NetworkStatus( - networkId = network3.id, + network = network3, value = NetworkStatus.NoAccount( amountToCreateAccount = amountToCreateAccount, address = NetworkAddress.Single(defaultAddress = "mock"), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt index fda72a1b06..7a045b5662 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -7,17 +7,25 @@ internal object MockTokens { val token1 get() = CryptoCurrency.Coin( - id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token1")), + id = ID( + ID.Prefix.COIN_PREFIX, + ID.Body.NetworkId(MockNetworks.network1.id.value), + ID.Suffix.RawID("token1"), + ), network = MockNetworks.network1, name = "Token 1", symbol = "T1", decimals = 8, iconUrl = null, - derivationPath = null, + isCustom = false, ) val token2 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token2")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network1.id.value), + ID.Suffix.RawID("token2"), + ), network = MockNetworks.network1, name = "Token 2", symbol = "T2", @@ -25,11 +33,14 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token3 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network1.id, ID.Suffix.RawID("token3")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network1.id.value), + ID.Suffix.RawID("token3"), + ), network = MockNetworks.network1, name = "Token 3", symbol = "T3", @@ -37,21 +48,28 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token4 get() = CryptoCurrency.Coin( - id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token4")), + id = ID( + ID.Prefix.COIN_PREFIX, + ID.Body.NetworkId(MockNetworks.network2.id.value), + ID.Suffix.RawID("token4"), + ), network = MockNetworks.network2, name = "Token 4", symbol = "T4", decimals = 8, iconUrl = null, - derivationPath = null, + isCustom = false, ) val token5 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token5")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network2.id.value), + ID.Suffix.RawID("token5"), + ), network = MockNetworks.network2, name = "Token 5", symbol = "T5", @@ -59,11 +77,14 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token6 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network2.id, ID.Suffix.RawID("token6")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network2.id.value), + ID.Suffix.RawID("token6"), + ), network = MockNetworks.network2, name = "Token 6", symbol = "T6", @@ -71,21 +92,28 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token7 get() = CryptoCurrency.Coin( - id = ID(ID.Prefix.COIN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token7")), + id = ID( + ID.Prefix.COIN_PREFIX, + ID.Body.NetworkId(MockNetworks.network3.id.value), + ID.Suffix.RawID("token7"), + ), network = MockNetworks.network3, name = "Token 7", symbol = "T7", decimals = 8, iconUrl = null, - derivationPath = null, + isCustom = false, ) val token8 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token8")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network3.id.value), + ID.Suffix.RawID("token8"), + ), network = MockNetworks.network3, name = "Token 8", symbol = "T8", @@ -93,11 +121,14 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token9 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token9")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network3.id.value), + ID.Suffix.RawID("token9"), + ), network = MockNetworks.network3, name = "Token 9", symbol = "T9", @@ -105,11 +136,14 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val token10 get() = CryptoCurrency.Token( - id = ID(ID.Prefix.TOKEN_PREFIX, MockNetworks.network3.id, ID.Suffix.RawID("token10")), + id = ID( + ID.Prefix.TOKEN_PREFIX, + ID.Body.NetworkId(MockNetworks.network3.id.value), + ID.Suffix.RawID("token10"), + ), network = MockNetworks.network3, name = "Token 10", symbol = "T10", @@ -117,7 +151,6 @@ internal object MockTokens { decimals = 8, iconUrl = null, contractAddress = "address", - derivationPath = null, ) val tokens = listOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index d01254b1e7..4dd52cf2a3 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -72,7 +72,7 @@ internal object MockTokensStates { val loadedTokensStates = failedTokenStates.map { status -> val networkStatus = MockNetworks.verifiedNetworksStatuses - .first { it.networkId == status.currency.network.id } + .first { it.network == status.currency.network } val amount = (networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!! val quote = MockQuotes.quotes.first { it.rawCurrencyId == status.currency.id.rawCurrencyId } val fiatAmount = amount * quote.fiatRate @@ -98,7 +98,7 @@ internal object MockTokensStates { hasCurrentNetworkTransactions = false, networkAddress = requireNotNull( value = MockNetworks.verifiedNetworksStatuses - .first { it.networkId == status.currency.network.id } + .first { it.network == status.currency.network } .value as? NetworkStatus.Verified, ).address, ), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index 73e096b201..8238bc90d6 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -40,10 +41,14 @@ internal class MockCurrenciesRepository( isTokensSortedByBalanceAfterSortingApply = isSortedByBalance } + override suspend fun addCurrencies(userWalletId: UserWalletId, currencies: List) = Unit + override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) { removeCurrencyResult.onLeft { throw it } } + override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) = Unit + override suspend fun getMultiCurrencyWalletCurrenciesSync( userWalletId: UserWalletId, refresh: Boolean, @@ -70,6 +75,10 @@ internal class MockCurrenciesRepository( return token } + override suspend fun getNetworkCoin(userWalletId: UserWalletId, networkId: Network.ID): CryptoCurrency.Coin { + TODO("Not yet implemented") + } + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return isGrouped.map { it.getOrElse { e -> throw e } } } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 0209a5e953..6d5026250c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -11,24 +11,19 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map internal class MockNetworksRepository( - private val networks: Either>, private val statuses: Flow>>, ) : NetworksRepository { - override fun getNetworks(networksIds: Set): Set { - return networks.getOrElse { throw it } - } - override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, - networks: Set, + networks: Set, ): Flow> { return statuses.map { it.getOrElse { e -> throw e } } } override suspend fun getNetworkStatusesSync( userWalletId: UserWalletId, - networks: Set, + networks: Set, refresh: Boolean, ): Set { return getNetworkStatusesUpdates(userWalletId, networks).first() diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt index 8ff4c05cff..b912b2111e 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -2,20 +2,16 @@ package com.tangem.domain.txhistory.repository import androidx.paging.PagingData import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.domain.txhistory.models.TxHistoryItem import kotlinx.coroutines.flow.Flow interface TxHistoryRepository { @Throws(TxHistoryStateError::class) - suspend fun getTxHistoryItemsCount(networkId: Network.ID, derivationPath: String?): Int + suspend fun getTxHistoryItemsCount(network: Network): Int @Throws(TxHistoryListError::class) - fun getTxHistoryItems( - networkId: Network.ID, - derivationPath: String?, - pageSize: Int, - ): Flow> + fun getTxHistoryItems(network: Network, pageSize: Int): Flow> } \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt index 172c8c2b3f..df9a8013cc 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -9,10 +9,10 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { - suspend operator fun invoke(networkId: Network.ID, derivationPath: String?): Either { + suspend operator fun invoke(network: Network): Either { return either { catch( - block = { repository.getTxHistoryItemsCount(networkId, derivationPath) }, + block = { repository.getTxHistoryItemsCount(network) }, catch = { throwable -> raise( when (throwable) { diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt index 38fbf4b87e..77ef74e98a 100644 --- a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -4,8 +4,8 @@ import androidx.paging.PagingData import arrow.core.Either import arrow.core.raise.either import com.tangem.domain.tokens.models.Network -import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.repository.TxHistoryRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch @@ -15,13 +15,12 @@ private const val DEFAULT_PAGE_SIZE = 20 class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { operator fun invoke( - networkId: Network.ID, - derivationPath: String?, + network: Network, pageSize: Int = DEFAULT_PAGE_SIZE, ): Either>> { return either { repository - .getTxHistoryItems(networkId = networkId, derivationPath = derivationPath, pageSize = pageSize) + .getTxHistoryItems(network = network, pageSize = pageSize) .catch { raise(TxHistoryListError.DataError(it)) } } } diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 474b10e64e..89f73bcf36 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { // region Domain modules implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) // endregion diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt index af407822fb..710566e799 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWallet.kt @@ -1,34 +1,37 @@ package com.tangem.domain.wallets.models +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse /** * Represents user's wallet which stored in app persistence - * @param name User wallet name - * @param walletId User wallet [UserWalletId] - * @param artworkUrl User wallet card artwork URL - * @param cardsInWallet List of cards IDs assigned with this user's wallet - * @param isMultiCurrency Indicates whether this user wallet can work with more than one currency - * @param scanResponse [ScanResponse] of primary user's wallet card. - * TODO: Replace with [com.tangem.domain.common.CardDTO] - * @property cardId ID of user's wallet primary card - * @property hasAccessCode Indicates if the user's wallet primary card has access code - * @property isLocked Indicates if this primary card has no currency wallets - * */ + * + * @property name User wallet name + * @property walletId User wallet [UserWalletId] + * @property artworkUrl User wallet card artwork URL + * @property cardsInWallet List of cards IDs assigned with this user's wallet. The list will be empty if the wallet + * has been backed up on another device. + * @property isMultiCurrency Indicates whether this user wallet can work with more than one currency + * @property scanResponse [ScanResponse] of primary user's wallet card. + */ data class UserWallet( val name: String, val walletId: UserWalletId, val artworkUrl: String, val cardsInWallet: Set, val isMultiCurrency: Boolean, - val scanResponse: ScanResponse, + val scanResponse: ScanResponse, // TODO: Replace with [com.tangem.domain.models.scan.CardDTO] ) { - val cardId: String - get() = scanResponse.card.cardId - val hasAccessCode: Boolean - get() = scanResponse.card.isAccessCodeSet + /** ID of user's wallet primary card */ + val cardId: String get() = scanResponse.card.cardId - val isLocked: Boolean - get() = scanResponse.card.wallets.isEmpty() + /** Indicates if the user's wallet primary card has access code */ + val hasAccessCode: Boolean get() = scanResponse.card.isAccessCodeSet + + /** Indicates if this primary card has no currency wallets */ + val isLocked: Boolean get() = scanResponse.card.wallets.isEmpty() + + /** Indicated if this primary card is imported */ + val isImported: Boolean get() = scanResponse.card.wallets.any(CardDTO.Wallet::isImported) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt index f292875ed2..7b320009c7 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt @@ -5,5 +5,7 @@ package com.tangem.domain.wallets.models */ sealed interface SaveWalletError { - object CommonError : SaveWalletError + object DataError : SaveWalletError + + data class WalletAlreadySaved(val messageId: Int) : SaveWalletError } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt index 564192c4f8..f255563e7f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt @@ -1,12 +1,17 @@ package com.tangem.domain.wallets.usecase +import arrow.core.raise.catch import com.tangem.domain.tokens.models.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +// TODO: Add tests class GetExploreUrlUseCase(private val walletsManagersFacade: WalletManagersFacade) { - suspend operator fun invoke(userWalletId: UserWalletId, networkId: Network.ID): String { - return walletsManagersFacade.getExploreUrl(userWalletId, networkId) + // FIXME: Handle error + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): String { + return catch({ walletsManagersFacade.getExploreUrl(userWalletId, network) }) { + "" + } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index 89d6fe1405..b56de58375 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -8,7 +8,8 @@ import com.tangem.domain.wallets.models.GetSelectedWalletError import com.tangem.domain.wallets.models.UserWallet /** - * Use case for getting selected wallet + * Use case for getting selected wallet. + * Important! If all wallets is locked, use case returns a error. * * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' * diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 3afdb23663..9998fa02aa 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -5,6 +5,7 @@ import arrow.core.left import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.SaveWalletError import com.tangem.domain.wallets.models.UserWallet @@ -19,9 +20,17 @@ import com.tangem.domain.wallets.models.UserWallet class SaveWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { - requireNotNull(walletsStateHolder.userWalletsListManager).save(userWallet, canOverride) + val userWalletListManager = walletsStateHolder.userWalletsListManager + ?: return SaveWalletError.DataError.left() + + userWalletListManager.save(userWallet, canOverride) .doOnSuccess { return Unit.right() } - .doOnFailure { return SaveWalletError.CommonError.left() } + .doOnFailure { + return when (it) { + is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved(it.messageResId) + else -> SaveWalletError.DataError + }.left() + } return Unit.right() } diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt index 11ee37cff4..921f57b107 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt @@ -1,13 +1,17 @@ package com.tangem.feature.referral.converters +import android.text.format.DateUtils import com.tangem.datasource.api.tangemTech.models.ReferralResponse -import com.tangem.feature.referral.domain.models.DiscountType -import com.tangem.feature.referral.domain.models.ReferralData -import com.tangem.feature.referral.domain.models.ReferralInfo -import com.tangem.feature.referral.domain.models.TokenData +import com.tangem.feature.referral.domain.models.* import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday import com.tangem.utils.safeValueOf +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder import org.joda.time.format.ISODateTimeFormat +import java.util.Locale import javax.inject.Inject class ReferralConverter @Inject constructor() : Converter { @@ -16,6 +20,7 @@ class ReferralConverter @Inject constructor() : Converter { + + /** Example, 2 Aug, 2023 */ + private val dateFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(1) + .appendLiteral(' ') + .appendMonthOfYearShortText() + .appendLiteral(", ") + .appendYear(4, 4) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: ReferralResponse.ExpectedAwards): ExpectedAwards { + return ExpectedAwards( + numberOfWallets = value.numberOfWallets, + expectedAwards = value.list.map { + ExpectedAward( + paymentDate = it.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(), + amount = "${it.amount} ${it.currency}", + ) + }, + ) + } + + 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 { + dateFormatter.print(localDate) + } + } +} + private class TokenConverter : Converter { override fun convert(value: ReferralResponse.Conditions.Award.Token): TokenData { diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 7f8d7470e7..8f8d3a5c42 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -22,7 +22,7 @@ internal class ReferralRepositoryImpl @Inject constructor( override val isDemoMode: Boolean get() = demoModeDatasource.isDemoModeActive - override suspend fun getReferralStatus(walletId: String): ReferralData { + override suspend fun getReferralData(walletId: String): ReferralData { return withContext(coroutineDispatcher.io) { referralConverter.convert( referralApi.getReferralStatus( diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 6280321c45..a62fbcca35 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -20,9 +20,11 @@ internal class ReferralInteractorImpl( get() = repository.isDemoMode override suspend fun getReferralStatus(): ReferralData { - val refStatus = repository.getReferralStatus(userWalletManager.getWalletId()) - saveRefTokens(refStatus.tokens) - return refStatus + val referralData = repository.getReferralData(userWalletManager.getWalletId()) + + saveReferralTokens(referralData.tokens) + + return referralData } override suspend fun startReferral(): ReferralData { @@ -37,7 +39,7 @@ internal class ReferralInteractorImpl( address = publicAddress, ) } else { - error("tokens for ref is empty") + error("Tokens for ref is empty") } } @@ -53,7 +55,7 @@ internal class ReferralInteractorImpl( return derivationPath } - private fun saveRefTokens(tokens: List) { + private fun saveReferralTokens(tokens: List) { tokensForReferral.clear() tokensForReferral.addAll(tokens) } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt index ad770f8c2e..c3b5eaa9ca 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt @@ -7,7 +7,7 @@ interface ReferralRepository { val isDemoMode: Boolean /** Returns data object of [ReferralData] depends on user program status */ - suspend fun getReferralStatus(walletId: String): ReferralData + suspend fun getReferralData(walletId: String): ReferralData /** Starts user referral program */ suspend fun startReferral(walletId: String, networkId: String, tokenId: String, address: String): ReferralData diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt index f5cc061cb0..751af6a116 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt @@ -18,6 +18,7 @@ sealed interface ReferralData { override val tosLink: String, override val tokens: List, val referral: ReferralInfo, + val expectedAwards: ExpectedAwards?, ) : ReferralData /** Data class that used if user is not participant of program */ @@ -47,6 +48,16 @@ data class ReferralInfo( val termsAcceptedAt: DateTime?, ) +data class ExpectedAwards( + val numberOfWallets: Int, + val expectedAwards: List, +) + +data class ExpectedAward( + val paymentDate: String, + val amount: String, +) + enum class DiscountType { PERCENTAGE, VALUE } \ No newline at end of file diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 0649e10587..335cd1af5b 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { /** Other libraries */ implementation(deps.compose.shimmer) implementation(deps.compose.accompanist.webView) + implementation(deps.compose.accompanist.systemUiController) /** DI */ implementation(deps.hilt.android) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt index b04b901b66..2dcb2c65e0 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt @@ -1,25 +1,29 @@ package com.tangem.feature.referral import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.transition.TransitionInflater +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.core.ui.theme.AppThemeModeHolder import com.tangem.feature.referral.presentation.R import com.tangem.feature.referral.router.ReferralRouter import com.tangem.feature.referral.ui.ReferralScreen import com.tangem.feature.referral.viewmodels.ReferralViewModel import dagger.hilt.android.AndroidEntryPoint import java.lang.ref.WeakReference +import javax.inject.Inject @AndroidEntryPoint -class ReferralFragment : Fragment() { +class ReferralFragment : ComposeFragment() { + + @Inject + override lateinit var appThemeModeHolder: AppThemeModeHolder + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { @@ -29,19 +33,17 @@ class ReferralFragment : Fragment() { exitTransition = inflater.inflateTransition(R.transition.fade) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + @Composable + override fun ScreenContent(modifier: Modifier) { viewModel.setRouter(ReferralRouter(fragmentManager = WeakReference(parentFragmentManager))) viewModel.onScreenOpened() - return ComposeView(inflater.context).apply { - isTransitionGroup = true - setContent { - TangemTheme { - ReferralScreen( - modifier = Modifier.systemBarsPadding(), - stateHolder = viewModel.uiState, - ) - } - } - } + + val backgroundColor = TangemTheme.colors.background.secondary + SystemBarsEffect { setSystemBarsColor(backgroundColor) } + + ReferralScreen( + modifier = Modifier.systemBarsPadding(), + stateHolder = viewModel.uiState, + ) } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt index 90e8def3d6..dc7b06e5ef 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -1,5 +1,7 @@ package com.tangem.feature.referral.models +import com.tangem.feature.referral.domain.models.ExpectedAwards + internal data class ReferralStateHolder( val headerState: HeaderState, val referralInfoState: ReferralInfoState, @@ -26,6 +28,7 @@ internal data class ReferralStateHolder( val code: String, val shareLink: String, override val url: String, + val expectedAwards: ExpectedAwards?, ) : ReferralInfoState, ReferralInfoContentState data class NonParticipantContent( diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index 2a1c5b1e86..c803919829 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -43,13 +44,16 @@ internal fun AgreementBottomSheetContent(url: String) { @Composable private fun AgreementHtmlView(url: String) { val state = rememberWebViewState(url) + val isInPreviewMode = LocalInspectionMode.current WebView( state = state, modifier = Modifier.background(TangemTheme.colors.background.secondary), onCreated = { - it.settings.apply { - javaScriptEnabled = false - allowFileAccess = false + if (!isInPreviewMode) { + it.settings.apply { + javaScriptEnabled = false + allowFileAccess = false + } } }, ) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt new file mode 100644 index 0000000000..7085ca0a34 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt @@ -0,0 +1,71 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Surface +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.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme + +@Suppress("LongParameterList") +@Composable +internal fun AwardText( + startText: String, + startTextColor: Color, + startTextStyle: TextStyle, + endText: String, + endTextColor: Color, + endTextStyle: TextStyle, + cornersToRound: CornersToRound, +) { + Surface( + shape = cornersToRound.getShape(), + color = TangemTheme.colors.background.primary, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = startText, + color = startTextColor, + maxLines = 1, + style = startTextStyle, + ) + + Text( + text = endText, + color = endTextColor, + maxLines = 1, + style = endTextStyle, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_AwardItem_Light() { + TangemTheme { + AwardText( + startText = "startText", + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = "endText", + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = CornersToRound.TOP_2, + ) + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt new file mode 100644 index 0000000000..b5070cdb28 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme + +internal enum class CornersToRound { + + ALL_4, + TOP_2, + BOTTOM_2, + ZERO, + ; + + @Suppress("TopLevelComposableFunctions") + @Composable + fun getShape(): RoundedCornerShape { + val radius = TangemTheme.dimens.radius12 + return when (this) { + ALL_4 -> RoundedCornerShape(radius) + TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) + BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + ZERO -> RoundedCornerShape(0.dp) + } + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 169fb81271..911aa890dd 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -2,15 +2,19 @@ package com.tangem.feature.referral.ui import android.content.Context import android.content.Intent +import androidx.compose.animation.* import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -19,14 +23,16 @@ import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview import androidx.core.content.ContextCompat.startActivity -import com.tangem.core.ui.components.PrimaryStartIconButton -import com.tangem.core.ui.components.SmallInfoCard +import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.domain.models.ExpectedAward +import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.presentation.R @Suppress("LongParameterList") @@ -36,6 +42,7 @@ internal fun ParticipateBottomBlock( purchasedWalletCount: Int, code: String, shareLink: String, + expectedAwards: ExpectedAwards?, onAgreementClick: () -> Unit, onShowCopySnackbar: () -> Unit, onCopyClick: () -> Unit, @@ -50,14 +57,6 @@ internal fun ParticipateBottomBlock( .padding(horizontal = TangemTheme.dimens.spacing16), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { - SmallInfoCard( - startText = stringResource(id = R.string.referral_friends_bought_title), - endText = pluralStringResource( - id = R.plurals.referral_wallets_purchased_count, - count = purchasedWalletCount, - purchasedWalletCount, - ), - ) PersonalCodeCard(code = code) AdditionalButtons( code = code, @@ -66,10 +65,187 @@ internal fun ParticipateBottomBlock( onCopyClick = onCopyClick, onShareClick = onShareClick, ) + CounterAndAwards(purchasedWalletCount = purchasedWalletCount, expectedAwards = expectedAwards) AgreementText(firstPartResId = R.string.referral_tos_enroled_prefix, onClick = onAgreementClick) } } +@Composable +private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { + Column { + Counter(purchasedWalletCount, expectedAwards) + + if (expectedAwards != null) { + Awards(expectedAwards) + } else if (purchasedWalletCount != 0) { + EmptyUpcomingPayments() + } + } +} + +@Composable +private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { + val isExpectedAwardsPresent = expectedAwards != null + + AwardText( + startText = stringResource(id = R.string.referral_friends_bought_title), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = pluralStringResource( + id = R.plurals.referral_wallets_purchased_count, + count = purchasedWalletCount, + purchasedWalletCount, + ), + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = if (isExpectedAwardsPresent || purchasedWalletCount != 0) { + CornersToRound.TOP_2 + } else { + CornersToRound.ALL_4 + }, + ) +} + +@Suppress("MagicNumber") +@Composable +private fun Awards(expectedAwards: ExpectedAwards) { + val elementsCountToShowInLessMode = 3 + val isExpanded = remember { mutableStateOf(false) } + + Divider( + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + ) + AwardText( + startText = stringResource(id = R.string.referral_expected_awards), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = pluralStringResource( + id = R.plurals.referral_number_of_wallets, + count = expectedAwards.numberOfWallets, + expectedAwards.numberOfWallets, + ), + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = CornersToRound.ZERO, + ) + + val initialItems = expectedAwards.expectedAwards.take(elementsCountToShowInLessMode) + val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode) + + initialItems.forEachIndexed { index, expectedAward -> + AwardText( + startText = expectedAward.paymentDate, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.subtitle2, + endText = expectedAward.amount, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = if (index == initialItems.size - 1 && extraItems.isEmpty()) { + CornersToRound.BOTTOM_2 + } else { + CornersToRound.ZERO + }, + ) + } + + AnimatedVisibility( + visible = isExpanded.value, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + ExtraItems(extraItems = extraItems) + } + + if (expectedAwards.expectedAwards.size > elementsCountToShowInLessMode) { + LessMoreButton(isExpanded = isExpanded) + } +} + +@Composable +private fun EmptyUpcomingPayments() { + Divider( + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + ) + AwardText( + startText = stringResource(id = R.string.referral_expected_awards), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = "", + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = CornersToRound.BOTTOM_2, + ) +} + +@Composable +private fun LessMoreButton(isExpanded: MutableState) { + Surface( + shape = RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius12, + bottomEnd = TangemTheme.dimens.radius12, + ), + ) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .clickable { isExpanded.value = !isExpanded.value } + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (isExpanded.value) { + stringResource(id = R.string.referral_less) + } else { + stringResource(id = R.string.referral_more) + }, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + + val chevronIcon = if (isExpanded.value) { + painterResource(id = com.tangem.core.ui.R.drawable.ic_chevron_up_24) + } else { + painterResource(id = com.tangem.core.ui.R.drawable.ic_chevron_24) + } + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = chevronIcon, + tint = TangemTheme.colors.text.tertiary, + contentDescription = null, + ) + } + } + } +} + +@Composable +private fun ExtraItems(extraItems: List) { + Column { + extraItems.forEach { expectedAward -> + AwardText( + startText = expectedAward.paymentDate, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.subtitle2, + endText = expectedAward.amount, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = CornersToRound.ZERO, + + ) + } + } +} + @Composable private fun PersonalCodeCard(code: String) { Column( @@ -157,11 +333,28 @@ private fun Context.shareText(text: String) { @Composable private fun Preview_ParticipateBottomBlock_InLightTheme() { TangemTheme(isDark = false) { - Column(Modifier.background(TangemTheme.colors.background.primary)) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( purchasedWalletCount = 3, code = "x4JdK", shareLink = "", + expectedAwards = ExpectedAwards( + numberOfWallets = 3, + expectedAwards = listOf( + ExpectedAward( + amount = "10 USDT", + paymentDate = "Today", + ), + ExpectedAward( + amount = "20 USDT", + paymentDate = "6 Aug 2023", + ), + ExpectedAward( + amount = "30 USDT", + paymentDate = "10 Aug 2023", + ), + ), + ), onAgreementClick = {}, onShowCopySnackbar = {}, onCopyClick = {}, @@ -173,13 +366,64 @@ private fun Preview_ParticipateBottomBlock_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_InDarkTheme() { - TangemTheme(isDark = true) { - Column(Modifier.background(TangemTheme.colors.background.primary)) { +private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() { + TangemTheme(isDark = false) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( purchasedWalletCount = 3, code = "x4JdK", shareLink = "", + expectedAwards = null, + onAgreementClick = {}, + onShowCopySnackbar = {}, + onCopyClick = {}, + onShareClick = {}, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ParticipateBottomBlock_Without_Awards_And_Purchased_Wallets_InLightTheme() { + TangemTheme(isDark = false) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { + ParticipateBottomBlock( + purchasedWalletCount = 0, + code = "x4JdK", + shareLink = "", + expectedAwards = null, + onAgreementClick = {}, + onShowCopySnackbar = {}, + onCopyClick = {}, + onShareClick = {}, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun LessMoreButton_White() { + TangemTheme(isDark = false) { + LessMoreButton( + isExpanded = remember { + mutableStateOf(false) + }, + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ParticipateBottomBlock_Without_Awards_InDarkTheme() { + TangemTheme(isDark = true) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { + ParticipateBottomBlock( + purchasedWalletCount = 3, + code = "x4JdK", + shareLink = "", + expectedAwards = null, onAgreementClick = {}, onShowCopySnackbar = {}, onCopyClick = {}, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 7c3e8b2ddf..6dcec255cf 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign @@ -31,6 +32,8 @@ import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.domain.models.ExpectedAward +import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.models.DemoModeException import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder.* @@ -159,6 +162,7 @@ private fun ReferralInfo( purchasedWalletCount = state.purchasedWalletCount, code = state.code, shareLink = state.shareLink, + expectedAwards = state.expectedAwards, onAgreementClick = onAgreementClick, onShowCopySnackbar = onShowCopySnackbar, onCopyClick = stateHolder.analytics.onCopyClicked, @@ -253,25 +257,47 @@ private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () -> private fun InfoForYou(award: String, networkName: String, address: String? = null) { ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) { Text( - text = buildAnnotatedString { - append(stringResource(id = R.string.referral_point_currencies_description_prefix)) - withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) { - append(" $award ") - } - append( - String.format( - stringResource(id = R.string.referral_point_currencies_description_suffix), - networkName, - if (!address.isNullOrBlank()) " $address" else "", - ), - ) - }, + formatAwardConditionsString( + quantity = award, + network = networkName, + address = if (!address.isNullOrBlank()) " $address" else "", + ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) } } +@Composable +private fun formatAwardConditionsString(quantity: String, network: String, address: String): AnnotatedString { + val rawString = stringResource(R.string.referral_point_currencies_description, quantity, network, address) + + val pattern = Regex("\\^\\^(.*?)\\^\\^") + var startIndex = 0 + val annotatedString = buildAnnotatedString { + pattern.findAll(rawString).forEach { matchResult -> + val index = matchResult.range.first + val matchedValue = matchResult.groups[1]?.value ?: "" + + // appends unformatted part + append(rawString.substring(startIndex, index)) + + // applies style on ^^-wrapped parts + withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) { + append(matchedValue) + } + + // goes to next part + startIndex = matchResult.range.last + 1 + } + + // appends remaining ending if exists + append(rawString.substring(startIndex)) + } + + return annotatedString +} + @Composable private fun InfoForYourFriend(discount: String) { ConditionInfo(title = stringResource(id = R.string.referral_point_discount_title)) { @@ -464,6 +490,7 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() { code = "x4JdK", shareLink = "", url = "", + expectedAwards = null, ), errorSnackbar = null, analytics = Analytics( @@ -492,6 +519,52 @@ private fun Preview_ReferralScreen_Participant_InDarkTheme() { code = "x4JdK", shareLink = "", url = "", + expectedAwards = null, + ), + errorSnackbar = null, + analytics = Analytics( + onAgreementClicked = {}, + onCopyClicked = {}, + onShareClicked = {}, + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() { + TangemTheme(isDark = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + networkName = "Tron", + address = "ma80...zk8q2", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + shareLink = "", + url = "", + expectedAwards = ExpectedAwards( + numberOfWallets = 5, + expectedAwards = listOf( + ExpectedAward( + amount = "10 USDT", + paymentDate = "Today", + ), + ExpectedAward( + amount = "20 USDT", + paymentDate = "6 Aug 2023", + ), + ExpectedAward( + amount = "30 USDT", + paymentDate = "10 Aug 2023", + ), + ), + ), ), errorSnackbar = null, analytics = Analytics( diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt index b2f802e181..82c0d08bb8 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt @@ -36,7 +36,7 @@ internal class ReferralViewModel @Inject constructor( private var referralRouter: ReferralRouter by Delegates.notNull() - private val lastReferralData = mutableStateOf(null) + private var lastReferralData: ReferralData? = null init { loadReferralData() @@ -67,7 +67,7 @@ internal class ReferralViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { referralInteractor.getReferralStatus().apply { - lastReferralData.value = this + lastReferralData = this } } .onSuccess(::showContent) @@ -84,14 +84,13 @@ internal class ReferralViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { referralInteractor.startReferral() } .onSuccess(::showContent) - .onFailure { - if (it is UserCancelledException) { - val lastRefData = lastReferralData.value - if (lastRefData != null) { - showContent(lastRefData) + .onFailure { throwable -> + if (throwable is UserCancelledException) { + lastReferralData?.let { referralData -> + showContent(referralData) } } else { - showErrorSnackbar(it) + showErrorSnackbar(throwable) } } } @@ -130,6 +129,7 @@ internal class ReferralViewModel @Inject constructor( code = referral.promocode, shareLink = referral.shareLink, url = tosLink, + expectedAwards = expectedAwards, ) is ReferralData.NonParticipantData -> ReferralInfoState.NonParticipantContent( award = getAwardValue(), diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 8248b21113..65614c399d 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -9,11 +9,15 @@ plugins { dependencies { /** Core modules */ - implementation(project(":core:analytics")) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(project(":core:featuretoggles")) - implementation(project(":core:utils")) - implementation(project(":core:ui")) + implementation(projects.core.featuretoggles) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.common) + + /** Domain modules **/ + implementation(projects.domain.balanceHiding) /** AndroidX */ implementation(deps.androidx.activity.compose) @@ -30,10 +34,11 @@ dependencies { implementation(deps.compose.constraintLayout) /** Api */ - implementation(project(":features:swap:api")) + implementation(projects.features.swap.api) /** Domain */ - implementation(project(":features:swap:domain")) + implementation(projects.features.swap.domain) + implementation(projects.domain.settings) /** Other libraries */ implementation(deps.compose.shimmer) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index a7ea53771e..73ec9cd110 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -41,6 +41,7 @@ data class SwapCardData( val tokenIconUrl: String, val tokenCurrency: String, val balance: String, + val isBalanceHidden: Boolean, val isNotNativeToken: Boolean, val canSelectAnotherToken: Boolean = false, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index d2d2622426..cc92e13246 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -24,6 +24,11 @@ class SwapFragment : Fragment() { private val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { viewModel.setRouter( SwapRouter( @@ -70,6 +75,11 @@ class SwapFragment : Fragment() { } } + override fun onDestroy() { + lifecycle.removeObserver(viewModel) + super.onDestroy() + } + companion object { const val CURRENCY_BUNDLE_KEY = "swap_currency" const val DERIVATION_PATH = "DERIVATION_STYLE" diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 00e8323623..9324dc4d15 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.Provider import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference @@ -21,7 +22,7 @@ import kotlinx.collections.immutable.toImmutableList * State builder creates a specific states for SwapScreen */ @Suppress("LargeClass") -internal class StateBuilder(val actions: UiActions) { +internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider: Provider) { private val tokensDataConverter = TokensDataConverter(actions.onSearchEntered, actions.onTokenSelected) @@ -39,6 +40,7 @@ internal class StateBuilder(val actions: UiActions) { canSelectAnotherToken = false, isNotNativeToken = initialCurrency.isNonNative(), balance = "", + isBalanceHidden = true, ), receiveCardData = SwapCardData( type = TransactionCardType.ReceiveCard(), @@ -50,6 +52,7 @@ internal class StateBuilder(val actions: UiActions) { balance = "", isNotNativeToken = false, coinId = null, + isBalanceHidden = true, ), fee = FeeState.Loading, networkCurrency = networkInfo.blockchainCurrency, @@ -84,6 +87,7 @@ internal class StateBuilder(val actions: UiActions) { isNotNativeToken = fromToken.isNonNative(), canSelectAnotherToken = canSelectSendToken, balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "", + isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardData( type = TransactionCardType.ReceiveCard(), @@ -95,6 +99,7 @@ internal class StateBuilder(val actions: UiActions) { isNotNativeToken = toToken.isNonNative(), canSelectAnotherToken = canSelectReceiveToken, balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", + isBalanceHidden = isBalanceHiddenProvider(), ), fee = FeeState.Loading, swapButton = SwapButton(enabled = false, loading = true, onClick = {}), @@ -144,6 +149,7 @@ internal class StateBuilder(val actions: UiActions) { tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = quoteModel.fromTokenInfo.tokenWalletBalance, + isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardData( type = TransactionCardType.ReceiveCard(), @@ -155,6 +161,7 @@ internal class StateBuilder(val actions: UiActions) { tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, balance = quoteModel.toTokenInfo.tokenWalletBalance, + isBalanceHidden = isBalanceHiddenProvider(), ), networkCurrency = quoteModel.networkCurrency, warnings = warnings, @@ -192,6 +199,7 @@ internal class StateBuilder(val actions: UiActions) { tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = emptyAmountState.fromTokenWalletBalance, + isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardData( type = TransactionCardType.ReceiveCard(), @@ -203,6 +211,7 @@ internal class StateBuilder(val actions: UiActions) { tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, balance = emptyAmountState.toTokenWalletBalance, + isBalanceHidden = isBalanceHiddenProvider(), ), warnings = emptyList(), fee = FeeState.Empty, @@ -254,6 +263,19 @@ internal class StateBuilder(val actions: UiActions) { ) } + fun updateBalanceHiddenState(uiState: SwapStateHolder, isBalanceHidden: Boolean): SwapStateHolder { + val patchedSendCardData = uiState.sendCardData.copy( + isBalanceHidden = isBalanceHidden, + ) + val patchedReceiveCardData = uiState.receiveCardData.copy( + isBalanceHidden = isBalanceHidden, + ) + return uiState.copy( + sendCardData = patchedSendCardData, + receiveCardData = patchedReceiveCardData, + ) + } + fun updateApproveType(uiState: SwapStateHolder, approveType: ApproveType): SwapStateHolder { return if (uiState.permissionState is SwapPermissionState.ReadyForRequest) { uiState.copy( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index acad315b28..c18c6a8889 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout +import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.states.Item @@ -143,7 +144,11 @@ private fun MainInfo(state: SwapStateHolder) { val priceImpactWarning = state.warnings.filterIsInstance().firstOrNull() TransactionCard( type = state.sendCardData.type, - balance = state.sendCardData.balance, + balance = if (state.sendCardData.isBalanceHidden) { + STARS + } else { + state.sendCardData.balance + }, textFieldValue = state.sendCardData.amountTextFieldValue, amountEquivalent = state.sendCardData.amountEquivalent, tokenIconUrl = state.sendCardData.tokenIconUrl, @@ -161,7 +166,7 @@ private fun MainInfo(state: SwapStateHolder) { val marginCard = TangemTheme.dimens.spacing16 TransactionCard( type = state.receiveCardData.type, - balance = state.receiveCardData.balance, + balance = if (state.receiveCardData.isBalanceHidden) STARS else state.receiveCardData.balance, textFieldValue = state.receiveCardData.amountTextFieldValue, amountEquivalent = state.receiveCardData.amountEquivalent, tokenIconUrl = state.receiveCardData.tokenIconUrl, @@ -326,16 +331,8 @@ private fun SwapWarnings(warnings: List) { @Composable private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> Unit) { + // order is important when { - state.warnings.any { it is SwapWarning.PermissionNeeded } -> { - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.swapping_give_permission), - enabled = true, - showProgress = state.swapButton.loading, - onClick = onPermissionWarningClick, - ) - } state.warnings.any { it is SwapWarning.InsufficientFunds } -> { PrimaryButton( modifier = Modifier.fillMaxWidth(), @@ -345,6 +342,15 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U onClick = state.swapButton.onClick, ) } + state.warnings.any { it is SwapWarning.PermissionNeeded } -> { + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.swapping_give_permission), + enabled = true, + showProgress = state.swapButton.loading, + onClick = onPermissionWarningClick, + ) + } else -> { PrimaryButtonIconEnd( modifier = Modifier.fillMaxWidth(), @@ -370,6 +376,7 @@ private val sendCard = SwapCardData( canSelectAnotherToken = false, balance = "123", coinId = "", + isBalanceHidden = false, ) private val receiveCard = SwapCardData( @@ -382,6 +389,7 @@ private val receiveCard = SwapCardData( canSelectAnotherToken = true, balance = "33333", coinId = "", + isBalanceHidden = false, ) val stateSelectable = SelectableItemsState( diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index e7a96cac0c..2379a8438d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -3,11 +3,12 @@ package com.tangem.feature.swap.viewmodels import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* +import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor @@ -27,8 +28,9 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.runCatching import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.serialization.decodeFromString +import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import timber.log.Timber import java.text.DecimalFormat @@ -37,15 +39,17 @@ import java.util.* import javax.inject.Inject import kotlin.properties.Delegates -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") @HiltViewModel internal class SwapViewModel @Inject constructor( private val swapInteractor: SwapInteractor, private val blockchainInteractor: BlockchainInteractor, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, + private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, savedStateHandle: SavedStateHandle, -) : ViewModel() { +) : ViewModel(), DefaultLifecycleObserver { private val currency = Json.decodeFromString( savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] @@ -53,15 +57,20 @@ internal class SwapViewModel @Inject constructor( ) private val derivationPath = savedStateHandle.get(SwapFragment.DERIVATION_PATH) + private var isBalanceHidden = true + private val stateBuilder = StateBuilder( actions = createUiActions(), + isBalanceHiddenProvider = Provider { isBalanceHidden }, ) + private val inputNumberFormatter = InputNumberFormatter(NumberFormat.getInstance(Locale.getDefault()) as DecimalFormat) private val amountDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler() private var dataState by mutableStateOf(SwapProcessDataState(networkId = currency.networkId)) + var uiState: SwapStateHolder by mutableStateOf( stateBuilder.createInitialLoadingState( initialCurrency = currency, @@ -82,6 +91,24 @@ internal class SwapViewModel @Inject constructor( initTokens(currency) } + override fun onCreate(owner: LifecycleOwner) { + isBalanceHiddenUseCase() + .flowWithLifecycle(owner.lifecycle) + .onEach { hidden -> + isBalanceHidden = hidden + withContext(dispatchers.main) { + uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) + } + } + .launchIn(viewModelScope) + + viewModelScope.launch { + listenToFlipsUseCase() + .flowWithLifecycle(owner.lifecycle) + .collect() + } + } + override fun onCleared() { singleTaskScheduler.cancelTask() super.onCleared() diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 1957acae29..704e1f8bc9 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(deps.compose.paging) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) implementation(deps.arrow.core) implementation(deps.jodatime) @@ -52,12 +53,14 @@ dependencies { implementation(projects.domain.appCurrency.models) implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.settings) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.balanceHiding) /** Feature Apis */ implementation(projects.features.tokendetails.api) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index b4af868ab8..e3b2eca4d0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -2,18 +2,33 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow internal object TokenDetailsPreviewData { - val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(onBackClick = {}, onMoreClick = {}) + val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig( + onBackClick = {}, + tokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( + persistentListOf( + TokenDetailsAppBarMenuConfig.MenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ), + ), + ) val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState( name = "Stellar (XLM) with long name test", @@ -52,20 +67,31 @@ internal object TokenDetailsPreviewData { actionButtons = actionButtons, fiatBalance = "123,00$", cryptoBalance = "866,96 USDT", + isBalanceHidden = false, ) val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons) private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT") + private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ) + val tokenDetailsState = TokenDetailsState( topAppBarConfig = tokenDetailsTopAppBarConfig, tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, + notifications = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions {}, ), ), + dialogConfig = null, + pendingTxs = persistentListOf(), + pullToRefreshConfig = pullToRefreshConfig, + bottomSheetConfig = null, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt new file mode 100644 index 0000000000..4926ba0b4a --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList) { + data class MenuItem( + val title: TextReference, + val textColorProvider: @Composable () -> Color, + val onClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt index 2f066dae01..9bcf4d84c3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -15,6 +15,7 @@ internal sealed class TokenDetailsBalanceBlockState { override val actionButtons: ImmutableList, val fiatBalance: String, val cryptoBalance: String, + val isBalanceHidden: Boolean, ) : TokenDetailsBalanceBlockState() data class Error( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 7006b6c4ef..cdf8590f46 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -1,12 +1,24 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList internal data class TokenDetailsState( val topAppBarConfig: TokenDetailsTopAppBarConfig, val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, + val notifications: ImmutableList, + val pendingTxs: PersistentList, val txHistoryState: TxHistoryState, + val dialogConfig: TokenDetailsDialogConfig?, + val pullToRefreshConfig: TokenDetailsPullToRefreshConfig, + val bottomSheetConfig: TangemBottomSheetConfig?, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt index 8ed33f724b..88ecf53046 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt @@ -1,6 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state -data class TokenDetailsTopAppBarConfig( +internal data class TokenDetailsTopAppBarConfig( val onBackClick: () -> Unit, - val onMoreClick: () -> Unit, + val tokenDetailsAppBarMenuConfig: TokenDetailsAppBarMenuConfig, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt index ac4c80561c..f6bec38a44 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -2,7 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.annotation.DrawableRes -data class TokenInfoBlockState( +internal data class TokenInfoBlockState( val name: String, val iconUrl: String, val currency: Currency, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt new file mode 100644 index 0000000000..02fc3679f3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt @@ -0,0 +1,81 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.tokendetails.impl.R + +/** + * Wallet bottom sheet config + * + * @property isShow flag that determine if bottom sheet is shown + * @property onDismissRequest lambda be invoked when bottom sheet is dismissed + * @property content content config + */ +internal data class TokenDetailsDialogConfig( + val isShow: Boolean, + val onDismissRequest: () -> Unit, + val content: DialogContentConfig, +) { + + sealed class DialogContentConfig { + + abstract val title: TextReference + abstract val message: TextReference + abstract val confirmButtonConfig: ButtonConfig + abstract val cancelButtonConfig: ButtonConfig? + + data class ButtonConfig( + val text: TextReference, + val onClick: () -> Unit, + val warning: Boolean = false, + ) + + data class ConfirmHideConfig( + val currencySymbol: String, + val onConfirmClick: () -> Unit, + val onCancelClick: () -> Unit, + ) : DialogContentConfig() { + override val title: TextReference = TextReference.Res( + id = R.string.token_details_hide_alert_title, + formatArgs = wrappedList(currencySymbol), + ) + + override val message: TextReference = TextReference.Res(R.string.token_details_hide_alert_message) + + override val cancelButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_cancel), + onClick = onCancelClick, + ) + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.token_details_hide_alert_hide), + onClick = onConfirmClick, + warning = true, + ) + } + + data class HasLinkedTokensConfig( + val currencySymbol: String, + val networkName: String, + val onConfirmClick: () -> Unit, + ) : DialogContentConfig() { + override val title: TextReference = TextReference.Res( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = wrappedList(currencySymbol), + ) + + override val message: TextReference = TextReference.Res( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = wrappedList(currencySymbol, networkName), + ) + + override val cancelButtonConfig: ButtonConfig? + get() = null + + override val confirmButtonConfig: ButtonConfig = ButtonConfig( + text = TextReference.Res(R.string.common_ok), + onClick = onConfirmClick, + ) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt new file mode 100644 index 0000000000..d42022a22d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning +import com.tangem.features.tokendetails.impl.R + +// TODO: Finalize notification strings [REDACTED_JIRA] +@Immutable +sealed class TokenDetailsNotification(open val config: NotificationConfig) { + + data class RentInfo( + private val rentInfo: CryptoCurrencyWarning.Rent, + private val onCloseClick: () -> Unit, + ) : TokenDetailsNotification( + config = NotificationConfig( + title = TextReference.Res(R.string.send_network_fee_title), + subtitle = TextReference.Res( + id = R.string.solana_rent_warning, + formatArgs = wrappedList(rentInfo.rent, rentInfo.exemptionAmount), + ), + iconResId = R.drawable.img_attention_20, + onCloseClick = onCloseClick, + ), + ) + + data class ExistentialDeposit( + private val existentialInfo: CryptoCurrencyWarning.ExistentialDeposit, + private val onCloseClick: () -> Unit, + ) : TokenDetailsNotification( + config = NotificationConfig( + title = TextReference.Str("Existential Deposit"), + subtitle = TextReference.Res( + id = R.string.warning_existential_deposit_message, + formatArgs = wrappedList(existentialInfo.currencyName, existentialInfo.edStringValueWithSymbol), + ), + iconResId = R.drawable.img_attention_20, + onCloseClick = onCloseClick, + ), + ) + + data class NetworkFee( + private val feeInfo: CryptoCurrencyWarning.BalanceNotEnoughForFee, + private val onBuyClick: () -> Unit, + ) : TokenDetailsNotification( + config = NotificationConfig( + title = TextReference.Res( + id = R.string.notification_title_not_enough_funds, + formatArgs = wrappedList(feeInfo.blockchainFullName), + ), + subtitle = TextReference.Res( + id = R.string.token_details_send_blocked_fee_format, + formatArgs = wrappedList( + feeInfo.currency.name, + feeInfo.blockchainFullName, + feeInfo.currency.name, + feeInfo.blockchainFullName, + feeInfo.blockchainSymbol, + ), + ), + iconResId = feeInfo.currency.networkIconResId, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = TextReference.Res(R.string.common_buy), + onClick = onBuyClick, + ), + ), + ) + + object NetworksUnreachable : TokenDetailsNotification( + config = NotificationConfig( + title = TextReference.Str("Some networks are unreachable"), + subtitle = TextReference.Str("The problem is on the crypto-network side. It will be fixed soon."), + iconResId = R.drawable.img_attention_20, + ), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt new file mode 100644 index 0000000000..5015995638 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsPullToRefreshConfig.kt @@ -0,0 +1,3 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.components + +data class TokenDetailsPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 19c2d0c8b4..44401e8414 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -10,21 +10,36 @@ import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryToTransactionStateConverter import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, + private val isBalanceHiddenProvider: Provider, + private val symbol: String, + private val decimals: Int, ) : Converter, TokenDetailsState> { + private val txHistoryItemConverter by lazy { + TokenDetailsTxHistoryToTransactionStateConverter(symbol, decimals) + } + override fun convert(value: Either): TokenDetailsState { return value.fold(ifLeft = { convertError() }, ifRight = ::convert) } private fun convertError(): TokenDetailsState { - // TODO: [REDACTED_JIRA] - return currentStateProvider() + val state = currentStateProvider() + return state.copy( + tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(state.tokenBalanceBlockState.actionButtons), + marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencyName), + notifications = persistentListOf(TokenDetailsNotification.NetworksUnreachable), + ) } private fun convert(status: CryptoCurrencyStatus): TokenDetailsState { @@ -33,6 +48,7 @@ internal class TokenDetailsLoadedBalanceConverter( return state.copy( tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status), marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), + pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList(), ) } @@ -48,6 +64,7 @@ internal class TokenDetailsLoadedBalanceConverter( actionButtons = currentState.actionButtons, fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), cryptoBalance = formatCryptoAmount(status), + isBalanceHidden = isBalanceHiddenProvider(), ) } is CryptoCurrencyStatus.Loading -> { @@ -56,7 +73,7 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Custom, - // TODO: [REDACTED_JIRA] + is CryptoCurrencyStatus.NoAmount, is CryptoCurrencyStatus.Unreachable, -> { TokenDetailsBalanceBlockState.Error(currentState.actionButtons) @@ -80,6 +97,7 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.NoAmount, is CryptoCurrencyStatus.Unreachable, -> MarketPriceBlockState.Error(currencyName) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt new file mode 100644 index 0000000000..385592d737 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.removeBy +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal class TokenDetailsNotificationConverter( + private val clickIntents: TokenDetailsClickIntents, +) : Converter, ImmutableList> { + + override fun convert(value: Set): ImmutableList { + return value.map(::mapToNotification).toImmutableList() + } + + fun removeExistentialDeposit(currentState: TokenDetailsState): ImmutableList { + val newNotifications = currentState.notifications.toMutableList() + newNotifications.removeBy { it is TokenDetailsNotification.ExistentialDeposit } + return newNotifications.toImmutableList() + } + + fun removeRentInfo(currentState: TokenDetailsState): ImmutableList { + val newNotifications = currentState.notifications.toMutableList() + newNotifications.removeBy { it is TokenDetailsNotification.RentInfo } + return newNotifications.toImmutableList() + } + + private fun mapToNotification(warning: CryptoCurrencyWarning): TokenDetailsNotification { + return when (warning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> TokenDetailsNotification.NetworkFee( + feeInfo = warning, + onBuyClick = clickIntents::onBuyClick, + ) + is CryptoCurrencyWarning.ExistentialDeposit -> TokenDetailsNotification.ExistentialDeposit( + existentialInfo = warning, + onCloseClick = clickIntents::onCloseExistentialDepositNotification, + ) + is CryptoCurrencyWarning.Rent -> TokenDetailsNotification.RentInfo( + rentInfo = warning, + onCloseClick = clickIntents::onCloseRentInfoNotification, + ) + CryptoCurrencyWarning.SomeNetworksUnreachable -> TokenDetailsNotification.NetworksUnreachable + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt new file mode 100644 index 0000000000..23c045fe3f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsRefreshStateConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.common.Provider +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.utils.converter.Converter + +internal class TokenDetailsRefreshStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: Boolean): TokenDetailsState { + val state = currentStateProvider() + return state.createPullToRefresh(value) + } + + private fun TokenDetailsState.createPullToRefresh(isRefreshing: Boolean): TokenDetailsState { + return copy(pullToRefreshConfig = pullToRefreshConfig.copy(isRefreshing = isRefreshing)) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index a02f4cc5fe..3d5bc47a22 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -2,15 +2,16 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.extensions.iconResId +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.tokens.models.CryptoCurrency -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsPullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -24,7 +25,7 @@ internal class TokenDetailsSkeletonStateConverter( return TokenDetailsState( topAppBarConfig = TokenDetailsTopAppBarConfig( onBackClick = clickIntents::onBackClick, - onMoreClick = clickIntents::onMoreClick, + tokenDetailsAppBarMenuConfig = createMenu(), ), tokenInfoBlockState = TokenInfoBlockState( name = value.cryptoCurrency.name, @@ -34,7 +35,7 @@ internal class TokenDetailsSkeletonStateConverter( is CryptoCurrency.Token -> TokenInfoBlockState.Currency.Token( networkName = currency.network.standardType.name, blockchainName = currency.network.name, - networkIcon = currency.iconResId, + networkIcon = currency.networkIconResId, ) }, ), @@ -42,14 +43,29 @@ internal class TokenDetailsSkeletonStateConverter( actionButtons = createButtons(), ), marketPriceBlockState = MarketPriceBlockState.Loading(value.cryptoCurrency.name), + notifications = persistentListOf(), + pendingTxs = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), + dialogConfig = null, + pullToRefreshConfig = createPullToRefresh(), + bottomSheetConfig = null, ) } + private fun createMenu(): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( + items = persistentListOf( + TokenDetailsAppBarMenuConfig.MenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = clickIntents::onHideClick, + ), + ), + ) + private fun createButtons(): ImmutableList { return persistentListOf( TokenDetailsActionButton.Buy(enabled = false, onClick = {}), @@ -60,5 +76,10 @@ internal class TokenDetailsSkeletonStateConverter( ) } + private fun createPullToRefresh(): TokenDetailsPullToRefreshConfig = TokenDetailsPullToRefreshConfig( + isRefreshing = false, + onRefresh = clickIntents::onRefreshSwipe, + ) + data class SkeletonModel(val cryptoCurrency: CryptoCurrency) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index e0ad501d38..874f8bc7e9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -2,16 +2,23 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import androidx.paging.PagingData import arrow.core.Either +import com.tangem.blockchain.common.address.Address import com.tangem.common.Provider +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig 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.tokens.model.TokenActionsState import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.domain.tokens.models.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents @@ -20,6 +27,7 @@ import kotlinx.coroutines.flow.Flow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, + private val isBalanceHiddenProvider: Provider, private val clickIntents: TokenDetailsClickIntents, symbol: String, decimals: Int, @@ -29,10 +37,17 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter(clickIntents = clickIntents) } + private val notificationConverter by lazy { + TokenDetailsNotificationConverter(clickIntents = clickIntents) + } + private val tokenDetailsLoadedBalanceConverter by lazy { TokenDetailsLoadedBalanceConverter( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, + isBalanceHiddenProvider = isBalanceHiddenProvider, + symbol = symbol, + decimals = decimals, ) } @@ -56,6 +71,12 @@ internal class TokenDetailsStateFactory( ) } + private val refreshStateConverter by lazy { + TokenDetailsRefreshStateConverter( + currentStateProvider = currentStateProvider, + ) + } + fun getInitialState(cryptoCurrency: CryptoCurrency): TokenDetailsState { return skeletonStateConverter.convert( TokenDetailsSkeletonStateConverter.SkeletonModel(cryptoCurrency = cryptoCurrency), @@ -81,4 +102,99 @@ internal class TokenDetailsStateFactory( ): TokenDetailsState { return loadedTxHistoryConverter.convert(txHistoryEither) } + + fun getStateWithClosedDialog(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(dialogConfig = state.dialogConfig?.copy(isShow = false)) + } + + fun getStateWithConfirmHideTokenDialog(currency: CryptoCurrency): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmHideConfig( + currencySymbol = currency.symbol, + onConfirmClick = clickIntents::onHideConfirmed, + onCancelClick = clickIntents::onDismissDialog, + ), + ), + ) + } + + fun getStateWithLinkedTokensDialog(currency: CryptoCurrency): TokenDetailsState { + return currentStateProvider().copy( + dialogConfig = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.HasLinkedTokensConfig( + currencySymbol = currency.symbol, + networkName = currency.network.name, + onConfirmClick = clickIntents::onDismissDialog, + ), + ), + ) + } + + fun getRefreshingState(): TokenDetailsState { + return refreshStateConverter.convert(true) + } + + fun getRefreshedState(): TokenDetailsState { + return refreshStateConverter.convert(false) + } + + fun getStateWithReceiveBottomSheet(currency: CryptoCurrency, addresses: List
): TokenDetailsState { + return currentStateProvider().copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = clickIntents::onDismissBottomSheet, + content = TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = addresses.map { + AddressModel( + value = it.value, + type = AddressModel.Type.valueOf(it.type.name), + ) + }, + ), + ), + ) + } + + fun getStateWithClosedBottomSheet(): TokenDetailsState { + val state = currentStateProvider() + return state.copy( + bottomSheetConfig = state.bottomSheetConfig?.copy(isShow = false), + ) + } + + fun getStateWithUpdatedHidden(isBalanceHidden: Boolean): TokenDetailsState { + val currentState = currentStateProvider() + val possibleTokenBalanceBlockState = currentState.tokenBalanceBlockState as? + TokenDetailsBalanceBlockState.Content + + possibleTokenBalanceBlockState?.let { + return currentState.copy( + tokenBalanceBlockState = possibleTokenBalanceBlockState.copy(isBalanceHidden = isBalanceHidden), + ) + } ?: return currentState + } + + fun getStateWithNotifications(warnings: Set): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.convert(warnings)) + } + + fun getStateWithRemovedExistentialNotification(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.removeExistentialDeposit(state)) + } + + fun getStateWithRemovedRentNotification(): TokenDetailsState { + val state = currentStateProvider() + return state.copy(notifications = notificationConverter.removeRentInfo(state)) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt index f9146ee1d3..4c5fbc9fa2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadedTxHistoryConverter.kt @@ -3,17 +3,17 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider -import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError 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 kotlinx.coroutines.flow.Flow internal class TokenDetailsLoadedTxHistoryConverter( private val currentStateProvider: Provider, - private val clickIntents: TxHistoryClickIntents, + private val clickIntents: TokenDetailsClickIntents, symbol: String, decimals: Int, ) : Converter>>, TokenDetailsState> { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt index 9f98c1ccf3..599d55f418 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsLoadingTxHistoryConverter.kt @@ -3,17 +3,17 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider -import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.txhistory.models.TxHistoryStateError 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 kotlinx.coroutines.flow.update internal class TokenDetailsLoadingTxHistoryConverter( private val currentStateProvider: Provider, - private val clickIntents: TxHistoryClickIntents, + private val clickIntents: TokenDetailsClickIntents, ) : Converter, TokenDetailsState> { override fun convert(value: Either): TokenDetailsState { @@ -23,12 +23,8 @@ internal class TokenDetailsLoadingTxHistoryConverter( private fun convertError(error: TxHistoryStateError): TokenDetailsState { return currentStateProvider().copy( txHistoryState = when (error) { - is TxHistoryStateError.EmptyTxHistories -> { - TxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) - } - is TxHistoryStateError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) - } + is TxHistoryStateError.EmptyTxHistories -> TxHistoryState.Empty + is TxHistoryStateError.DataError -> TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) is TxHistoryStateError.TxHistoryNotImplemented -> { TxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) } 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 a8f2d95f25..d1de3a2c01 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 @@ -3,13 +3,13 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. import android.text.format.DateUtils import androidx.paging.* import com.tangem.common.Provider -import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents 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.extensions.TextReference 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.features.tokendetails.impl.R import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isToday @@ -18,7 +18,10 @@ import com.tangem.utils.toBriefAddressFormat import com.tangem.utils.toFormattedCurrencyString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.format.DateTimeFormatterBuilder @@ -29,7 +32,7 @@ internal class TokenDetailsTxHistoryItemFlowConverter( private val currentStateProvider: Provider, private val symbol: String, private val decimals: Int, - private val clickIntents: TxHistoryClickIntents, + private val clickIntents: TokenDetailsClickIntents, ) : Converter>, TxHistoryState> { /** Example, 2 Aug, 2023 */ diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt new file mode 100644 index 0000000000..e0e714eea4 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryToTransactionStateConverter.kt @@ -0,0 +1,94 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory + +import com.tangem.core.ui.components.transactions.state.TransactionState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.converter.Converter +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder +import java.math.BigDecimal +import java.util.Locale + +internal class TokenDetailsTxHistoryToTransactionStateConverter( + private val symbol: String, + private val decimals: Int, +) : Converter { + + /** Example, 13:35 */ + private val timeFormatter by lazy { + DateTimeFormatterBuilder() + .appendHourOfDay(1) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: TxHistoryItem): TransactionState { + return when (value.type) { + TxHistoryItem.TransactionType.Transfer -> { + when (val direction = value.direction) { + is TxHistoryItem.TransactionDirection.Incoming -> { + createIncomingTransferTransaction(value, direction) + } + is TxHistoryItem.TransactionDirection.Outgoing -> { + createOutgoingTransferTransaction(value, direction) + } + } + } + } + } + + private fun createIncomingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Incoming, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())), + ) + } + } + + private fun createOutgoingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Outgoing, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + txHash = item.txHash, + address = direction.extractAddress(), + amount = item.amount.toCryptoCurrencyFormat(), + timestamp = timeFormatter.print(DateTime(item.timestampInMillis, DateTimeZone.getDefault())), + ) + } + } + + private fun BigDecimal.toCryptoCurrencyFormat(): String { + return toFormattedCurrencyString(currency = symbol, decimals = decimals) + } + + private fun TxHistoryItem.TransactionDirection.extractAddress(): TextReference = when (val addr = address) { + TxHistoryItem.Address.Multiple -> TextReference.Res(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.Address.Single -> TextReference.Str(addr.rawAddress.toBriefAddressFormat()) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a54eef8db7..9ab8a4487f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,30 +1,53 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.util.fastForEach import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.transactions.Transaction +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.txHistoryItems import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock +import kotlinx.collections.immutable.PersistentList +// TODO: Split to blocks [REDACTED_JIRA] +@Suppress("LongMethod") +@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @Composable internal fun TokenDetailsScreen(state: TokenDetailsState) { Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> + val pullRefreshState = rememberPullRefreshState( + refreshing = state.pullToRefreshConfig.isRefreshing, + onRefresh = state.pullToRefreshConfig.onRefresh, + ) + val txHistoryItems = if (state.txHistoryState is TxHistoryState.Content) { state.txHistoryState.contentItems.collectAsLazyPagingItems() } else { @@ -35,27 +58,74 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { val itemModifier = Modifier .padding(top = betweenItemsPadding) .padding(horizontal = horizontalPadding) - LazyColumn( + + Box( modifier = Modifier - .padding(paddingValues = scaffoldPaddings) - .fillMaxSize(), + .padding(scaffoldPaddings) + .pullRefresh(pullRefreshState), ) { - item { - TokenInfoBlock( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing4) - .padding(horizontal = horizontalPadding), - state = state.tokenInfoBlockState, + LazyColumn( + modifier = Modifier + .fillMaxSize(), + ) { + item { + TokenInfoBlock( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = horizontalPadding), + state = state.tokenInfoBlockState, + ) + } + item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } + items( + items = state.notifications, + key = { it.config::class.java }, + contentType = { it.config::class.java }, + itemContent = { Notification(config = it.config, modifier = itemModifier.animateItemPlacement()) }, ) + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, + ) + if (state.txHistoryState is TxHistoryState.NotSupported && state.pendingTxs.isNotEmpty()) { + item { + PendingTxsBlock( + pendingTxs = state.pendingTxs, + modifier = itemModifier, + ) + } + } + txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } - item { TokenDetailsBalanceBlock(modifier = itemModifier, state = state.tokenBalanceBlockState) } - item( - key = MarketPriceBlockState::class.java, - contentType = MarketPriceBlockState::class.java, - content = { MarketPriceBlock(modifier = itemModifier, state = state.marketPriceBlockState) }, + + PullRefreshIndicator( + modifier = Modifier.align(Alignment.TopCenter), + refreshing = state.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, ) - txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems) } + + TokenDetailsDialogs(state = state) + + state.bottomSheetConfig?.let { config -> + TokenReceiveBottomSheet( + config = config, + ) + } + } +} + +@Composable +private fun PendingTxsBlock(pendingTxs: PersistentList, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + horizontalAlignment = Alignment.Start, + ) { + pendingTxs.fastForEach { Transaction(state = it) } } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt new file mode 100644 index 0000000000..fa6ff8aca3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt @@ -0,0 +1,229 @@ +@file:Suppress("TopLevelPropertyNaming") +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.animation.core.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties + +/** + * Just copy paste [DropdownMenu] from material3 with deleting vertical paddings. + */ +@Composable +internal fun TangemDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + offset: DpOffset = DpOffset(0.dp, 0.dp), + properties: PopupProperties = PopupProperties(focusable = true), + content: @Composable ColumnScope.() -> Unit, +) { + val expandedStates = remember { MutableTransitionState(false) } + expandedStates.targetState = expanded + + if (expandedStates.currentState || expandedStates.targetState) { + val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) } + val density = LocalDensity.current + val popupPositionProvider = DropdownMenuPositionProvider( + offset, + density, + ) { parentBounds, menuBounds -> + transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds) + } + + Popup( + onDismissRequest = onDismissRequest, + popupPositionProvider = popupPositionProvider, + properties = properties, + ) { + DropdownMenuContent( + expandedStates = expandedStates, + transformOriginState = transformOriginState, + modifier = modifier, + content = content, + ) + } + } +} + +private const val InTransitionDuration = 120 +private const val OutTransitionDuration = 75 + +@Suppress("ReusedModifierInstance", "MagicNumber") +@Composable +private fun DropdownMenuContent( + expandedStates: MutableTransitionState, + transformOriginState: MutableState, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + // Menu open/close animation. + val transition = updateTransition(expandedStates, "DropDownMenu") + + val scale by transition.animateFloat( + transitionSpec = { + if (false isTransitioningTo true) { + // Dismissed to expanded + tween( + durationMillis = InTransitionDuration, + easing = LinearOutSlowInEasing, + ) + } else { + // Expanded to dismissed. + tween( + durationMillis = 1, + delayMillis = OutTransitionDuration - 1, + ) + } + }, + label = "", + ) { + if (it) { + // Menu is expanded. + 1f + } else { + // Menu is dismissed. + 0.8f + } + } + + val alpha by transition.animateFloat( + transitionSpec = { + if (false isTransitioningTo true) { + // Dismissed to expanded + tween(durationMillis = 30) + } else { + // Expanded to dismissed. + tween(durationMillis = OutTransitionDuration) + } + }, + label = "", + ) { + if (it) { + // Menu is expanded. + 1f + } else { + // Menu is dismissed. + 0f + } + } + Card( + modifier = Modifier.graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + transformOrigin = transformOriginState.value + }, + elevation = CardDefaults.cardElevation(), + ) { + Column( + modifier = modifier + .width(IntrinsicSize.Max) + .verticalScroll(rememberScrollState()), + content = content, + ) + } +} + +private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin { + val pivotX = when { + menuBounds.left >= parentBounds.right -> 0f + menuBounds.right <= parentBounds.left -> 1f + menuBounds.width == 0 -> 0f + else -> { + val intersectionCenter = + ( + kotlin.math.max(parentBounds.left, menuBounds.left) + + kotlin.math.min(parentBounds.right, menuBounds.right) + ) / 2 + (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width + } + } + val pivotY = when { + menuBounds.top >= parentBounds.bottom -> 0f + menuBounds.bottom <= parentBounds.top -> 1f + menuBounds.height == 0 -> 0f + else -> { + val intersectionCenter = + ( + kotlin.math.max(parentBounds.top, menuBounds.top) + + kotlin.math.min(parentBounds.bottom, menuBounds.bottom) + ) / 2 + (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height + } + } + return TransformOrigin(pivotX, pivotY) +} + +private val MenuVerticalMargin = 48.dp + +@Immutable +internal data class DropdownMenuPositionProvider( + val contentOffset: DpOffset, + val density: Density, + val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }, +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + // The min margin above and below the menu, relative to the screen. + val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() } + // The content offset specified using the dropdown offset parameter. + val contentOffsetX = with(density) { contentOffset.x.roundToPx() } + val contentOffsetY = with(density) { contentOffset.y.roundToPx() } + + // Compute horizontal position. + val toRight = anchorBounds.left + contentOffsetX + val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width + val toDisplayRight = windowSize.width - popupContentSize.width + val toDisplayLeft = 0 + val x = if (layoutDirection == LayoutDirection.Ltr) { + sequenceOf( + toRight, + toLeft, + // If the anchor gets outside of the window on the left, we want to position + // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight. + if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft, + ) + } else { + sequenceOf( + toLeft, + toRight, + // If the anchor gets outside of the window on the right, we want to position + // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft. + if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight, + ) + }.firstOrNull { + it >= 0 && it + popupContentSize.width <= windowSize.width + } ?: toLeft + + // Compute vertical position. + val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin) + val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height + val toCenter = anchorBounds.top - popupContentSize.height / 2 + val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin + val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull { + it >= verticalMargin && + it + popupContentSize.height <= windowSize.height - verticalMargin + } ?: toTop + + onPositionCalculated( + anchorBounds, + IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height), + ) + return IntOffset(x, y) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index b1b9ac832a..9e2b59dc9e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -72,7 +72,7 @@ private fun FiatBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = state.fiatBalance, + text = if (state.isBalanceHidden) DOTS else state.fiatBalance, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -96,7 +96,7 @@ private fun CryptoBalance(state: TokenDetailsBalanceBlockState, modifier: Modifi ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = state.cryptoBalance, + text = if (state.isBalanceHidden) DOTS else state.cryptoBalance, style = TangemTheme.typography.caption, color = TangemTheme.colors.text.primary1, ) @@ -135,4 +135,6 @@ private class TokenDetailsBalanceBlockStateProvider : CollectionPreviewParameter TokenDetailsPreviewData.balanceContent, TokenDetailsPreviewData.balanceError, ), -) \ No newline at end of file +) + +const val DOTS = "***" \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt new file mode 100644 index 0000000000..174eb35567 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt @@ -0,0 +1,37 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig + +@Composable +internal fun TokenDetailsDialogs(state: TokenDetailsState) { + val dialogConfig = state.dialogConfig + if (dialogConfig != null && dialogConfig.isShow) { + TokenDetailsDialog(config = dialogConfig) + } +} + +@Composable +private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { + BasicDialog( + message = config.content.message.resolveReference(), + confirmButton = DialogButton( + title = config.content.confirmButtonConfig.text.resolveReference(), + warning = config.content.confirmButtonConfig.warning, + onClick = config.content.confirmButtonConfig.onClick, + ), + onDismissDialog = config.onDismissRequest, + title = config.content.title.resolveReference(), + dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig -> + DialogButton( + title = cancelButtonConfig.text.resolveReference(), + warning = cancelButtonConfig.warning, + onClick = cancelButtonConfig.onClick, + ) + }, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt index a7901787f0..b5e0839679 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -1,17 +1,31 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.features.tokendetails.impl.R +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig +import com.tangem.features.tokendetails.impl.R @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { + var showDropdownMenu by rememberSaveable { mutableStateOf(false) } TopAppBar( navigationIcon = { IconButton(onClick = config.onBackClick) { @@ -24,13 +38,28 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { }, title = {}, actions = { - IconButton(onClick = config.onMoreClick) { + IconButton(onClick = { showDropdownMenu = true }) { Icon( painter = painterResource(id = R.drawable.ic_more_vertical_24), tint = TangemTheme.colors.icon.primary1, contentDescription = "More", ) } + + TangemDropdownMenu( + expanded = showDropdownMenu, + modifier = Modifier.background(TangemTheme.colors.background.primary), + onDismissRequest = { showDropdownMenu = false }, + offset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)), + content = { + config.tokenDetailsAppBarMenuConfig.items.fastForEach { + AppBarDropdownItem( + item = it, + dismissParent = { showDropdownMenu = false }, + ) + } + }, + ) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = TangemTheme.colors.background.secondary, @@ -41,6 +70,57 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { ) } +@Suppress("ComposableEventParameterNaming") +@Composable +private fun AppBarDropdownItem( + item: TokenDetailsAppBarMenuConfig.MenuItem, + dismissParent: () -> Unit, + modifier: Modifier = Modifier, +) { + Text( + modifier = modifier + .clickable { + dismissParent() + item.onClick() + } + .padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16), + text = item.title.resolveReference(), + style = TangemTheme.typography.body1.copy(color = item.textColorProvider()), + ) +} + +@Preview +@Composable +private fun Preview_TokenDetailsAppBarDropdownItem_LightTheme() { + TangemTheme(isDark = false) { + AppBarDropdownItem( + modifier = Modifier.background(TangemTheme.colors.background.primary), + dismissParent = {}, + item = TokenDetailsAppBarMenuConfig.MenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsAppBarDropdownItem_DarkTheme() { + TangemTheme(isDark = true) { + AppBarDropdownItem( + modifier = Modifier.background(TangemTheme.colors.background.primary), + dismissParent = {}, + item = TokenDetailsAppBarMenuConfig.MenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ) + } +} + @Preview @Composable private fun Preview_TokenDetailsTopAppBar_LightTheme() { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index fba6130de7..9881457874 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -1,13 +1,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels -import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents - -interface TokenDetailsClickIntents : TxHistoryClickIntents { +interface TokenDetailsClickIntents { fun onBackClick() - fun onMoreClick() - fun onSendClick() fun onReceiveClick() @@ -15,4 +11,24 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents { fun onSellClick() fun onSwapClick() + + fun onDismissDialog() + + fun onHideClick() + + fun onHideConfirmed() + + fun onRefreshSwipe() + + fun onBuyClick() + + fun onReloadClick() + + fun onExploreClick() + + fun onDismissBottomSheet() + + fun onCloseRentInfoNotification() + + fun onCloseExistentialDepositNotification() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index f4defde6c4..36cfad8630 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -9,14 +9,16 @@ import arrow.core.getOrElse import com.tangem.common.Provider import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency 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 import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase @@ -31,6 +33,7 @@ import com.tangem.utils.coroutines.saveIn import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates @@ -41,10 +44,17 @@ internal class TokenDetailsViewModel @Inject constructor( private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, + private val removeCurrencyUseCase: RemoveCurrencyUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, + private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, + private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { @@ -55,23 +65,29 @@ internal class TokenDetailsViewModel @Inject constructor( var router by Delegates.notNull() private val marketPriceJobHolder = JobHolder() + private val refreshStateJobHolder = JobHolder() private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var wallet by Delegates.notNull() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private var isBalanceHidden = true + private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + isBalanceHiddenProvider = Provider { isBalanceHidden }, clickIntents = this, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, ) + var uiState: TokenDetailsState by mutableStateOf(stateFactory.getInitialState(cryptoCurrency)) private set override fun onCreate(owner: LifecycleOwner) { getWallet() updateContent(selectedWallet = wallet) + handleBalanceHiding(owner) } private fun getWallet() { @@ -84,18 +100,46 @@ internal class TokenDetailsViewModel @Inject constructor( private fun updateContent(selectedWallet: UserWallet) { updateMarketPrice(selectedWallet = selectedWallet) - updateButtons(userWalletId = selectedWallet.walletId, currencyId = cryptoCurrency.id.value) updateTxHistory() + updateWarnings(selectedWallet = selectedWallet) } - private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { - getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) + private fun handleBalanceHiding(owner: LifecycleOwner) { + isBalanceHiddenUseCase() + .flowWithLifecycle(owner.lifecycle) + .onEach { hidden -> + isBalanceHidden = hidden + uiState = stateFactory.getStateWithUpdatedHidden(isBalanceHidden = hidden) + } + .launchIn(viewModelScope) + + viewModelScope.launch { + listenToFlipsUseCase() + .flowWithLifecycle(owner.lifecycle) + .collect() + } + } + + private fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrencyStatus = currencyStatus) .distinctUntilChanged() .onEach { uiState = stateFactory.getManageButtonsState(actions = it.states) } .flowOn(dispatchers.io) .launchIn(viewModelScope) } + private fun updateWarnings(selectedWallet: UserWallet) { + viewModelScope.launch(dispatchers.io) { + getCurrencyWarningsUseCase.invoke( + userWalletId = selectedWallet.walletId, + currency = cryptoCurrency, + ) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getStateWithNotifications(it) } + .launchIn(viewModelScope) + } + } + private fun updateMarketPrice(selectedWallet: UserWallet) { getCurrencyStatusUpdatesUseCase( userWalletId = selectedWallet.walletId, @@ -104,27 +148,30 @@ internal class TokenDetailsViewModel @Inject constructor( .distinctUntilChanged() .onEach { either -> uiState = stateFactory.getCurrencyLoadedBalanceState(either) - either.onRight { status -> cryptoCurrencyStatus = status } + either.onRight { status -> + cryptoCurrencyStatus = status + updateButtons(userWalletId = selectedWallet.walletId, currencyStatus = status) + } } .flowOn(dispatchers.io) .launchIn(viewModelScope) .saveIn(marketPriceJobHolder) } - private fun updateTxHistory() { + private fun updateTxHistory(refresh: Boolean = false) { viewModelScope.launch(dispatchers.io) { val txHistoryItemsCountEither = txHistoryItemsCountUseCase( - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.derivationPath, + network = cryptoCurrency.network, ) - uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + if (!refresh) { + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + } txHistoryItemsCountEither.onRight { uiState = stateFactory.getLoadedTxHistoryState( txHistoryEither = txHistoryItemsUseCase( - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.derivationPath, + network = cryptoCurrency.network, ).map { it.cachedIn(viewModelScope) }, @@ -149,10 +196,6 @@ internal class TokenDetailsViewModel @Inject constructor( router.popBackStack() } - override fun onMoreClick() { - TODO("Not yet implemented") - } - override fun onBuyClick() { val status = cryptoCurrencyStatus ?: return @@ -170,11 +213,54 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onSendClick() { - reduxStateHolder.dispatch(TradeCryptoAction.New.Send) + val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return + + when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin( + userWallet = wallet, + coinStatus = cryptoCurrencyStatus, + ), + ) + } + is CryptoCurrency.Token -> sendToken(status = cryptoCurrencyStatus) + } + } + + private fun sendToken(status: CryptoCurrencyStatus) { + viewModelScope.launch(dispatchers.io) { + getNetworkCoinStatusUseCase( + userWalletId = wallet.walletId, + networkId = status.currency.network.id, + ) + .take(count = 1) + .collectLatest { + it.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = wallet, + tokenStatus = status, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } + } + } } override fun onReceiveClick() { - // TODO: [REDACTED_JIRA] + viewModelScope.launch(dispatchers.io) { + val addresses = walletManagersFacade.getAddress( + userWalletId = wallet.walletId, + network = cryptoCurrency.network, + ) + + uiState = stateFactory.getStateWithReceiveBottomSheet( + currency = cryptoCurrency, + addresses = addresses, + ) + } } override fun onSellClick() { @@ -191,14 +277,64 @@ internal class TokenDetailsViewModel @Inject constructor( reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency)) } + override fun onDismissDialog() { + uiState = stateFactory.getStateWithClosedDialog() + } + + override fun onHideClick() { + viewModelScope.launch { + val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(wallet.walletId, cryptoCurrency) + uiState = if (hasLinkedTokens) { + stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) + } else { + stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency) + } + } + } + + override fun onHideConfirmed() { + viewModelScope.launch { + removeCurrencyUseCase.invoke(wallet.walletId, cryptoCurrency) + .onLeft { Timber.e(it) } + .onRight { router.popBackStack() } + } + } + override fun onExploreClick() { viewModelScope.launch { router.openUrl( url = getExploreUrlUseCase( userWalletId = wallet.walletId, - networkId = cryptoCurrency.network.id, + network = cryptoCurrency.network, ), ) } } + + override fun onRefreshSwipe() { + uiState = stateFactory.getRefreshingState() + + viewModelScope.launch(dispatchers.io) { + fetchCurrencyStatusUseCase.invoke( + userWalletId = wallet.walletId, + id = cryptoCurrency.id, + refresh = true, + ) + updateTxHistory(refresh = true) + + uiState = stateFactory.getRefreshedState() + }.saveIn(refreshStateJobHolder) + } + + override fun onDismissBottomSheet() { + uiState = stateFactory.getStateWithClosedBottomSheet() + } + + override fun onCloseExistentialDepositNotification() { + uiState = stateFactory.getStateWithRemovedExistentialNotification() + } + + override fun onCloseRentInfoNotification() { + uiState = stateFactory.getStateWithRemovedRentNotification() + } } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 0e84af12c3..8de129fa16 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -44,6 +44,8 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) /** Domain modules */ implementation(projects.common) @@ -60,6 +62,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.balanceHiding) /** Feature Apis */ implementation(projects.features.wallet.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 60f3ec11ff..4b51515ef3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -2,19 +2,25 @@ package com.tangem.feature.wallet.presentation.common import androidx.paging.PagingData import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.event.consumed +import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.components.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.persistentListOf @@ -26,7 +32,7 @@ import java.util.UUID @Suppress("LargeClass") internal object WalletPreviewData { - val walletTopBarConfig by lazy { WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}) } + val topBarConfig by lazy { WalletTopBarConfig(onDetailsClick = {}) } val walletCardContentState by lazy { WalletCardState.Content( @@ -57,6 +63,8 @@ internal object WalletPreviewData { imageResId = R.drawable.ill_businessman_3d, onRenameClick = { _, _ -> }, onDeleteClick = {}, + balance = "8923,05 $", + additionalInfo = TextReference.Str("3 cards • Seed phrase"), ) } @@ -87,23 +95,46 @@ internal object WalletPreviewData { ) } + private val coinIconState + get() = TokenItemState.IconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_polygon_22, + isGrayscale = false, + isCustom = false, + ) + + private val tokenIconState + get() = TokenItemState.IconState.TokenIcon( + url = null, + networkBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + ) + + private val customTokenIconState + get() = TokenItemState.IconState.CustomTokenIcon( + tint = TangemColorPalette.Black, + background = TangemColorPalette.Meadow, + networkBadgeIconResId = R.drawable.img_polygon_22, + isGrayscale = false, + ) + val tokenItemVisibleState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = coinIconState, name = "Polygon", amount = "5,412 MATIC", hasPending = true, - tokenOptions = TokenOptionsState.Visible( + tokenOptions = TokenOptionsState( fiatAmount = "321 $", config = PriceChangeConfig( valueInPercent = "2%", type = PriceChangeConfig.Type.UP, ), + isBalanceHidden = false, ), - isTestnet = false, onItemClick = {}, onItemLongClick = {}, ) @@ -112,26 +143,26 @@ internal object WalletPreviewData { val testnetTokenItemVisibleState by lazy { tokenItemVisibleState.copy( name = "Polygon testnet", - isTestnet = true, + icon = tokenIconState.copy(isGrayscale = true), ) } val tokenItemHiddenState by lazy { TokenItemState.Content( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", amount = "5,412 MATIC", hasPending = true, - tokenOptions = TokenOptionsState.Hidden( + tokenOptions = TokenOptionsState( config = PriceChangeConfig( valueInPercent = "2%", type = PriceChangeConfig.Type.UP, ), + fiatAmount = "321 $", + isBalanceHidden = false, + ), - isTestnet = false, onItemClick = {}, onItemLongClick = {}, ) @@ -140,25 +171,37 @@ internal object WalletPreviewData { val tokenItemDragState by lazy { TokenItemState.Draggable( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", - isTestnet = false, - fiatAmount = "3 172,14 $", + info = stringReference(value = "3 172,14 $"), ) } val tokenItemUnreachableState by lazy { TokenItemState.Unreachable( id = UUID.randomUUID().toString(), - tokenIconUrl = null, - tokenIconResId = R.drawable.img_polygon_22, - networkBadgeIconResId = R.drawable.img_polygon_22, + icon = tokenIconState, name = "Polygon", ) } + val customTokenItemVisibleState by lazy { + tokenItemVisibleState.copy( + name = "Polygon custom", + icon = customTokenIconState.copy( + tint = TangemColorPalette.White, + background = TangemColorPalette.Black, + ), + ) + } + + val customTestnetTokenItemVisibleState by lazy { + tokenItemVisibleState.copy( + name = "Polygon custom testnet", + icon = customTokenIconState.copy(isGrayscale = true), + ) + } + val loadingTokenItemState by lazy { TokenItemState.Loading(id = "Loading#1") } private const val networksSize = 10 @@ -171,7 +214,7 @@ internal object WalletPreviewData { val networkNumber = index + 1 val group = DraggableItem.GroupHeader( - id = "group_$networkNumber", + id = networkNumber, networkName = "$networkNumber", roundingMode = when (index) { 0 -> DraggableItem.RoundingMode.Top() @@ -188,7 +231,6 @@ internal object WalletPreviewData { tokenItemState = tokenItemDragState.copy( id = "${group.id}_token_$tokenNumber", name = "Token $tokenNumber from $networkNumber network", - networkBadgeIconResId = R.drawable.img_eth_22.takeIf { i != 0 }, ), groupId = group.id, roundingMode = when { @@ -199,7 +241,7 @@ internal object WalletPreviewData { ) } - val divider = DraggableItem.GroupPlaceholder(id = "divider_$networkNumber") + val divider = DraggableItem.Placeholder(id = "divider_$networkNumber") buildList { add(group) @@ -234,7 +276,7 @@ internal object WalletPreviewData { ), dndConfig = OrganizeTokensState.DragAndDropConfig( onItemDragged = { _, _ -> }, - onDragStart = {}, + onItemDragStart = {}, canDragItemOver = { _, _ -> false }, onItemDragEnd = {}, ), @@ -242,7 +284,7 @@ internal object WalletPreviewData { onApplyClick = {}, onCancelClick = {}, ), - scrollListToTop = consumed, + scrollListToTop = consumedEvent(), ) } @@ -255,10 +297,10 @@ internal object WalletPreviewData { } val bottomSheet by lazy { - WalletBottomSheetConfig( + TangemBottomSheetConfig( isShow = false, onDismissRequest = {}, - content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + content = WalletBottomSheetConfig.UnlockWallets( onUnlockClick = {}, onScanClick = {}, ), @@ -270,7 +312,7 @@ internal object WalletPreviewData { onDismissRequest = {}, actions = listOf( TokenActionButtonConfig( - text = "Send", + text = TextReference.Str("Send"), iconResId = R.drawable.ic_share_24, onClick = {}, ), @@ -290,17 +332,15 @@ internal object WalletPreviewData { val multicurrencyWalletScreenState by lazy { WalletMultiCurrencyState.Content( onBackClick = {}, - topBarConfig = walletTopBarConfig, + topBarConfig = topBarConfig, walletsListConfig = walletListConfig, tokensListState = WalletTokensListState.Content( persistentListOf( - TokensListItemState.NetworkGroupTitle(TextReference.Str("Bitcoin")), + TokensListItemState.NetworkGroupTitle(id = 0, stringReference("Bitcoin")), TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_1", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -308,8 +348,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_2", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -317,8 +355,6 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_3", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), @@ -326,50 +362,46 @@ internal object WalletPreviewData { tokenItemVisibleState.copy( id = "token_4", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), - TokensListItemState.NetworkGroupTitle(TextReference.Str("Ethereum")), + TokensListItemState.NetworkGroupTitle(id = 1, stringReference("Ethereum")), TokensListItemState.Token( tokenItemVisibleState.copy( id = "token_5", name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkBadgeIconResId = null, amount = "1,89340821 ETH", ), ), ), - onOrganizeTokensClick = {}, + organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Visible(isEnabled = true, {}), ), pullToRefreshConfig = WalletPullToRefreshConfig( isRefreshing = false, onRefresh = {}, ), notifications = persistentListOf( - WalletNotification.UnreachableNetworks, - WalletNotification.LikeTangemApp(onClick = {}), - WalletNotification.BackupCard(onClick = {}), - WalletNotification.ScanCard(onClick = {}), + WalletNotification.Critical.DevCard, + WalletNotification.MissingAddresses(missingAddressesCount = 0, onGenerateClick = {}), + WalletNotification.Warning.NetworksUnreachable, ), bottomSheetConfig = bottomSheet, tokenActionsBottomSheet = actionsBottomSheet, onManageTokensClick = {}, + event = consumedEvent(), ) } val singleWalletScreenState by lazy { WalletSingleCurrencyState.Content( onBackClick = {}, - topBarConfig = walletTopBarConfig, + topBarConfig = topBarConfig, walletsListConfig = walletListConfig, pullToRefreshConfig = WalletPullToRefreshConfig( isRefreshing = false, onRefresh = {}, ), - notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), + notifications = persistentListOf(WalletNotification.Warning.NetworksUnreachable), buttons = manageButtons, bottomSheetConfig = bottomSheet, marketPriceBlockState = MarketPriceBlockState.Content( @@ -406,6 +438,7 @@ internal object WalletPreviewData { ), ), ), + event = consumedEvent(), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 6169aa9a8c..14b4a2065a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -3,11 +3,15 @@ package com.tangem.feature.wallet.presentation.common.component import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.tooling.preview.Preview @@ -17,37 +21,21 @@ import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope import androidx.constraintlayout.compose.Dimension -import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.token.TokenCryptoInfoBlock import com.tangem.feature.wallet.presentation.common.component.token.TokenFiatInfoBlock -import com.tangem.feature.wallet.presentation.common.component.token.TokenIcon +import com.tangem.feature.wallet.presentation.common.component.token.icon.TokenIcon import com.tangem.feature.wallet.presentation.common.state.TokenItemState import org.burnoutcrew.reorderable.ReorderableLazyListState -// TODO: Add custom token state: [REDACTED_JIRA] -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun TokenItem( state: TokenItemState, modifier: Modifier = Modifier, reorderableTokenListState: ReorderableLazyListState? = null, ) { - val hapticFeedback = LocalHapticFeedback.current - val containerModifier: Modifier = remember(state) { - when (state) { - is TokenItemState.Content -> modifier.combinedClickable( - onClick = state.onItemClick, - onLongClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - state.onItemLongClick() - }, - ) - else -> modifier - } - } - BaseContainer(modifier = containerModifier) { + BaseContainer(modifier = modifier.tokenClickable(state)) { val (iconRef, cryptoInfoRef, fiatInfoRef) = createRefs() TokenIcon( @@ -90,7 +78,7 @@ private inline fun BaseContainer( ) { ConstraintLayout( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding( horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing14, @@ -110,8 +98,32 @@ private fun Modifier.constrainAsOptionsItem(scope: ConstraintLayoutScope, ref: C } } -// region preview +@OptIn(ExperimentalFoundationApi::class) +private fun Modifier.tokenClickable(state: TokenItemState): Modifier = composed { + when (state) { + is TokenItemState.Content -> { + val hapticFeedback = LocalHapticFeedback.current + val onLongClick = remember(state) { + { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + state.onItemLongClick() + } + } + this.combinedClickable( + onClick = state.onItemClick, + onLongClick = onLongClick, + ) + } + is TokenItemState.Draggable, + is TokenItemState.Unreachable, + is TokenItemState.Loading, + is TokenItemState.Locked, + -> this + } +} + +// region preview @Preview @Composable private fun Preview_Tokens_LightTheme(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { @@ -136,6 +148,8 @@ private class TokenConfigProvider : CollectionPreviewParameterProvider if (state.tokenOptions is TokenOptionsState.Hidden) DOTS else state.amount - is TokenItemState.Draggable -> state.fiatAmount + is TokenItemState.Content -> if (state.tokenOptions.isBalanceHidden) STARS else state.amount + is TokenItemState.Draggable -> state.info.resolveReference() is TokenItemState.Unreachable -> null }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt index ef5d0d5f72..8c9f6db9fa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatInfoBlock.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.common.component.token import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -12,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW4 import com.tangem.core.ui.components.marketprice.PriceChangeConfig @@ -19,7 +19,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState -import com.tangem.feature.wallet.presentation.common.state.TokenItemState.Companion.DOTS import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder @@ -57,9 +56,10 @@ private fun ContentBlock(state: TokenOptionsState, modifier: Modifier = Modifier modifier = Modifier.align(Alignment.End), ) { Text( - text = when (it) { - is TokenOptionsState.Visible -> it.fiatAmount - is TokenOptionsState.Hidden -> DOTS + text = if (it.isBalanceHidden) { + STARS + } else { + it.fiatAmount }, style = TangemTypography.body2, color = TangemTheme.colors.text.primary1, @@ -87,13 +87,17 @@ private fun PriceChangeIcon(type: PriceChangeConfig.Type, modifier: Modifier = M label = "Update the price change's arrow", modifier = modifier, ) { - Image( + Icon( painter = painterResource( id = when (it) { - PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8 - PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8 + PriceChangeConfig.Type.UP -> R.drawable.ic_arrow_up_8 + PriceChangeConfig.Type.DOWN -> R.drawable.ic_arrow_down_8 }, ), + tint = when (it) { + PriceChangeConfig.Type.UP -> TangemTheme.colors.icon.accent + PriceChangeConfig.Type.DOWN -> TangemTheme.colors.icon.warning + }, contentDescription = null, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt deleted file mode 100644 index 6ddfe82790..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenIcon.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.tangem.feature.wallet.presentation.common.component.token - -import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.ColorMatrix -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.CircleShimmer -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.common.state.TokenItemState - -private const val GRAY_SCALE_SATURATION = 0f - -@Composable -internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) { - when (state) { - is TokenItemState.ContentState -> ContentIcon(content = state, modifier = modifier) - is TokenItemState.Loading -> LoadingIcon(modifier = modifier) - is TokenItemState.Locked -> LockedIcon(modifier = modifier) - } -} - -@Composable -private fun ContentIcon(content: TokenItemState.ContentState, modifier: Modifier = Modifier) { - BaseContainer(modifier = modifier) { - val isTestnet = when (content) { - is TokenItemState.Content -> content.isTestnet - is TokenItemState.Draggable -> content.isTestnet - is TokenItemState.Unreachable -> false - } - - val colorFilter = remember(isTestnet) { - if (isTestnet) { - ColorFilter.colorMatrix( - colorMatrix = ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }, - ) - } else { - null - } - } - - Icon( - content = content, - colorFilter = colorFilter, - modifier = Modifier.align(Alignment.BottomStart), - ) - - NetworkBadge( - iconResId = content.networkBadgeIconResId, - colorFilter = colorFilter, - modifier = Modifier.align(Alignment.TopEnd), - ) - } -} - -@Composable -private fun Icon(content: TokenItemState.ContentState, colorFilter: ColorFilter?, modifier: Modifier = Modifier) { - val iconUrl = content.tokenIconUrl - val iconData: Any = remember(iconUrl) { - if (iconUrl.isNullOrEmpty()) content.tokenIconResId else iconUrl - } - - SubcomposeAsyncImage( - modifier = modifier.iconSize(), - model = ImageRequest.Builder(context = LocalContext.current) - .data(data = iconData) - .placeholder(drawableResId = content.tokenIconResId) - .error(drawableResId = content.tokenIconResId) - .fallback(drawableResId = content.tokenIconResId) - .crossfade(enable = true) - .build(), - colorFilter = colorFilter, - contentDescription = null, - ) -} - -@Composable -private fun BoxScope.NetworkBadge( - @DrawableRes iconResId: Int?, - colorFilter: ColorFilter?, - modifier: Modifier = Modifier, -) { - AnimatedVisibility( - visible = iconResId != null, - modifier = modifier - .size(TangemTheme.dimens.size18) - .background(color = TangemTheme.colors.background.primary, shape = CircleShape), - ) { - if (iconResId == null) return@AnimatedVisibility - - Image( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing2) - .align(Alignment.Center), - painter = painterResource(id = iconResId), - colorFilter = colorFilter, - contentDescription = null, - ) - } -} - -@Composable -private fun LoadingIcon(modifier: Modifier = Modifier) { - BaseContainer(modifier) { - CircleShimmer( - modifier = Modifier - .iconSize() - .align(alignment = Alignment.BottomStart), - ) - } -} - -@Composable -private fun LockedIcon(modifier: Modifier = Modifier) { - BaseContainer(modifier) { - Box( - modifier = Modifier - .iconSize() - .align(Alignment.BottomStart), - ) { - Box( - modifier = Modifier - .matchParentSize() - .background(color = TangemTheme.colors.background.secondary, shape = CircleShape), - ) - } - } -} - -@Composable -private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { - Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) -} - -private fun Modifier.iconSize(): Modifier = composed { - return@composed this.size(size = TangemTheme.dimens.size36) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt new file mode 100644 index 0000000000..49e3b3f183 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/ContentIcon.kt @@ -0,0 +1,145 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +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.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +@Composable +internal fun ContentIcon( + icon: TokenItemState.IconState, + alpha: Float, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + when (icon) { + is TokenItemState.IconState.CoinIcon -> CoinIcon( + modifier = modifier, + url = icon.url, + fallbackResId = icon.fallbackResId, + alpha = alpha, + colorFilter = colorFilter, + ) + is TokenItemState.IconState.TokenIcon -> TokenIcon( + modifier = modifier, + url = icon.url, + alpha = alpha, + colorFilter = colorFilter, + errorIcon = { + CustomTokenIcon( + modifier = modifier, + tint = icon.fallbackTint, + background = icon.fallbackBackground, + alpha = alpha, + ) + }, + ) + is TokenItemState.IconState.CustomTokenIcon -> CustomTokenIcon( + modifier = modifier, + tint = icon.tint, + background = icon.background, + alpha = alpha, + ) + } +} + +@Composable +private fun CoinIcon( + url: String?, + @DrawableRes fallbackResId: Int, + alpha: Float, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url + + DefaultCurrencyIcon( + modifier = modifier, + iconData = iconData, + errorIcon = { + Image( + painter = painterResource(id = fallbackResId), + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) + }, + alpha = alpha, + colorFilter = colorFilter, + ) +} + +@Composable +private fun TokenIcon( + url: String?, + alpha: Float, + colorFilter: ColorFilter?, + errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + if (url == null) { + errorIcon() + } else { + DefaultCurrencyIcon( + modifier = modifier, + iconData = url, + errorIcon = errorIcon, + alpha = alpha, + colorFilter = colorFilter, + ) + } +} + +@Composable +private fun CustomTokenIcon(tint: Color, background: Color, alpha: Float, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background( + color = background.copy(alpha = alpha), + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.matchParentSize(), + painter = painterResource(id = R.drawable.ic_custom_token_44), + tint = tint.copy(alpha = alpha), + contentDescription = null, + ) + } +} + +@Composable +private inline fun DefaultCurrencyIcon( + iconData: Any, + alpha: Float, + colorFilter: ColorFilter?, + crossinline errorIcon: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + SubcomposeAsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(iconData) + .crossfade(enable = true) + .build(), + loading = { LoadingIcon() }, + error = { errorIcon() }, + alpha = alpha, + colorFilter = colorFilter, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt new file mode 100644 index 0000000000..6fd0339923 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/IconBadge.kt @@ -0,0 +1,63 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun NetworkBadge( + @DrawableRes iconResId: Int, + alpha: Float, + colorFilter: ColorFilter?, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size18) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Image( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .matchParentSize(), + painter = painterResource(id = iconResId), + colorFilter = colorFilter, + alpha = alpha, + contentDescription = null, + ) + } +} + +@Composable +internal fun CustomBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size12) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Box( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .matchParentSize() + .background( + color = TangemTheme.colors.icon.informative, + shape = CircleShape, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt new file mode 100644 index 0000000000..be02ad2229 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/icon/TokenIcon.kt @@ -0,0 +1,100 @@ +package com.tangem.feature.wallet.presentation.common.component.token.icon + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.ColorMatrix +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +private const val GRAY_SCALE_SATURATION = 0f +private const val GRAY_SCALE_ALPHA = 0.4f +private const val NORMAL_ALPHA = 1f + +@Composable +internal fun TokenIcon(state: TokenItemState, modifier: Modifier = Modifier) { + BaseContainer(modifier = modifier) { + val iconModifier = Modifier + .align(Alignment.Center) + .size(TangemTheme.dimens.size36) + + when (state) { + is TokenItemState.Loading -> LoadingIcon(modifier = iconModifier) + is TokenItemState.Locked -> LockedIcon(modifier = iconModifier) + is TokenItemState.ContentState -> ContentIconContainer( + modifier = iconModifier, + icon = state.icon, + ) + } + } +} + +@Composable +internal fun LoadingIcon(modifier: Modifier = Modifier) { + CircleShimmer(modifier = modifier) +} + +@Composable +private fun LockedIcon(modifier: Modifier = Modifier) { + Box(modifier = modifier) { + Box( + modifier = Modifier + .matchParentSize() + .background( + color = TangemTheme.colors.background.secondary, + shape = CircleShape, + ), + ) + } +} + +@Composable +private fun BoxScope.ContentIconContainer(icon: TokenItemState.IconState, modifier: Modifier = Modifier) { + val networkBadgeOffset = TangemTheme.dimens.spacing4 + val (alpha, colorFilter) = remember(icon.isGrayscale) { + if (icon.isGrayscale) { + GRAY_SCALE_ALPHA to GrayscaleColorFilter + } else { + NORMAL_ALPHA to null + } + } + + ContentIcon( + modifier = modifier, + icon = icon, + alpha = alpha, + colorFilter = colorFilter, + ) + + if (icon.networkBadgeIconResId != null) { + NetworkBadge( + modifier = Modifier + .offset(x = networkBadgeOffset, y = -networkBadgeOffset) + .align(Alignment.TopEnd), + iconResId = requireNotNull(icon.networkBadgeIconResId), + alpha = alpha, + colorFilter = colorFilter, + ) + } + + if (icon.isCustom) { + CustomBadge(modifier = Modifier.align(Alignment.BottomEnd)) + } +} + +@Composable +private inline fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { + Box(modifier = modifier.size(size = TangemTheme.dimens.size40), content = content) +} + +private val GrayscaleColorFilter: ColorFilter + get() = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(GRAY_SCALE_SATURATION) }) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 8310f134d5..70e14a9b86 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -2,7 +2,9 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.extensions.TextReference /** Token item state */ @Immutable @@ -18,107 +20,135 @@ internal sealed interface TokenItemState { data class Locked(override val id: String) : TokenItemState /** Content state */ - sealed class ContentState( - override val id: String, - open val tokenIconUrl: String?, - @DrawableRes open val tokenIconResId: Int, - @DrawableRes open val networkBadgeIconResId: Int?, - open val name: String, - ) : TokenItemState + @Immutable + sealed class ContentState : TokenItemState { + + abstract val icon: IconState + abstract val name: String + } /** * Content token state * * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name * @property amount amount of token * @property hasPending pending tx in blockchain * @property tokenOptions state for token options - * @property isTestnet indicates whether the token is from test network or not * @property onItemClick callback which will be called when an item is clicked * @property onItemLongClick callback which will be called when an item is long clicked */ data class Content( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, val amount: String, val hasPending: Boolean, val tokenOptions: TokenOptionsState, - val isTestnet: Boolean, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + ) : ContentState() /** * Draggable token state * * @property id unique id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name - * @property fiatAmount fiat amount of token - * @property isTestnet indicates whether the token is from test network or not + * @property info token info (e.g. fiat balance or status) */ data class Draggable( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, - val fiatAmount: String, - val isTestnet: Boolean, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + val info: TextReference, + ) : ContentState() /** * Unreachable token state * * @property id token id - * @property tokenIconUrl token icon url - * @property tokenIconResId token icon resource id - * @property networkBadgeIconResId network badge icon resource id, may be null if it is a coin + * @property icon token icon state * @property name token name */ data class Unreachable( override val id: String, - override val tokenIconUrl: String?, - @DrawableRes override val tokenIconResId: Int, - @DrawableRes override val networkBadgeIconResId: Int?, + override val icon: IconState, override val name: String, - ) : ContentState(id, tokenIconUrl, tokenIconResId, networkBadgeIconResId, name) + ) : ContentState() + + /** + * Represents the various states an icon can be in. + */ + @Immutable + sealed class IconState { + + abstract val isGrayscale: Boolean + abstract val isCustom: Boolean + abstract val networkBadgeIconResId: Int? + + /** + * Represents a coin icon. + * + * @property url The URL where the coin icon can be fetched from. May be `null` if not found. + * @property fallbackResId The drawable resource ID to be used as a fallback if the URL is not available. + * @property isGrayscale Specifies whether to show the icon in grayscale. + */ + data class CoinIcon( + val url: String?, + @DrawableRes val fallbackResId: Int, + override val isGrayscale: Boolean, + override val isCustom: Boolean, + ) : IconState() { + + override val networkBadgeIconResId: Int? = null + } + + /** + * Represents a token icon. + * + * @property url The URL where the token icon can be fetched from. May be `null` if not found. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + * @property fallbackTint The color to be used for tinting the fallback icon. + * @property fallbackBackground The background color to be used for the fallback icon. + */ + data class TokenIcon( + val url: String?, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + val fallbackTint: Color, + val fallbackBackground: Color, + ) : IconState() { + + override val isCustom: Boolean = false + } + + /** + * Represents a custom token icon. + * + * @property tint The color to be used for tinting the icon. + * @property background The background color to be used for the icon. + * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property isGrayscale Specifies whether to show the icon in grayscale. + */ + data class CustomTokenIcon( + val tint: Color, + val background: Color, + @DrawableRes override val networkBadgeIconResId: Int, + override val isGrayscale: Boolean, + ) : IconState() { + + override val isCustom: Boolean = false + } + } /** Token options state */ @Immutable - sealed interface TokenOptionsState { - - val config: PriceChangeConfig - - /** - * Visible token options state - * - * @property fiatAmount fiat amount of token - * @property config value of price changing - */ - data class Visible( - override val config: PriceChangeConfig, - val fiatAmount: String, - ) : TokenOptionsState - - /** - * Hidden token options state - * - * @property config value of price changing - */ - data class Hidden(override val config: PriceChangeConfig) : TokenOptionsState - } - - companion object { - const val DOTS = "•••" - } + data class TokenOptionsState( + val config: PriceChangeConfig, + val fiatAmount: String, + val isBalanceHidden: Boolean, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt new file mode 100644 index 0000000000..8c07d881af --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/utils/CryptoCurrencyToIconStateConverter.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.wallet.presentation.common.utils + +import com.tangem.core.ui.extensions.getTintForTokenIcon +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.models.CryptoCurrency +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.utils.converter.Converter + +internal class CryptoCurrencyToIconStateConverter : Converter { + + override fun convert(value: CryptoCurrencyStatus): TokenItemState.IconState { + return when (val currency = value.currency) { + is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError) + is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError) + } + } + + private fun getIconStateForCoin( + coin: CryptoCurrency.Coin, + isUnreachable: Boolean, + ): TokenItemState.IconState.CoinIcon { + return TokenItemState.IconState.CoinIcon( + url = coin.iconUrl, + fallbackResId = coin.networkIconResId, + isGrayscale = coin.network.isTestnet || isUnreachable, + isCustom = coin.isCustom, + ) + } + + private fun getIconStateForToken(token: CryptoCurrency.Token, isErrorStatus: Boolean): TokenItemState.IconState { + val isGrayscale = token.network.isTestnet || isErrorStatus + val background = token.tryGetBackgroundForTokenIcon(isGrayscale) + val tint = getTintForTokenIcon(background) + + return if (token.isCustom) { + TokenItemState.IconState.CustomTokenIcon( + tint = tint, + background = background, + networkBadgeIconResId = token.networkIconResId, + isGrayscale = isGrayscale, + ) + } else { + TokenItemState.IconState.TokenIcon( + url = token.iconUrl, + networkBadgeIconResId = token.networkIconResId, + isGrayscale = isGrayscale, + fallbackTint = tint, + fallbackBackground = background, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 238ff49a18..b47d1fd500 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -1,8 +1,7 @@ package com.tangem.feature.wallet.presentation.organizetokens import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.* -import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* @@ -19,10 +18,13 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig @@ -37,7 +39,10 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import org.burnoutcrew.reorderable.* +import org.burnoutcrew.reorderable.ReorderableItem +import org.burnoutcrew.reorderable.ReorderableLazyListState +import org.burnoutcrew.reorderable.rememberReorderableLazyListState +import org.burnoutcrew.reorderable.reorderable @Composable internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier = Modifier) { @@ -91,27 +96,28 @@ private fun TokenList( canDragOver = dndConfig.canDragItemOver, onDragEnd = onDragEnd, ) - val items = state.items + + val listContentPadding = PaddingValues( + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing92, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ) LazyColumn( modifier = Modifier - .reorderable(reorderableListState) .align(Alignment.TopCenter) - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxSize(), + .reorderable(reorderableListState), state = reorderableListState.listState, - contentPadding = PaddingValues( - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing92, - ), + contentPadding = listContentPadding, ) { itemsIndexed( - items = items, + items = state.items, key = { _, item -> item.id }, ) { index, item -> val onDragStart = remember(item) { - { dndConfig.onDragStart(item) } + { dndConfig.onItemDragStart(item) } } DraggableItem( @@ -127,7 +133,6 @@ private fun TokenList( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun LazyItemScope.DraggableItem( index: Int, @@ -135,15 +140,18 @@ private fun LazyItemScope.DraggableItem( reorderableState: ReorderableLazyListState, onDragStart: () -> Unit, ) { + var isDragging by remember { + mutableStateOf(value = false) + } + + val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow) + ReorderableItem( - defaultDraggingModifier = Modifier.animateItemPlacement( - animationSpec = tween(easing = LinearOutSlowInEasing), - ), - state = reorderableState, + reorderableState = reorderableState, index = index, key = item.id, - ) { isDragging -> - val itemModifier = Modifier.applyShapeAndShadow(item.roundingMode, item.showShadow) + ) { isItemDragging -> + isDragging = isItemDragging when (item) { is DraggableItem.GroupHeader -> DraggableNetworkGroupItem( @@ -157,10 +165,12 @@ private fun LazyItemScope.DraggableItem( reorderableTokenListState = reorderableState, ) // Should be presented in the list but remain invisible - is DraggableItem.GroupPlaceholder -> Box(modifier = Modifier.fillMaxWidth()) + is DraggableItem.Placeholder -> Box(modifier = Modifier.fillMaxWidth()) } + } - LaunchedEffect(isDragging) { + DisposableEffect(isDragging) { + onDispose { if (isDragging) { onDragStart() } @@ -283,57 +293,71 @@ private fun Actions(config: OrganizeTokensState.ActionsConfig, modifier: Modifie } } -private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier = - composed { +private fun Modifier.applyShapeAndShadow(roundingMode: DraggableItem.RoundingMode, showShadow: Boolean): Modifier { + return composed { val radius by animateDpAsState( - targetValue = if (roundingMode !is DraggableItem.RoundingMode.None) { - TangemTheme.dimens.radius16 - } else { - TangemTheme.dimens.radius0 + targetValue = when (roundingMode) { + is DraggableItem.RoundingMode.None -> TangemTheme.dimens.radius0 + is DraggableItem.RoundingMode.All -> TangemTheme.dimens.radius12 + is DraggableItem.RoundingMode.Bottom, + is DraggableItem.RoundingMode.Top, + -> TangemTheme.dimens.radius16 }, label = "item_shape_radius", ) - val shape = when (roundingMode) { - is DraggableItem.RoundingMode.None -> RectangleShape - is DraggableItem.RoundingMode.Top -> RoundedCornerShape( - topStart = radius, - topEnd = radius, - ) - is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape( - bottomStart = radius, - bottomEnd = radius, - ) - is DraggableItem.RoundingMode.All -> RoundedCornerShape( - size = radius, - ) - } - - val paddingValue = TangemTheme.dimens.spacing4 - val padding = if (roundingMode.showGap) { - when (roundingMode) { - is DraggableItem.RoundingMode.None -> null - is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue) - is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue) - is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue) - } - } else { - null - } + val elevation by animateDpAsState( + targetValue = if (showShadow) { + TangemTheme.dimens.elevation8 + } else { + TangemTheme.dimens.elevation0 + }, + label = "item_elevation", + ) this - .let { - if (padding != null) { - it.padding(padding) - } else { - it - } - } + .padding(paddingValues = getItemGap(roundingMode)) .shadow( - elevation = if (showShadow) TangemTheme.dimens.elevation12 else TangemTheme.dimens.elevation0, - shape = shape, + elevation = elevation, + shape = getItemShape(roundingMode, radius), clip = true, ) } +} + +@Composable +@ReadOnlyComposable +private fun getItemGap(roundingMode: DraggableItem.RoundingMode): PaddingValues { + val paddingValue = TangemTheme.dimens.spacing4 + + return if (roundingMode.showGap) { + when (roundingMode) { + is DraggableItem.RoundingMode.None -> PaddingValues(all = 0.dp) + is DraggableItem.RoundingMode.All -> PaddingValues(vertical = paddingValue) + is DraggableItem.RoundingMode.Top -> PaddingValues(top = paddingValue) + is DraggableItem.RoundingMode.Bottom -> PaddingValues(bottom = paddingValue) + } + } else { + PaddingValues(all = 0.dp) + } +} + +@Stable +private fun getItemShape(roundingMode: DraggableItem.RoundingMode, radius: Dp): Shape { + return when (roundingMode) { + is DraggableItem.RoundingMode.None -> RectangleShape + is DraggableItem.RoundingMode.Top -> RoundedCornerShape( + topStart = radius, + topEnd = radius, + ) + is DraggableItem.RoundingMode.Bottom -> RoundedCornerShape( + bottomStart = radius, + bottomEnd = radius, + ) + is DraggableItem.RoundingMode.All -> RoundedCornerShape( + size = radius, + ) + } +} // region Preview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt index ec8c8655c2..a00a26f468 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt @@ -1,8 +1,8 @@ package com.tangem.feature.wallet.presentation.organizetokens import com.tangem.common.Provider -import com.tangem.core.ui.event.consumed -import com.tangem.core.ui.event.triggered +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.TokenListSortingError @@ -66,7 +66,7 @@ internal class OrganizeTokensStateHolder( fun updateStateAfterTokenListSorting(tokenList: TokenList) { updateState { tokenListConverter.convert(tokenList).copy( - scrollListToTop = triggered(::consumeScrollListToTopEvent), + scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), ) } } @@ -110,19 +110,19 @@ internal class OrganizeTokensStateHolder( ), dndConfig = OrganizeTokensState.DragAndDropConfig( onItemDragged = dragAndDropIntents::onItemDragged, - onDragStart = dragAndDropIntents::onItemDraggingStart, + onItemDragStart = dragAndDropIntents::onItemDraggingStart, onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, canDragItemOver = dragAndDropIntents::canDragItemOver, ), - scrollListToTop = consumed, + scrollListToTop = consumedEvent(), ) } - private fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { + private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { stateFlowInternal.update(block) } private fun consumeScrollListToTopEvent() { - updateState { copy(scrollListToTop = consumed) } + updateState { copy(scrollListToTop = consumedEvent()) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 5c24a976b3..a972f62260 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -1,10 +1,10 @@ package com.tangem.feature.wallet.presentation.organizetokens -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.* import arrow.core.getOrElse import com.tangem.common.Provider +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.ApplyTokenListSortingUseCase @@ -13,6 +13,7 @@ import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase import com.tangem.domain.tokens.ToggleTokenListSortingUseCase import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver @@ -20,13 +21,14 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disabl import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.router.WalletRoute +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class OrganizeTokensViewModel @Inject constructor( private val getTokenListUseCase: GetTokenListUseCase, @@ -34,8 +36,10 @@ internal class OrganizeTokensViewModel @Inject constructor( private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventsHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, -) : ViewModel(), OrganizeTokensIntents { +) : ViewModel(), DefaultLifecycleObserver, OrganizeTokensIntents { lateinit var router: InnerWalletRouter @@ -43,7 +47,6 @@ internal class OrganizeTokensViewModel @Inject constructor( private val dragAndDropAdapter = DragAndDropAdapter( listStateProvider = Provider { uiState.value.itemsState }, - scope = viewModelScope, ) private val stateHolder = OrganizeTokensStateHolder( @@ -67,12 +70,18 @@ internal class OrganizeTokensViewModel @Inject constructor( val uiState: StateFlow = stateHolder.stateFlow + override fun onCreate(owner: LifecycleOwner) { + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ScreenOpened) + } + override fun onBackClick() { router.popBackStack() } override fun onSortClick() { - viewModelScope.launch(Dispatchers.Default) { + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) + + viewModelScope.launch(dispatchers.default) { val list = tokenList ?: return@launch toggleTokenListSortingUseCase(list).fold( @@ -86,7 +95,9 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onGroupClick() { - viewModelScope.launch(Dispatchers.Default) { + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) + + viewModelScope.launch(dispatchers.default) { val list = tokenList ?: return@launch toggleTokenListGroupingUseCase(list).fold( @@ -100,37 +111,49 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onApplyClick() { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(dispatchers.default) { stateHolder.updateStateToDisplayProgress() val listState = uiState.value.itemsState val resolver = CryptoCurrenciesIdsResolver() + val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork + val isSortedByBalance = uiState.value.header.isSortedByBalance + + sendAnalyticsEvent( + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + val result = applyTokenListSortingUseCase( userWalletId = userWalletId, sortedTokensIds = resolver.resolve(listState, tokenList), - isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork, - isSortedByBalance = uiState.value.header.isSortedByBalance, + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, ) result.fold( ifLeft = stateHolder::updateStateWithError, ifRight = { stateHolder.updateStateToHideProgress() - withContext(Dispatchers.Main) { router.popBackStack() } + withContext( + dispatchers.main, + ) { router.popBackStack() } }, ) } } override fun onCancelClick() { + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Cancel) + router.popBackStack() } private fun bootstrapTokenList() { - viewModelScope.launch(Dispatchers.Default) { + viewModelScope.launch(dispatchers.default) { val maybeTokenList = getTokenListUseCase(userWalletId) - .first { it.getOrNull()?.totalFiatBalance is TokenList.FiatBalance.Loaded } + .first { it.getOrNull()?.totalFiatBalance !is TokenList.FiatBalance.Loading } maybeTokenList.fold( ifLeft = stateHolder::updateStateWithError, @@ -163,4 +186,21 @@ internal class OrganizeTokensViewModel @Inject constructor( initialValue = AppCurrency.Default, ) } + + private fun sendAnalyticsEvent(isGroupedByNetwork: Boolean, isSortedByBalance: Boolean) { + analyticsEventsHandler.send( + PortfolioOrganizeTokensAnalyticsEvent.Apply( + grouping = if (isGroupedByNetwork) { + AnalyticsParam.OnOffState.On + } else { + AnalyticsParam.OnOffState.Off + }, + organizeSortType = if (isSortedByBalance) { + AnalyticsParam.OrganizeSortType.ByBalance + } else { + AnalyticsParam.OrganizeSortType.Manually + }, + ), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt new file mode 100644 index 0000000000..4d5ccf9b2c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.organizetokens.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam + +sealed class PortfolioOrganizeTokensAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Portfolio / Organize Tokens", event, params) { + + object ScreenOpened : PortfolioOrganizeTokensAnalyticsEvent("Organize Tokens Screen Opened") + + object ByBalance : PortfolioOrganizeTokensAnalyticsEvent("Button - By Balance") + + object Group : PortfolioOrganizeTokensAnalyticsEvent("Button - Group") + + class Apply( + grouping: AnalyticsParam.OnOffState, + organizeSortType: AnalyticsParam.OrganizeSortType, + ) : PortfolioOrganizeTokensAnalyticsEvent( + "Button - Apply", + params = mapOf( + "Group" to grouping.value, + "Sort" to organizeSortType.value, + ), + ) + + object Cancel : PortfolioOrganizeTokensAnalyticsEvent("Button - Cancel") +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt index 5d367c0436..f7c411a377 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem * */ @Immutable internal sealed class DraggableItem { - abstract val id: String + abstract val id: Any abstract val roundingMode: RoundingMode abstract val showShadow: Boolean @@ -26,7 +26,7 @@ internal sealed class DraggableItem { * @property showShadow if true then item should be elevated * */ data class GroupHeader( - override val id: String, + override val id: Int, val networkName: String, override val roundingMode: RoundingMode = RoundingMode.None, override val showShadow: Boolean = false, @@ -43,7 +43,7 @@ internal sealed class DraggableItem { * */ data class Token( val tokenItemState: TokenItemState.Draggable, - val groupId: String, + val groupId: Int, override val showShadow: Boolean = false, override val roundingMode: RoundingMode = RoundingMode.None, ) : DraggableItem() { @@ -51,12 +51,11 @@ internal sealed class DraggableItem { } /** - * Helper item used to detect possible positions where a network group can be placed. - * Used only on [OrganizeTokensListState.GroupedByNetwork] and placed between network groups. + * Helper item used to detect possible positions where a draggable item can be placed. * * @property id ID of the placeholder * */ - data class GroupPlaceholder( + data class Placeholder( override val id: String, ) : DraggableItem() { override val showShadow: Boolean = false @@ -109,7 +108,7 @@ internal sealed class DraggableItem { * @return updated [DraggableItem] * */ fun updateRoundingMode(mode: RoundingMode): DraggableItem = when (this) { - is GroupPlaceholder -> this + is Placeholder -> this is GroupHeader -> this.copy(roundingMode = mode) is Token -> this.copy(roundingMode = mode) } @@ -122,7 +121,7 @@ internal sealed class DraggableItem { * @return updated [DraggableItem] * */ fun updateShadowVisibility(show: Boolean): DraggableItem = when (this) { - is GroupPlaceholder -> this + is Placeholder -> this is GroupHeader -> this.copy(showShadow = show) is Token -> this.copy(showShadow = show) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt index 3f87cab410..5d16b5035c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt @@ -13,7 +13,7 @@ internal sealed class OrganizeTokensListState { ) : OrganizeTokensListState() data class Ungrouped( - override val items: PersistentList, + override val items: PersistentList, ) : OrganizeTokensListState() object Empty : OrganizeTokensListState() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt index 860c4c5b24..a269938058 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt @@ -11,7 +11,7 @@ internal data class OrganizeTokensState( val header: HeaderConfig, val actions: ActionsConfig, val dndConfig: DragAndDropConfig, - val scrollListToTop: StateEvent, + val scrollListToTop: StateEvent, ) { data class HeaderConfig( @@ -33,6 +33,6 @@ internal data class OrganizeTokensState( val onItemDragged: (ItemPosition, ItemPosition) -> Unit, val canDragItemOver: (ItemPosition, ItemPosition) -> Boolean, val onItemDragEnd: () -> Unit, - val onDragStart: (DraggableItem) -> Unit, + val onItemDragStart: (DraggableItem) -> Unit, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt index 71690752fb..5c7d1de901 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt @@ -11,7 +11,7 @@ internal class CryptoCurrenciesIdsResolver { val draggableTokens = when (listState) { is OrganizeTokensListState.Empty -> return emptyList() is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance() - is OrganizeTokensListState.Ungrouped -> listState.items + is OrganizeTokensListState.Ungrouped -> listState.items.filterIsInstance() } val currenciesStatuses = when (tokenList) { is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt index acfcac13b7..1db8f4356d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt @@ -2,6 +2,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -internal fun getGroupPlaceholder(index: Int): DraggableItem.GroupPlaceholder { - return DraggableItem.GroupPlaceholder(id = "placeholder_${index.inc()}") +internal fun getGroupPlaceholder(index: Int): DraggableItem.Placeholder { + return DraggableItem.Placeholder(id = "placeholder_${index.inc()}") } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt index fee7cb1033..7a0beac577 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt @@ -3,20 +3,22 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.common import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem internal fun List.uniteItems(): List { - val lastItemIndex = this.lastIndex + val items = prepareItems() + val lastItemIndex = items.lastIndex - return this.mapIndexed { index, item -> + return prepareItems().mapIndexed { index, item -> val mode = when (index) { - 0 -> DraggableItem.RoundingMode.Top() + // 1 index is used because the first item is always a placeholder, check `prepareItems()` function + 1 -> DraggableItem.RoundingMode.Top() lastItemIndex -> DraggableItem.RoundingMode.Bottom() else -> when (item) { + is DraggableItem.Placeholder -> DraggableItem.RoundingMode.None is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true) - is DraggableItem.Token -> if (this[index + 1] is DraggableItem.GroupPlaceholder) { + is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) { DraggableItem.RoundingMode.Bottom(showGap = true) } else { DraggableItem.RoundingMode.None } - is DraggableItem.GroupPlaceholder -> DraggableItem.RoundingMode.None } } @@ -24,4 +26,49 @@ internal fun List.uniteItems(): List { .updateRoundingMode(mode) .updateShadowVisibility(show = false) } +} + +internal fun List.divideMovingItem(movingItem: DraggableItem): List { + val mutableList = this.toMutableList() + val listIterator = mutableList.listIterator() + + while (listIterator.hasNext()) { + val item = listIterator.next() + + if (item.id == movingItem.id) { + val dividedItem = movingItem + .updateRoundingMode(DraggableItem.RoundingMode.All()) + .updateShadowVisibility(show = true) + + listIterator.set(dividedItem) + break + } + } + + return mutableList +} + +/** + * !!! Workaround !!! + * + * We need to add a [DraggableItem.Placeholder] (since it's not draggable) as the first item of the list, because the + * [DND library](https://github.com/aclassen/ComposeReorderable) glitches when a user tries to drag the first item. + * + * @since 07.09.2023 + * */ +private fun List.prepareItems(): List { + val firstPlaceholderId = "initial_placeholder" + val items = this + + return mutableListOf().apply { + add(DraggableItem.Placeholder(firstPlaceholderId)) + + val itemsWithoutFirstPlaceholder = if (items.firstOrNull()?.id == firstPlaceholderId) { + items.drop(n = 1) + } else { + items + } + + addAll(itemsWithoutFirstPlaceholder) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt index e28e32c3a8..5cf4e81e8e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt @@ -5,4 +5,4 @@ import com.tangem.domain.tokens.models.Network internal fun getTokenItemId(currencyId: CryptoCurrency.ID): String = currencyId.value -internal fun getGroupHeaderId(networkId: Network.ID): String = networkId.value \ No newline at end of file +internal fun getGroupHeaderId(network: Network): Int = network.hashCode() \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt index 4c041f01fe..3b6cf65d8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt @@ -5,7 +5,6 @@ import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeToken import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList -@Suppress("UNCHECKED_CAST") internal inline fun OrganizeTokensListState.updateItems( update: (PersistentList) -> List, ): OrganizeTokensListState { @@ -13,7 +12,7 @@ internal inline fun OrganizeTokensListState.updateItems( return when (this) { is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems) - is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems as PersistentList) + is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems) is OrganizeTokensListState.Empty -> this } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt new file mode 100644 index 0000000000..91e8124c43 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenItemHiddenStateConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.wallet.presentation.organizetokens.utils.converter + +import com.tangem.feature.wallet.presentation.common.state.TokenItemState + +internal class TokenItemHiddenStateConverter { + + fun updateHiddenState( + optionsState: TokenItemState.TokenOptionsState, + isBalanceHidden: Boolean, + ): TokenItemState.TokenOptionsState { + return when { + !optionsState.isBalanceHidden && isBalanceHidden -> { + optionsState.copy(isBalanceHidden = true) + } + optionsState.isBalanceHidden && !isBalanceHidden -> { + optionsState.copy(isBalanceHidden = false) + } + else -> optionsState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 11f4747c2b..a6c36995b0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,12 +1,14 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items import com.tangem.common.Provider -import com.tangem.core.ui.extensions.iconResId -import com.tangem.core.ui.extensions.networkBadgeIconResId +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId @@ -16,6 +18,8 @@ internal class CryptoCurrencyToDraggableItemConverter( private val appCurrencyProvider: Provider, ) : Converter { + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { return createDraggableToken(value, appCurrencyProvider()) } @@ -32,7 +36,7 @@ internal class CryptoCurrencyToDraggableItemConverter( ): DraggableItem.Token { return DraggableItem.Token( tokenItemState = createTokenItemState(currencyStatus, appCurrency), - groupId = getGroupHeaderId(currencyStatus.currency.network.id), + groupId = getGroupHeaderId(currencyStatus.currency.network), ) } @@ -44,12 +48,13 @@ internal class CryptoCurrencyToDraggableItemConverter( return TokenItemState.Draggable( id = getTokenItemId(currency.id), - tokenIconUrl = currency.iconUrl, - tokenIconResId = currencyStatus.currency.iconResId, - networkBadgeIconResId = currencyStatus.currency.networkBadgeIconResId, + icon = iconStateConverter.convert(currencyStatus), name = currency.name, - fiatAmount = getFormattedFiatAmount(currencyStatus, appCurrency), - isTestnet = currencyStatus.currency.network.isTestnet, + info = if (currencyStatus.value.isError) { + resourceReference(id = R.string.common_unreachable) + } else { + stringReference(getFormattedFiatAmount(currencyStatus, appCurrency)) + }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt index ddd09e1da9..0205df476a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -31,11 +31,11 @@ internal class NetworkGroupToDraggableItemsConverter( } private fun createGroupHeader(group: NetworkGroup) = DraggableItem.GroupHeader( - id = getGroupHeaderId(group.network.id), + id = getGroupHeaderId(group.network), networkName = group.network.name, ) private fun createTokens(group: NetworkGroup): List { - return itemConverter.convertList(group.currencies.toList()) + return itemConverter.convertList(group.currencies) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt index c73798c77e..45dcb02cee 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt @@ -4,21 +4,17 @@ import com.tangem.common.Provider import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems import kotlinx.collections.immutable.mutate -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.launch import org.burnoutcrew.reorderable.ItemPosition internal class DragAndDropAdapter( private val listStateProvider: Provider, - private val scope: CoroutineScope, ) : DragAndDropIntents { private val draggableGroupsOperations = DraggableGroupsOperations() @@ -37,9 +33,12 @@ internal class DragAndDropAdapter( get() = listStateFlowInternal override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { - val items = (currentListState as? OrganizeTokensListState.GroupedByNetwork) - ?.items - ?: return true // If ungrouped then item can be moved anywhere + val items = when (val listState = currentListState) { + is OrganizeTokensListState.GroupedByNetwork -> listState.items + is OrganizeTokensListState.Empty, + is OrganizeTokensListState.Ungrouped, + -> return true // If ungrouped then item can be moved anywhere + } val (dragOverItem, draggingItem) = findItemsToMove( items = items, @@ -54,7 +53,7 @@ internal class DragAndDropAdapter( return when (draggingItem) { is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex) is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem) - is DraggableItem.GroupPlaceholder -> false + is DraggableItem.Placeholder -> false } } @@ -64,11 +63,11 @@ internal class DragAndDropAdapter( updateListState { when (item) { - is DraggableItem.GroupPlaceholder -> items + is DraggableItem.Placeholder -> items is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) is DraggableItem.Token -> when (this) { - is OrganizeTokensListState.GroupedByNetwork -> draggableGroupsOperations.divideGroups(items, item) - is OrganizeTokensListState.Ungrouped -> divideTokens(items, item) + is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item) + is OrganizeTokensListState.Ungrouped -> items.divideMovingItem(item) is OrganizeTokensListState.Empty -> items } } @@ -76,21 +75,17 @@ internal class DragAndDropAdapter( } override fun onItemDraggingEnd() { - scope.launch(Dispatchers.IO) { - val draggingItem = currentDraggingItem ?: return@launch + val draggingItem = currentDraggingItem ?: return - delay(FINISH_DRAGGING_DELAY_MILLIS) - - updateListState { - when (draggingItem) { - is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) - is DraggableItem.Token -> items.uniteItems() - is DraggableItem.GroupPlaceholder -> items - } + updateListState { + when (draggingItem) { + is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) + is DraggableItem.Token -> items.uniteItems() + is DraggableItem.Placeholder -> items } - - currentDraggingItem = null } + + currentDraggingItem = null } override fun onItemDragged(from: ItemPosition, to: ItemPosition) = updateListState { @@ -137,7 +132,7 @@ internal class DragAndDropAdapter( return when { moveOverItemPosition.index == 0 -> true moveOverItemPosition.index == lastItemIndex -> true - moveOverItem is DraggableItem.GroupPlaceholder -> true + moveOverItem is DraggableItem.Placeholder -> true else -> false } } @@ -147,23 +142,7 @@ internal class DragAndDropAdapter( return when (moveOverItem) { is DraggableItem.GroupHeader -> false // Token item can not be moved to group item is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group - is DraggableItem.GroupPlaceholder -> false + is DraggableItem.Placeholder -> false } } - - @Suppress("UNCHECKED_CAST") // Erased type - private fun divideTokens( - items: List, - movingItem: DraggableItem.Token, - ): List { - return items.map { token -> - token - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = token.id == movingItem.id) - } as List - } - - private companion object { - const val FINISH_DRAGGING_DELAY_MILLIS = 200L - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt index d28133195a..3891fdea66 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt @@ -1,12 +1,13 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems internal class DraggableGroupsOperations { - private var groupIdToTokens: Map>? = null + private var groupIdToTokens: Map>? = null fun collapseGroup(items: List, movingGroup: DraggableItem.GroupHeader): List { if (!groupIdToTokens.isNullOrEmpty()) return items @@ -20,7 +21,7 @@ internal class DraggableGroupsOperations { it is DraggableItem.Token && it.groupId == movingGroup.id } - return divideGroups(itemsWithoutGroupTokens, movingGroup) + return itemsWithoutGroupTokens.divideMovingItem(movingGroup) } fun expandGroups(items: List): List { @@ -45,62 +46,4 @@ internal class DraggableGroupsOperations { return expandedGroups } - - fun divideGroups(items: List, movingItem: DraggableItem): List { - val lastItemIndex = items.lastIndex - - return items.mapIndexed { index, item -> - when { - // Case when current item is the moving item - item.id == movingItem.id -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = true) - } - // Case when moving item is a token and current item is the group of the moving token - movingItem is DraggableItem.Token && item.id == movingItem.groupId -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = true) - } - // Case when both moving item and current item are tokens and belong to the same group - movingItem is DraggableItem.Token && - item is DraggableItem.Token && item.groupId == movingItem.groupId -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.All(showGap = true)) - .updateShadowVisibility(show = false) - } - // Case when current item is the first item in the list - index == 0 -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Top()) - .updateShadowVisibility(show = false) - } - // Case when current item is the last item in the list - index == lastItemIndex -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Bottom()) - .updateShadowVisibility(show = false) - } - // Case when previous item is a GroupPlaceholder - items[index - 1] is DraggableItem.GroupPlaceholder -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Top(showGap = true)) - .updateShadowVisibility(show = false) - } - // Case when next item is a GroupPlaceholder - items[index + 1] is DraggableItem.GroupPlaceholder -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.Bottom(showGap = true)) - .updateShadowVisibility(show = false) - } - // Default case when none of the above conditions are met - else -> { - item - .updateRoundingMode(DraggableItem.RoundingMode.None) - .updateShadowVisibility(show = false) - } - } - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/PortfolioEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/PortfolioEvent.kt new file mode 100644 index 0000000000..85553a1445 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/PortfolioEvent.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class PortfolioEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Portfolio", event, params) { + + object Refreshed : PortfolioEvent("Refreshed") + + object ButtonManageTokens : PortfolioEvent("Button - Manage Tokens") + + object TokenTapped : PortfolioEvent("Token is Tapped") + + object OrganizeTokens : PortfolioEvent("Button - Organize Tokens") +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt new file mode 100644 index 0000000000..4eabe0f5b5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.wallet.presentation.wallet.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam + +sealed class WalletScreenAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Main Screen", event, params) { + + object ScreenOpened : WalletScreenAnalyticsEvent("Screen opened") + object WalletSwipe : WalletScreenAnalyticsEvent("Wallet Swipe") + + class EnableBiometrics(state: AnalyticsParam.OnOffState) : WalletScreenAnalyticsEvent( + event = "Enable Biometric", + params = mapOf("State" to state.value), + ) + + // TODO [REDACTED_JIRA] + class NoticeRateAppButton(result: AnalyticsParam.RateApp) : WalletScreenAnalyticsEvent( + event = "Notice - Rate The App Button Tapped", + params = mapOf("Result" to result.value), + ) + + object NoticeBackupYourWalletTapped : WalletScreenAnalyticsEvent("Notice - Backup Your Wallet Tapped") + object NoticeScanYourCardTapped : WalletScreenAnalyticsEvent("Notice - Scan Your Card Tapped") + object NoticeWalletLocked : WalletScreenAnalyticsEvent("Notice - Wallet Locked") +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt new file mode 100644 index 0000000000..ab7e236835 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UserWalletExt.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.wallets.models.UserWallet + +fun UserWallet.getCardsCount(): Int? { + return if (isMultiCurrency) { + when (val status = scanResponse.card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount + 1 + is CardDTO.BackupStatus.CardLinked -> status.cardCount + 1 + is CardDTO.BackupStatus.NoBackup -> 1 + null -> 1 // Multi-currency wallet without backup function. Example, 4.12 + } + } else { + null + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 4b03d163f5..9124311a34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -2,9 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.plus +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R import java.math.BigDecimal @@ -16,55 +17,73 @@ import java.math.BigDecimal */ internal object WalletAdditionalInfoFactory { - private val DIVIDER_RES by lazy { TextReference.Str(value = " • ") } + private val DIVIDER by lazy(mode = LazyThreadSafetyMode.NONE) { stringReference(value = " • ") } /** * Get additional info * - * @param cardTypesResolver card type resolver * @param wallet current wallet * @param currencyAmount amount of currency */ - fun resolve( - cardTypesResolver: CardTypesResolver, - wallet: UserWallet, - currencyAmount: BigDecimal? = null, - ): TextReference { - return if (cardTypesResolver.isMultiwalletAllowed()) { - resolveMultiCurrencyInfo(cardTypesResolver, wallet) + fun resolve(wallet: UserWallet, currencyAmount: BigDecimal? = null): TextReference { + return if (wallet.isMultiCurrency) { + wallet.resolveMultiCurrencyInfo() } else { - resolveSingleCurrencyInfo(cardTypesResolver, wallet, currencyAmount) + wallet.resolveSingleCurrencyInfo(currencyAmount) } } - private fun resolveMultiCurrencyInfo(cardTypeResolver: CardTypesResolver, wallet: UserWallet): TextReference { - val backupCardsCount = wallet.cardsInWallet.size + 1 - val backupInfoRes = TextReference.PluralRes( - id = R.plurals.card_label_card_count, - count = backupCardsCount, - formatArgs = wrappedList(backupCardsCount), - ) - - return if (wallet.isLocked) { - backupInfoRes + DIVIDER_RES + TextReference.Res(R.string.common_locked) + private fun UserWallet.resolveMultiCurrencyInfo(): TextReference { + return if (isLocked) { + getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res(R.string.common_locked) } else { + val cardTypeResolver = scanResponse.cardTypesResolver if (cardTypeResolver.isWallet2()) { - backupInfoRes + DIVIDER_RES + TextReference.Res(id = R.string.common_seed_phrase) + resolveWallet2Info() } else { - backupInfoRes + getBackupInfo(backupCardsCount = getCardsCount()) } } } - private fun resolveSingleCurrencyInfo( - cardTypeResolver: CardTypesResolver, - wallet: UserWallet, - currencyAmount: BigDecimal?, - ): TextReference { - return if (wallet.isLocked) { + private fun UserWallet.resolveWallet2Info(): TextReference { + return if (isImported) { + getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + + TextReference.Res(id = R.string.common_seed_phrase) + } else { + getBackupInfo(backupCardsCount = getCardsCount()) + } + } + + private fun getBackupInfoWithDivider(backupCardsCount: Int?): TextReference { + return if (backupCardsCount != null) { + getBackupInfoTextReference(count = backupCardsCount) + DIVIDER + } else { + TextReference.EMPTY + } + } + + private fun getBackupInfo(backupCardsCount: Int?): TextReference { + return if (backupCardsCount != null) { + getBackupInfoTextReference(count = backupCardsCount) + } else { + TextReference.EMPTY + } + } + + private fun getBackupInfoTextReference(count: Int): TextReference { + return TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = count, + formatArgs = wrappedList(count), + ) + } + + private fun UserWallet.resolveSingleCurrencyInfo(currencyAmount: BigDecimal?): TextReference { + return if (isLocked) { TextReference.Res(R.string.common_locked) } else { - val blockchain = cardTypeResolver.getBlockchain() + val blockchain = scanResponse.cardTypesResolver.getBlockchain() val amount = currencyAmount?.let { BigDecimalFormatter.formatCryptoAmount( cryptoAmount = it, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index c54155dd33..c262b71ea3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -2,7 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.domain import androidx.annotation.DrawableRes import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.impl.R /** @@ -12,34 +13,41 @@ import com.tangem.feature.wallet.impl.R */ internal object WalletImageResolver { - private const val DOUBLE_WALLET_SET_BACKUP_COUNT = 1 - private const val TRIPLE_WALLET_SET_BACKUP_COUNT = 2 + private const val DOUBLE_WALLET_SET_BACKUP_COUNT = 2 + private const val TRIPLE_WALLET_SET_BACKUP_COUNT = 3 - /** Get image by [cardTypesResolver] */ + /** Get a specified wallet [userWallet] image */ @DrawableRes - fun resolve(cardTypesResolver: CardTypesResolver): Int? { + fun resolve(userWallet: UserWallet): Int? { + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver return when { - cardTypesResolver.isWallet2() -> resolveWallet2(cardTypesResolver) + cardTypesResolver.isWallet2() -> userWallet.resolveWallet2() cardTypesResolver.isTangemWallet() -> R.drawable.ill_wallet_120_106 cardTypesResolver.isWhiteWallet() -> R.drawable.ill_old_wallet_120_106 cardTypesResolver.isTangemTwins() -> R.drawable.ill_twin_120_106 cardTypesResolver.isStart2Coin() -> R.drawable.ill_start2coin_120_106 - cardTypesResolver.isTangemNote() -> resolveNote(cardTypesResolver) - cardTypesResolver.isDev() -> R.drawable.ill_dev_120_106 + cardTypesResolver.isTangemNote() -> resolveNote(blockchain = cardTypesResolver.getBlockchain()) + cardTypesResolver.isDevKit() -> R.drawable.ill_dev_120_106 else -> null } } - private fun resolveWallet2(cardTypesResolver: CardTypesResolver): Int? { - return when (cardTypesResolver.getBackupCardsCount()) { - DOUBLE_WALLET_SET_BACKUP_COUNT -> R.drawable.ill_wallet2_cards2_120_106 - TRIPLE_WALLET_SET_BACKUP_COUNT -> R.drawable.ill_wallet2_cards3_120_106 - else -> null + private fun UserWallet.resolveWallet2(): Int? { + val count = getCardsCount() + + return if (count != null) { + when (count) { + DOUBLE_WALLET_SET_BACKUP_COUNT -> R.drawable.ill_wallet2_cards2_120_106 + TRIPLE_WALLET_SET_BACKUP_COUNT -> R.drawable.ill_wallet2_cards3_120_106 + else -> null + } + } else { + null } } - private fun resolveNote(cardTypesResolver: CardTypesResolver): Int? { - return when (cardTypesResolver.getBlockchain()) { + private fun resolveNote(blockchain: Blockchain): Int? { + return when (blockchain) { Blockchain.Bitcoin -> R.drawable.ill_note_btc_120_106 Blockchain.Ethereum -> R.drawable.ill_note_ethereum_120_106 Blockchain.BSC -> R.drawable.ill_note_binance_120_106 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt index 934bfd6230..2c95c35f55 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/TokenActionButtonConfig.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference /** * Action button config @@ -11,7 +12,7 @@ import androidx.annotation.DrawableRes * @property enabled enabled */ data class TokenActionButtonConfig( - val text: String, + val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, val enabled: Boolean = true, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt new file mode 100644 index 0000000000..61c176f094 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletEvent.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class WalletEvent { + + data class ChangeWallet(val index: Int) : WalletEvent() + + data class ShowError(val text: TextReference) : WalletEvent() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt index d22b1b6db6..d91449abb4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMultiCurrencyState.kt @@ -1,5 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.state +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.feature.wallet.presentation.wallet.state.components.* import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -20,8 +23,9 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() { override val walletsListConfig: WalletsListConfig, override val pullToRefreshConfig: WalletPullToRefreshConfig, override val notifications: ImmutableList, - override val bottomSheetConfig: WalletBottomSheetConfig?, + override val bottomSheetConfig: TangemBottomSheetConfig?, override val tokensListState: WalletTokensListState, + override val event: StateEvent = consumedEvent(), val tokenActionsBottomSheet: ActionsBottomSheetConfig?, val onManageTokensClick: () -> Unit, ) : WalletMultiCurrencyState() @@ -36,16 +40,17 @@ internal sealed class WalletMultiCurrencyState : WalletState.ContentState() { override val onScanClick: () -> Unit, override val isBottomSheetShow: Boolean = false, override val onBottomSheetDismiss: () -> Unit = {}, + override val event: StateEvent = consumedEvent(), ) : WalletMultiCurrencyState(), WalletLockedState { override val notifications = persistentListOf( WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick), ) - override val bottomSheetConfig = WalletBottomSheetConfig( + override val bottomSheetConfig = TangemBottomSheetConfig( isShow = isBottomSheetShow, onDismissRequest = onBottomSheetDismiss, - content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + content = WalletBottomSheetConfig.UnlockWallets( onUnlockClick = onUnlockClick, onScanClick = onScanClick, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt index 6991db467a..6a5467d025 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletSingleCurrencyState.kt @@ -2,11 +2,15 @@ package com.tangem.feature.wallet.presentation.wallet.state import androidx.compose.runtime.Immutable import androidx.paging.PagingData +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.event.consumedEvent import com.tangem.feature.wallet.presentation.wallet.state.components.* import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -19,7 +23,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { /** Manage buttons */ - abstract val buttons: ImmutableList + abstract val buttons: PersistentList /** Transactions history state */ abstract val txHistoryState: TxHistoryState @@ -30,9 +34,10 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { override val walletsListConfig: WalletsListConfig, override val pullToRefreshConfig: WalletPullToRefreshConfig, override val notifications: ImmutableList, - override val bottomSheetConfig: WalletBottomSheetConfig?, - override val buttons: ImmutableList, + override val bottomSheetConfig: TangemBottomSheetConfig?, + override val buttons: PersistentList, override val txHistoryState: TxHistoryState, + override val event: StateEvent = consumedEvent(), val marketPriceBlockState: MarketPriceBlockState, ) : WalletSingleCurrencyState() @@ -41,12 +46,13 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, override val pullToRefreshConfig: WalletPullToRefreshConfig, - override val buttons: ImmutableList, + override val buttons: PersistentList, override val onUnlockWalletsNotificationClick: () -> Unit, override val onUnlockClick: () -> Unit, override val onScanClick: () -> Unit, override val isBottomSheetShow: Boolean = false, override val onBottomSheetDismiss: () -> Unit = {}, + override val event: StateEvent = consumedEvent(), val onExploreClick: () -> Unit, ) : WalletSingleCurrencyState(), WalletLockedState { @@ -54,10 +60,10 @@ internal sealed class WalletSingleCurrencyState : WalletState.ContentState() { WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick), ) - override val bottomSheetConfig = WalletBottomSheetConfig( + override val bottomSheetConfig = TangemBottomSheetConfig( isShow = isBottomSheetShow, onDismissRequest = onBottomSheetDismiss, - content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + content = WalletBottomSheetConfig.UnlockWallets( onUnlockClick = onUnlockClick, onScanClick = onScanClick, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt index c9c327f540..9872db9572 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletState.kt @@ -1,6 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.state -import com.tangem.feature.wallet.presentation.wallet.state.components.* +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.event.StateEvent +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import kotlinx.collections.immutable.ImmutableList /** @@ -29,30 +34,51 @@ internal sealed class WalletState { abstract val notifications: ImmutableList /** Bottom sheet config */ - abstract val bottomSheetConfig: WalletBottomSheetConfig? + abstract val bottomSheetConfig: TangemBottomSheetConfig? + + /** State event */ + abstract val event: StateEvent /** * Util function that allow to make a copy * * @param walletsListConfig wallets list config * @param pullToRefreshConfig pull to refresh config + * @param event state event */ fun copySealed( walletsListConfig: WalletsListConfig = this.walletsListConfig, pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig, + event: StateEvent = this.event, ): ContentState { return when (this) { is WalletMultiCurrencyState.Content -> { - copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig) + copy( + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + event = event, + ) } is WalletMultiCurrencyState.Locked -> { - copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig) + copy( + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + event = event, + ) } is WalletSingleCurrencyState.Content -> { - copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig) + copy( + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + event = event, + ) } is WalletSingleCurrencyState.Locked -> { - copy(walletsListConfig = walletsListConfig, pullToRefreshConfig = pullToRefreshConfig) + copy( + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + event = event, + ) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt index eef9a95ef0..44d45cf615 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletBottomSheetConfig.kt @@ -2,98 +2,51 @@ package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.annotation.DrawableRes import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemColorPalette import com.tangem.feature.wallet.impl.R /** * Wallet bottom sheet config * - * @property isShow flag that determine if bottom sheet is shown - * @property onDismissRequest lambda be invoked when bottom sheet is dismissed - * @property content content config - * [REDACTED_AUTHOR] */ -// TODO: Finalize notification strings [REDACTED_JIRA] -internal data class WalletBottomSheetConfig( - val isShow: Boolean, - val onDismissRequest: () -> Unit, - val content: BottomSheetContentConfig, -) { +sealed class WalletBottomSheetConfig( + open val title: TextReference, + open val subtitle: TextReference, + @DrawableRes open val iconResId: Int, + open val tint: Color? = null, + val primaryButtonConfig: ButtonConfig, + val secondaryButtonConfig: ButtonConfig, +) : TangemBottomSheetConfigContent { - sealed class BottomSheetContentConfig( - open val title: TextReference, - open val subtitle: TextReference, - @DrawableRes open val iconResId: Int, - open val tint: Color? = null, - val primaryButtonConfig: ButtonConfig, - val secondaryButtonConfig: ButtonConfig, - ) { + data class ButtonConfig( + val text: TextReference, + val onClick: () -> Unit, + @DrawableRes val iconResId: Int? = null, + ) - data class ButtonConfig( - val text: TextReference, - val onClick: () -> Unit, - @DrawableRes val iconResId: Int? = null, - ) - - data class UnlockWallets( - val onUnlockClick: () -> Unit, - val onScanClick: () -> Unit, - ) : BottomSheetContentConfig( - title = TextReference.Str(value = "Unlock needed"), - subtitle = TextReference.Str( - value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + - "incididunt ut labore et dolore magna aliqua.", + data class UnlockWallets(val onUnlockClick: () -> Unit, val onScanClick: () -> Unit) : WalletBottomSheetConfig( + title = resourceReference(id = R.string.common_unlock_needed), + subtitle = resourceReference( + id = R.string.unlock_wallet_description_full, + formatArgs = wrappedList( + resourceReference(R.string.common_biometrics), ), - iconResId = R.drawable.ic_locked_24, - tint = TangemColorPalette.Black, - primaryButtonConfig = ButtonConfig(text = TextReference.Str(value = "Unlock"), onClick = onUnlockClick), - secondaryButtonConfig = ButtonConfig( - text = TextReference.Str(value = "Scan card"), - onClick = onScanClick, - iconResId = R.drawable.ic_tangem_24, - ), - ) - - data class LikeTangemApp( - val onRateTheAppClick: () -> Unit, - val onShareClick: () -> Unit, - ) : BottomSheetContentConfig( - title = TextReference.Str(value = "Like Tangem App?"), - subtitle = TextReference.Str(value = "How was your experience with our app? Let us know:"), - iconResId = R.drawable.ic_star_24, - tint = TangemColorPalette.Tangerine, - primaryButtonConfig = ButtonConfig( - text = TextReference.Str(value = "Rate the app"), - onClick = onRateTheAppClick, - ), - secondaryButtonConfig = ButtonConfig( - text = TextReference.Str(value = "Share feedback"), - onClick = onShareClick, - ), - ) - - data class CriticalWarningAlreadySignedHashes( - val onOkClick: () -> Unit, - val onCancelClick: () -> Unit, - ) : BottomSheetContentConfig( - title = TextReference.Res( - id = R.string.warning_important_security_info, - formatArgs = WrappedList(listOf("\u26A0")), - ), - subtitle = TextReference.Res(id = R.string.alert_signed_hashes_message), - iconResId = R.drawable.img_attention_20, - tint = null, - primaryButtonConfig = ButtonConfig( - text = TextReference.Res(id = R.string.common_ok), - onClick = onOkClick, - ), - secondaryButtonConfig = ButtonConfig( - text = TextReference.Res(id = R.string.common_cancel), - onClick = onCancelClick, - ), - ) - } + ), + iconResId = R.drawable.ic_locked_24, + tint = TangemColorPalette.Black, + primaryButtonConfig = ButtonConfig( + text = resourceReference(id = R.string.user_wallet_list_unlock_all), + onClick = onUnlockClick, + ), + secondaryButtonConfig = ButtonConfig( + text = resourceReference(id = R.string.welcome_unlock_card), + onClick = onScanClick, + iconResId = R.drawable.ic_tangem_24, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt index e25b851c21..c9830c2a7b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletCardState.kt @@ -15,9 +15,6 @@ internal sealed interface WalletCardState { /** Title */ val title: String - /** Additional text */ - val additionalInfo: TextReference? - /** Wallet image resource id */ @get:DrawableRes val imageResId: Int? @@ -33,19 +30,19 @@ internal sealed interface WalletCardState { * * @property id wallet id * @property title wallet name - * @property additionalInfo wallet additional info * @property imageResId wallet image resource id * @property onRenameClick lambda be invoked when Rename button is clicked * @property onDeleteClick lambda be invoked when Delete button is clicked + * @property additionalInfo wallet additional info * @property balance wallet balance */ data class Content( override val id: UserWalletId, override val title: String, - override val additionalInfo: TextReference, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, + val additionalInfo: TextReference, val balance: String, ) : WalletCardState @@ -54,18 +51,20 @@ internal sealed interface WalletCardState { * * @property id wallet id * @property title wallet name - * @property additionalInfo wallet additional info * @property imageResId wallet image resource id * @property onRenameClick lambda be invoked when Rename button is clicked * @property onDeleteClick lambda be invoked when Delete button is clicked + * @property additionalInfo wallet additional info + * @property balance wallet balance */ data class HiddenContent( override val id: UserWalletId, override val title: String, - override val additionalInfo: TextReference = HIDDEN_BALANCE_TEXT, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, + val additionalInfo: TextReference, + val balance: String, ) : WalletCardState /** @@ -73,18 +72,18 @@ internal sealed interface WalletCardState { * * @property id wallet id * @property title wallet name - * @property additionalInfo wallet additional info * @property imageResId wallet image resource id * @property onRenameClick lambda be invoked when Rename button is clicked * @property onDeleteClick lambda be invoked when Delete button is clicked + * @property additionalInfo wallet additional info */ data class LockedContent( override val id: UserWalletId, override val title: String, - override val additionalInfo: TextReference? = null, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, + val additionalInfo: TextReference, ) : WalletCardState /** @@ -92,7 +91,6 @@ internal sealed interface WalletCardState { * * @property id wallet id * @property title wallet name - * @property additionalInfo wallet additional info * @property imageResId wallet image resource id * @property onRenameClick lambda be invoked when Rename button is clicked * @property onDeleteClick lambda be invoked when Delete button is clicked @@ -100,7 +98,6 @@ internal sealed interface WalletCardState { data class Error( override val id: UserWalletId, override val title: String, - override val additionalInfo: TextReference = EMPTY_BALANCE_TEXT, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, @@ -109,17 +106,15 @@ internal sealed interface WalletCardState { /** * Wallet card loading state * - * @property id wallet id - * @property title wallet name - * @property additionalInfo wallet additional info - * @property imageResId wallet image resource id - * @property onRenameClick lambda be invoked when Rename button is clicked - * @property onDeleteClick lambda be invoked when Delete button is clicked + * @property id wallet id + * @property title wallet name + * @property imageResId wallet image resource id + * @property onRenameClick lambda be invoked when Rename button is clicked + * @property onDeleteClick lambda be invoked when Delete button is clicked */ data class Loading( override val id: UserWalletId, override val title: String, - override val additionalInfo: TextReference? = null, override val imageResId: Int?, override val onRenameClick: (UserWalletId, String) -> Unit, override val onDeleteClick: (UserWalletId) -> Unit, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 60abf781ae..c38b2b3bfa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -1,180 +1,155 @@ package com.tangem.feature.wallet.presentation.wallet.state.components import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.notifications.NotificationState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.WrappedList -import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.* import com.tangem.feature.wallet.impl.R /** * Wallet notification component state * - * @property state state + * @property config state * [REDACTED_AUTHOR] */ -// TODO: Finalize notification strings [REDACTED_JIRA] @Immutable -sealed class WalletNotification(open val state: NotificationState) { +sealed class WalletNotification(val config: NotificationConfig) { - /** Clickable notification */ - sealed interface Clickable { + sealed class Critical(title: TextReference, subtitle: TextReference) : WalletNotification( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_circle_24, + ), + ) { - /** Lambda be invoked when notification is clicked */ - val onClick: () -> Unit + object DevCard : Critical( + title = resourceReference(id = R.string.common_warning), + subtitle = resourceReference(id = R.string.alert_developer_card), + ) + + object DemoCard : Critical( + title = resourceReference(id = R.string.common_warning), + subtitle = resourceReference(id = R.string.alert_demo_message), + ) + + object TestNetCard : Critical( + title = resourceReference(id = R.string.common_warning), + subtitle = resourceReference(id = R.string.warning_testnet_card_message), + ) + + object FailedCardValidation : Critical( + title = resourceReference(id = R.string.warning_failed_to_verify_card_title), + subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), + ) + + data class LowSignatures(val count: Int) : Critical( + title = resourceReference(id = R.string.common_warning), + subtitle = resourceReference( + id = R.string.warning_low_signatures_format, + formatArgs = wrappedList(count), + ), + ) } - /** "Development card" notification */ - object DevCard : WalletNotification( - state = NotificationState.Simple( - title = TextReference.Res(id = R.string.common_warning), - subtitle = TextReference.Res(id = R.string.alert_developer_card), - iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, + sealed class Warning( + title: TextReference, + subtitle: TextReference, + buttonsState: NotificationConfig.ButtonsState? = null, + onClick: (() -> Unit)? = null, + ) : WalletNotification( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = R.drawable.img_attention_20, + buttonsState = buttonsState, + onClick = onClick, ), - ) + ) { - /** "Test card" notification */ - object TestCard : WalletNotification( - state = NotificationState.Simple( - title = TextReference.Res(id = R.string.common_warning), - subtitle = TextReference.Res(id = R.string.warning_testnet_card_message), - iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, - ), - ) + data class MissingBackup(val onStartBackupClick: () -> Unit) : Warning( + title = resourceReference(id = R.string.main_no_backup_warning_title), + subtitle = resourceReference(id = R.string.main_no_backup_warning_subtitle), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = R.string.button_start_backup_process), + onClick = onStartBackupClick, + ), + ) - /** "Demo card" notification */ - object DemoCard : WalletNotification( - state = NotificationState.Simple( - title = TextReference.Res(id = R.string.common_warning), - subtitle = TextReference.Res(id = R.string.alert_demo_message), - iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, - ), - ) + object NetworksUnreachable : Warning( + title = resourceReference(id = R.string.wallet_balance_blockchain_unreachable), + subtitle = resourceReference(id = R.string.warning_subtitle_network_unreachable), + ) - /** "Card verification failed" notification */ - object CardVerificationFailed : WalletNotification( - state = NotificationState.Simple( - title = TextReference.Res(id = R.string.warning_failed_to_verify_card_title), - subtitle = TextReference.Res(id = R.string.warning_failed_to_verify_card_message), - iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, - ), - ) + object SomeNetworksUnreachable : Warning( + title = resourceReference(id = R.string.warning_title_some_networks_unreachable), + subtitle = resourceReference(id = R.string.warning_subtitle_some_networks_unreachable), + ) - /** - * "Remaining signatures left" notification - * - * @param count number of remaining signatures - */ - class RemainingSignaturesLeft(count: Int) : WalletNotification( - state = NotificationState.Simple( - title = TextReference.Res(id = R.string.common_warning), - subtitle = TextReference.Res( - id = R.string.warning_low_signatures_format, - formatArgs = WrappedList(data = listOf(count)), + data class TopUpNote(val errorMessage: String) : Warning( + title = resourceReference(id = R.string.warning_title_note_top_up), + subtitle = stringReference(value = errorMessage), + ) + + object NumberOfSignedHashesIncorrect : Warning( + title = resourceReference(id = R.string.common_warning), + subtitle = resourceReference(id = R.string.alert_card_signed_transactions), + ) + + data class MultiWalletSignedHashesIncorrect(val onClick: () -> Unit) : Warning( + title = resourceReference(id = R.string.common_warning), + subtitle = resourceReference(id = R.string.warning_signed_tx_previously), + onClick = onClick, + ) + } + + data class MissingAddresses(val missingAddressesCount: Int, val onGenerateClick: () -> Unit) : WalletNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.main_warning_missing_derivation_title), + subtitle = pluralReference( + id = R.plurals.main_warning_missing_derivation_description, + count = missingAddressesCount, + formatArgs = wrappedList(missingAddressesCount), ), iconResId = R.drawable.ic_alert_circle_24, - tint = TangemColorPalette.Amaranth, - ), - ) - - /** - * "Already topped up and signed hashes" warning notification - * - * @property onClick lambda be invoked when notification's close button is clicked - */ - data class WarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Closable( - title = TextReference.Res(id = R.string.common_warning), - subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions), - iconResId = R.drawable.img_attention_20, - tint = null, - onCloseClick = onClick, - ), - ) - - /** - * "Already signed hashes" critical warning notification - * - * @property onClick lambda be invoked when notification is clicked - */ - data class CriticalWarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( - title = TextReference.Res( - id = R.string.warning_important_security_info, - formatArgs = WrappedList(listOf("\u26A0")), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(id = R.string.common_generate_addresses), + iconResId = R.drawable.ic_tangem_24, + onClick = onGenerateClick, ), - subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously), - iconResId = R.drawable.img_attention_20, - onClick = onClick, - tint = null, ), ) - /** - * "Backup the card" notification - * - * @property onClick lambda be invoked when notification is clicked - */ - data class BackupCard(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( - title = TextReference.Str(value = "Backup your card"), - iconResId = R.drawable.img_attention_20, - onClick = onClick, - tint = null, - ), - ) - - /** "Unreachable networks" notification */ - object UnreachableNetworks : WalletNotification( - state = NotificationState.Simple( - title = TextReference.Str(value = "Some networks are unreachable"), - iconResId = R.drawable.img_attention_20, - tint = null, - ), - ) - - /** - * "Like Tangem App" notification - * - * @property onClick lambda be invoked when notification is clicked - */ - data class LikeTangemApp(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( - title = TextReference.Str(value = "Like Tangem App?"), - iconResId = R.drawable.ic_star_24, - onClick = onClick, - tint = TangemColorPalette.Tangerine, - ), - ) - - /** - * "Scan the card" notification - * - * @property onClick lambda be invoked when notification is clicked - */ - data class ScanCard(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( - title = TextReference.Str(value = "Scan your card to continue"), - iconResId = R.drawable.ic_tangem_24, - onClick = onClick, - ), - ) - - /** - * "Unlock wallets" notification - * - * @property onClick lambda be invoked when notification is clicked - */ - data class UnlockWallets(override val onClick: () -> Unit) : Clickable, WalletNotification( - state = NotificationState.Clickable( - title = TextReference.Str(value = "Unlock needed"), + data class UnlockWallets(val onClick: () -> Unit) : WalletNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.common_unlock_needed), + subtitle = resourceReference( + id = R.string.unlock_wallet_description_short, + formatArgs = wrappedList( + resourceReference(R.string.common_biometrics), + ), + ), iconResId = R.drawable.ic_locked_24, onClick = onClick, ), ) + + data class RateApp( + val onPositiveClick: () -> Unit, + val onNegativeClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.warning_rate_app_title), + subtitle = resourceReference(id = R.string.warning_rate_app_message), + iconResId = R.drawable.ic_star_24, + buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig( + primaryText = resourceReference(id = R.string.warning_button_love_it), + onPrimaryClick = onPositiveClick, + secondaryText = resourceReference(id = R.string.warning_button_can_be_better), + onSecondaryClick = onNegativeClick, + ), + onCloseClick = onCloseClick, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt index 8b973054f2..b81f513cc0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTokensListState.kt @@ -5,6 +5,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import javax.annotation.concurrent.Immutable /** * Wallet tokens list state @@ -20,11 +21,10 @@ internal sealed class WalletTokensListState { * Wallet content token list state * * @property items content items - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked */ sealed class ContentState( open val items: ImmutableList, - open val onOrganizeTokensClick: (() -> Unit)?, + open val organizeTokensButton: OrganizeTokensButtonState, ) : WalletTokensListState() /** @@ -37,44 +37,73 @@ internal sealed class WalletTokensListState { TokensListItemState.Token(state = TokenItemState.Loading(id = FIRST_LOADING_TOKEN_ID)), TokensListItemState.Token(state = TokenItemState.Loading(id = SECOND_LOADING_TOKEN_ID)), ), - ) : ContentState(items = items, onOrganizeTokensClick = null) + ) : ContentState(items = items, organizeTokensButton = OrganizeTokensButtonState.Hidden) /** * Content state * * @property items content items - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + * @property organizeTokensButton represents the state of the 'Organize Tokens' button */ data class Content( override val items: ImmutableList, - override val onOrganizeTokensClick: (() -> Unit)?, - ) : ContentState(items, onOrganizeTokensClick) + override val organizeTokensButton: OrganizeTokensButtonState, + ) : ContentState(items, organizeTokensButton) /** Locked content state */ object Locked : ContentState( items = persistentListOf( - TokensListItemState.NetworkGroupTitle(value = TextReference.Res(id = R.string.main_tokens)), + TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), TokensListItemState.Token(state = TokenItemState.Locked(id = LOCKED_TOKEN_ID)), ), - onOrganizeTokensClick = null, + organizeTokensButton = OrganizeTokensButtonState.Hidden, ) + /** + * Represents the state of the 'Organize Tokens' button. + */ + @Immutable + sealed class OrganizeTokensButtonState { + + /** Represents the state where the 'Organize Tokens' button is hidden. */ + object Hidden : OrganizeTokensButtonState() + + /** + * Represents the state where the 'Organize Tokens' button is visible. + * + * @property isEnabled Indicates if the button is enabled or not. + * @property onClick Callback to be executed when the button is clicked. + */ + data class Visible( + val isEnabled: Boolean, + val onClick: () -> Unit, + ) : OrganizeTokensButtonState() + } + /** Tokens list item state */ + @Immutable sealed interface TokensListItemState { + val id: Any + /** * Network group title item * - * @property value network name + * @property name network name */ - data class NetworkGroupTitle(val value: TextReference) : TokensListItemState + data class NetworkGroupTitle( + override val id: Int, + val name: TextReference, + ) : TokensListItemState /** * Token item * * @property state token item state */ - data class Token(val state: TokenItemState) : TokensListItemState + data class Token(val state: TokenItemState) : TokensListItemState { + override val id: String = state.id + } } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTopBarConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTopBarConfig.kt index 52e984f940..9b3cf42edf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTopBarConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletTopBarConfig.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.components /** * Wallet screen top bar config * - * @property onScanCardClick lambda be invoked when scan card button is clicked - * @property onMoreClick lambda be invoked when more button is clicked + * @property onDetailsClick lambda be invoked when details button is clicked */ -data class WalletTopBarConfig(val onScanCardClick: () -> Unit, val onMoreClick: () -> Unit) \ No newline at end of file +internal data class WalletTopBarConfig(val onDetailsClick: () -> Unit) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt index f9a7dbfc4e..d3e51d1d76 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -1,52 +1,70 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory -import com.tangem.common.Provider +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.TokenActionButtonConfig -import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList /** * Converter from loaded [TokenItemState.Content] to ImmutableList<[TokenActionButtonConfig]> * - * @property currentStateProvider current ui state provider + * @property clickIntents screen click intents * */ -@Suppress("UnusedPrivateMember") -internal class TokenActionsProvider( - private val currentStateProvider: Provider, -) { +@Suppress("UnusedPrivateMember") // will be used in next PRs +internal class TokenActionsProvider(private val clickIntents: WalletClickIntents) { - @Suppress("UnusedPrivateMember") - fun provideActions(tokenId: String): ImmutableList { - // TODO: [REDACTED_JIRA] - return mockTokenActionButtonConfig().toImmutableList() + fun provideActions(tokenActions: TokenActionsState): ImmutableList { + return tokenActions.states + .map { mapTokenActionState(it, tokenActions.cryptoCurrencyStatus) } + .toImmutableList() } - private fun mockTokenActionButtonConfig(): List { - return listOf( - TokenActionButtonConfig( - text = "Send", - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), - TokenActionButtonConfig( - text = "Buy", - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), - TokenActionButtonConfig( - text = "Sell", - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), - TokenActionButtonConfig( - text = "Swap", - iconResId = R.drawable.ic_plus_24, - onClick = {}, - ), + private fun mapTokenActionState( + actionsState: TokenActionsState.ActionState, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): TokenActionButtonConfig { + val title: TextReference + val icon: Int + val action: () -> Unit + when (actionsState) { + is TokenActionsState.ActionState.Buy -> { + title = resourceReference(R.string.common_buy) + icon = R.drawable.ic_plus_24 + action = { clickIntents.onBuyClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Receive -> { + title = resourceReference(R.string.common_receive) + icon = R.drawable.ic_arrow_down_24 + action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Sell -> { + title = resourceReference(R.string.common_sell) + icon = R.drawable.ic_currency_24 + action = { clickIntents.onSellClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Send -> { + title = resourceReference(R.string.common_send) + icon = R.drawable.ic_arrow_up_24 + action = { clickIntents.onMultiCurrencySendClick(cryptoCurrencyStatus) } + } + is TokenActionsState.ActionState.Swap -> { + title = resourceReference(R.string.common_swap) + icon = R.drawable.ic_exchange_horizontal_24 + action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } + } + } + return TokenActionButtonConfig( + text = title, + iconResId = icon, + onClick = action, + enabled = actionsState.enabled, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt index 597680d5eb..e347773cc5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletCryptoCurrencyActionsConverter.kt @@ -8,15 +8,15 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList internal class WalletCryptoCurrencyActionsConverter( private val currentStateProvider: Provider, private val clickIntents: WalletClickIntents, -) : Converter, WalletState> { +) : Converter { - override fun convert(value: List): WalletState { + override fun convert(value: TokenActionsState): WalletState { return when (val state = currentStateProvider()) { is WalletSingleCurrencyState.Content -> state.copy(buttons = value.mapToManageButtons()) is WalletSingleCurrencyState.Locked, @@ -26,25 +26,36 @@ internal class WalletCryptoCurrencyActionsConverter( } } - private fun List.mapToManageButtons(): ImmutableList { - return this + private fun TokenActionsState.mapToManageButtons(): PersistentList { + return this.states .mapNotNull { action -> when (action) { is TokenActionsState.ActionState.Buy -> { - WalletManageButton.Buy(enabled = action.enabled, onClick = clickIntents::onBuyClick) + WalletManageButton.Buy( + enabled = action.enabled, + onClick = { clickIntents.onBuyClick(cryptoCurrencyStatus) }, + ) } is TokenActionsState.ActionState.Receive -> { - WalletManageButton.Receive(onClick = clickIntents::onReceiveClick) + WalletManageButton.Receive( + onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus) }, + ) } is TokenActionsState.ActionState.Sell -> { - WalletManageButton.Sell(enabled = action.enabled, onClick = clickIntents::onSellClick) + WalletManageButton.Sell( + enabled = action.enabled, + onClick = { clickIntents.onSellClick(cryptoCurrencyStatus) }, + ) } is TokenActionsState.ActionState.Send -> { - WalletManageButton.Send(enabled = action.enabled, onClick = clickIntents::onSendClick) + WalletManageButton.Send( + enabled = action.enabled, + onClick = { clickIntents.onSingleCurrencySendClick(cryptoCurrencyStatus) }, + ) } is TokenActionsState.ActionState.Swap -> null } } - .toImmutableList() + .toPersistentList() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt new file mode 100644 index 0000000000..c6ccfcd05d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletDeleteStateConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletDeleteStateConverter.DeleteWalletModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +/** + * Converter that responds on wallet deleting action. Returns [WalletState] without deleted wallet. + * + * @property currentStateProvider current state provider + */ +internal class WalletDeleteStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: DeleteWalletModel): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + value.cacheState.copySealed( + walletsListConfig = state.walletsListConfig.copy( + selectedWalletIndex = value.action.selectedWalletIndex, + wallets = state.walletsListConfig.wallets.deleteWallet(id = value.action.deletedWalletId), + ), + pullToRefreshConfig = value.cacheState.pullToRefreshConfig.copy(isRefreshing = false), + ) + } + is WalletState.Initial -> state + } + } + + private fun List.deleteWallet(id: UserWalletId): ImmutableList { + return this + .mapIndexedNotNull { index, currentWallet -> + if (currentWallet.id == id) return@mapIndexedNotNull null + getOrNull(index) ?: return@mapIndexedNotNull null + } + .toImmutableList() + } + + data class DeleteWalletModel( + val cacheState: WalletState.ContentState, + val action: WalletsUpdateActionResolver.Action.DeleteWallet, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt index a98b75e4f8..1f6d2ceff0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -3,13 +3,11 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import arrow.core.Either import com.tangem.common.Provider import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents @@ -18,50 +16,36 @@ import com.tangem.utils.converter.Converter /** * Converter from loaded [TokenListError] or [TokenList] to [WalletMultiCurrencyState] * - * @property currentStateProvider current ui state provider - * @param cardTypeResolverProvider card type resolver - * @param currentWalletProvider current wallet provider - * @param clickIntents screen click intents + * @property currentStateProvider current ui state provider + * @property tokenListErrorConverter converter of tokens list + * @param appCurrencyProvider app currency provider + * @param currentWalletProvider current wallet provider + * @param clickIntents screen click intents * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class WalletLoadedTokensListConverter( private val currentStateProvider: Provider, + private val tokenListErrorConverter: TokenListErrorConverter, appCurrencyProvider: Provider, - cardTypeResolverProvider: Provider, currentWalletProvider: Provider, + isBalanceHiddenProvider: Provider, clickIntents: WalletClickIntents, -) : Converter { +) : Converter, WalletState> { private val tokenListStateConverter = TokenListToWalletStateConverter( currentStateProvider = currentStateProvider, - cardTypeResolverProvider = cardTypeResolverProvider, currentWalletProvider = currentWalletProvider, appCurrencyProvider = appCurrencyProvider, - isWalletContentHidden = false, // TODO: [REDACTED_JIRA] + isBalanceHiddenProvider = isBalanceHiddenProvider, clickIntents = clickIntents, ) - private val tokenListErrorStateConverter = TokenListErrorConverter( - currentStateProvider = currentStateProvider, - ) - - override fun convert(value: LoadedTokensListModel): WalletState { - return value.tokenListEither.fold( - ifLeft = tokenListErrorStateConverter::convert, - ifRight = { - tokenListStateConverter.convert( - value = TokenListToWalletStateConverter.TokensListModel( - tokenList = it, - isRefreshing = value.isRefreshing, - ), - ) - }, + override fun convert(value: Either): WalletState { + return value.fold( + ifLeft = tokenListErrorConverter::convert, + ifRight = tokenListStateConverter::convert, ) } - - data class LoadedTokensListModel( - val tokenListEither: Either, - val isRefreshing: Boolean, - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt index 5ec6613bb8..51cb49325d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLockedConverter.kt @@ -1,110 +1,78 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import com.tangem.common.Provider -import com.tangem.domain.common.CardTypesResolver -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopBarConfig -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList internal class WalletLockedConverter( private val currentStateProvider: Provider, - private val currentCardTypeResolverProvider: Provider, - private val currentWalletProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { override fun convert(value: Unit): WalletState { return when (val state = currentStateProvider()) { - is WalletState.ContentState -> { - val cardTypeResolver = currentCardTypeResolverProvider() - - if (cardTypeResolver.isMultiwalletAllowed()) { - state.toMultiCurrencyLockedState(cardTypeResolver) - } else { - state.toSingleCurrencyLockedState(cardTypeResolver) - } - } - is WalletState.Initial -> state + is WalletMultiCurrencyState.Content -> state.toMultiCurrencyLockedState() + is WalletSingleCurrencyState.Content -> state.toSingleCurrencyLockedState() + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state } } - private fun WalletState.ContentState.toMultiCurrencyLockedState( - cardTypeResolver: CardTypesResolver, - ): WalletMultiCurrencyState.Locked { + private fun WalletMultiCurrencyState.Content.toMultiCurrencyLockedState(): WalletState { return WalletMultiCurrencyState.Locked( onBackClick = onBackClick, - topBarConfig = createTopBarConfig(), - walletsListConfig = createWalletsListConfig(cardTypeResolver), - pullToRefreshConfig = pullToRefreshConfig, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, ) } - private fun WalletState.ContentState.toSingleCurrencyLockedState( - cardTypeResolver: CardTypesResolver, - ): WalletSingleCurrencyState.Locked { + private fun WalletSingleCurrencyState.Content.toSingleCurrencyLockedState(): WalletState { return WalletSingleCurrencyState.Locked( onBackClick = onBackClick, - topBarConfig = createTopBarConfig(), - walletsListConfig = createWalletsListConfig(cardTypeResolver), - pullToRefreshConfig = pullToRefreshConfig, - buttons = createButtons(), + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + buttons = buttons.disableButtons(), onUnlockWalletsNotificationClick = clickIntents::onUnlockWalletNotificationClick, onUnlockClick = clickIntents::onUnlockWalletClick, - onScanClick = clickIntents::onScanCardClick, + onScanClick = clickIntents::onScanToUnlockWalletClick, onExploreClick = clickIntents::onExploreClick, ) } - private fun WalletState.ContentState.createTopBarConfig(): WalletTopBarConfig { - return topBarConfig.copy(onMoreClick = clickIntents::onUnlockWalletNotificationClick) + private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig { + return copy(onDetailsClick = clickIntents::onUnlockWalletNotificationClick) } - private fun WalletState.ContentState.createWalletsListConfig( - cardTypeResolver: CardTypesResolver, - ): WalletsListConfig { - return walletsListConfig.copy( - wallets = walletsListConfig.wallets - .map { walletCardState -> - WalletCardState.LockedContent( - id = walletCardState.id, - title = walletCardState.title, - additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { - WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolver, - wallet = currentWalletProvider(), - ) - } else { - null - }, - imageResId = walletCardState.imageResId, - onRenameClick = walletCardState.onRenameClick, - onDeleteClick = walletCardState.onDeleteClick, - ) + private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig { + return copy(isRefreshing = false) + } + + private fun PersistentList.disableButtons(): PersistentList { + return this + .map { button -> + when (button) { + is WalletManageButton.Buy -> button.copy(enabled = false) + is WalletManageButton.Sell -> button.copy(enabled = false) + is WalletManageButton.Send -> button.copy(enabled = false) + is WalletManageButton.Swap -> button.copy(enabled = false) + is WalletManageButton.Receive -> button } - .toImmutableList(), - ) - } - - private fun createButtons(): ImmutableList { - return persistentListOf( - WalletManageButton.Buy(enabled = false, onClick = {}), - WalletManageButton.Send(enabled = false, onClick = {}), - WalletManageButton.Receive(onClick = {}), - WalletManageButton.Sell(enabled = false, onClick = {}), - ) + } + .toPersistentList() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt index f820ff8444..2344194b89 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRefreshStateConverter.kt @@ -1,124 +1,115 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import com.tangem.common.Provider -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.domain.common.CardTypesResolver -import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.* -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState -import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.update +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate internal class WalletRefreshStateConverter( private val currentStateProvider: Provider, - private val currentCardTypeResolverProvider: Provider, - private val clickIntents: WalletClickIntents, -) : Converter { +) : Converter { - override fun convert(value: Unit): WalletState { - return when (val state = currentStateProvider()) { - is WalletMultiCurrencyState.Content -> state.getRefreshState() - is WalletSingleCurrencyState.Content -> state.getRefreshState() - else -> state - } - } + override fun convert(value: Boolean): WalletState { + val state = currentStateProvider() + val contentState = state as? WalletState.ContentState ?: return state - private fun WalletMultiCurrencyState.Content.getRefreshState(): WalletMultiCurrencyState.Content { - return copy( - walletsListConfig = createWalletsListConfig(), - pullToRefreshConfig = createPullToRefreshConfig(), - tokensListState = createTokenListState(), - ) - } - - private fun WalletSingleCurrencyState.Content.getRefreshState(): WalletSingleCurrencyState.Content { - return copy( - walletsListConfig = createWalletsListConfig(), - pullToRefreshConfig = createPullToRefreshConfig(), - buttons = buttons.mapToDisabledButton(), - txHistoryState = createTxHistoryState(), - marketPriceBlockState = MarketPriceBlockState.Loading(currencyName = marketPriceBlockState.currencyName), - ) - } - - private fun WalletState.ContentState.createWalletsListConfig(): WalletsListConfig { - val selectedWallet = walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] - val additionalInfo = if (currentCardTypeResolverProvider().isMultiwalletAllowed()) { - selectedWallet.additionalInfo + return if (value) { + contentState.getRefreshingState() } else { - null + contentState.getRefreshedState() } + } - return walletsListConfig.copy( - wallets = walletsListConfig.wallets.toPersistentList().set( - index = walletsListConfig.selectedWalletIndex, - element = WalletCardState.Loading( - id = selectedWallet.id, - title = selectedWallet.title, - additionalInfo = additionalInfo, - imageResId = selectedWallet.imageResId, - onRenameClick = selectedWallet.onRenameClick, - onDeleteClick = selectedWallet.onDeleteClick, - ), - ), + private fun WalletState.ContentState.getRefreshingState(): WalletState { + return when (this) { + is WalletMultiCurrencyState.Content -> getRefreshingState() + is WalletSingleCurrencyState.Content -> getRefreshingState() + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + -> this + } + } + + private fun WalletState.ContentState.getRefreshedState(): WalletState { + return when (this) { + is WalletMultiCurrencyState.Content -> getRefreshedState() + is WalletSingleCurrencyState.Content -> getRefreshedState() + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + -> this + } + } + + private fun WalletMultiCurrencyState.Content.getRefreshingState(): WalletMultiCurrencyState { + return copy( + pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = true), + tokensListState = updateTokenListState(isRefreshing = true), ) } - private fun WalletState.ContentState.createPullToRefreshConfig(): WalletPullToRefreshConfig { - return pullToRefreshConfig.copy(isRefreshing = true) + private fun WalletSingleCurrencyState.Content.getRefreshingState(): WalletSingleCurrencyState { + return copy( + pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = true), + buttons = updateButtons(isRefreshing = true), + ) } - private fun WalletMultiCurrencyState.Content.createTokenListState(): WalletTokensListState { - return when (tokensListState) { + private fun WalletMultiCurrencyState.Content.getRefreshedState(): WalletMultiCurrencyState { + return copy( + pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = false), + tokensListState = updateTokenListState(isRefreshing = false), + ) + } + + private fun WalletSingleCurrencyState.Content.getRefreshedState(): WalletSingleCurrencyState { + return copy( + pullToRefreshConfig = updatePullToRefreshConfig(isRefreshing = false), + buttons = updateButtons(isRefreshing = false), + ) + } + + private fun WalletMultiCurrencyState.updateTokenListState(isRefreshing: Boolean): WalletTokensListState { + return when (val listState = tokensListState) { is WalletTokensListState.Content -> { - WalletTokensListState.Loading( - items = tokensListState.items - .filterIsInstance() - .mapToLoadingTokenState(), - ) + when (listState.organizeTokensButton) { + is WalletTokensListState.OrganizeTokensButtonState.Hidden -> listState + is WalletTokensListState.OrganizeTokensButtonState.Visible -> listState.copy( + organizeTokensButton = listState.organizeTokensButton.copy( + isEnabled = !isRefreshing, + ), + ) + } } - is WalletTokensListState.Empty -> WalletTokensListState.Loading() - is WalletTokensListState.Loading, is WalletTokensListState.Locked, - -> tokensListState + is WalletTokensListState.Loading, + is WalletTokensListState.Empty, + -> listState } } - private fun List.mapToLoadingTokenState(): ImmutableList { - return this - .map { TokensListItemState.Token(state = TokenItemState.Loading(id = it.state.id)) } - .toImmutableList() - } + private fun WalletSingleCurrencyState.updateButtons(isRefreshing: Boolean): PersistentList { + val isButtonsEnabled = !isRefreshing - private fun ImmutableList.mapToDisabledButton(): ImmutableList { - return this - .mapNotNull { button -> + return buttons.mutate { + it.mapNotNull { button -> when (button) { - is WalletManageButton.Buy -> button.copy(enabled = false) - is WalletManageButton.Send -> button.copy(enabled = false) + is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button - is WalletManageButton.Sell -> button.copy(enabled = false) is WalletManageButton.Swap -> null } } - .toImmutableList() + } } - private fun WalletSingleCurrencyState.Content.createTxHistoryState(): TxHistoryState { - if (txHistoryState is TxHistoryState.Content) { - txHistoryState.contentItems.update { - TxHistoryState.getDefaultLoadingTransactions(onExploreClick = clickIntents::onExploreClick) - } - } - - return txHistoryState + private fun WalletState.ContentState.updatePullToRefreshConfig(isRefreshing: Boolean): WalletPullToRefreshConfig { + return pullToRefreshConfig.copy(isRefreshing = isRefreshing) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt new file mode 100644 index 0000000000..946afa55df --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletRenameStateConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList + +internal class WalletRenameStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: String): WalletState { + return when (val state = currentStateProvider()) { + is WalletState.ContentState -> { + state.copySealed( + walletsListConfig = state.walletsListConfig.renameSelectedWallet(name = value), + ) + } + is WalletState.Initial -> state + } + } + + private fun WalletsListConfig.renameSelectedWallet(name: String): WalletsListConfig { + return copy( + wallets = wallets + .mapIndexed { index, walletCard -> + if (index == selectedWalletIndex) walletCard.copySealed(title = name) else walletCard + } + .toImmutableList(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt index 6acdd08c82..d0d570cacb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSingleCurrencyLoadedBalanceConverter.kt @@ -6,53 +6,50 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig -import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel +import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal internal class WalletSingleCurrencyLoadedBalanceConverter( private val currentStateProvider: Provider, - private val cardTypeResolverProvider: Provider, private val appCurrencyProvider: Provider, private val currentWalletProvider: Provider, -) : Converter { + private val currencyStatusErrorConverter: CurrencyStatusErrorConverter, +) : Converter, WalletState> { - override fun convert(value: SingleCurrencyLoadedBalanceModel): WalletSingleCurrencyState.Content { - return value.cryptoCurrencyEither.fold( - ifLeft = { convertError() }, - ifRight = { convertContent(it, value.isRefreshing) }, + override fun convert(value: Either): WalletState { + return value.fold( + ifLeft = currencyStatusErrorConverter::convert, + ifRight = ::convertContent, ) } - private fun convertError(): WalletSingleCurrencyState.Content { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) - } + private fun convertContent(status: CryptoCurrencyStatus): WalletState { + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + val currencyName = state.marketPriceBlockState.currencyName - private fun convertContent( - status: CryptoCurrencyStatus, - isRefreshing: Boolean, - ): WalletSingleCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) - val currencyName = state.marketPriceBlockState.currencyName - return state.copy( - walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), - pullToRefreshConfig = if (isRefreshing) { - state.pullToRefreshConfig.copy(isRefreshing = status.value is CryptoCurrencyStatus.Loading) - } else { - state.pullToRefreshConfig - }, - marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), - ) + state.copy( + walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state), + marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName), + ) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState { @@ -72,6 +69,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, -> MarketPriceBlockState.Error(currencyName) } } @@ -90,7 +88,6 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( id = selectedWallet.id, title = selectedWallet.title, additionalInfo = WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolverProvider(), wallet = currentWalletProvider(), currencyAmount = status.amount, ), @@ -113,6 +110,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, -> { WalletCardState.Error( id = selectedWallet.id, @@ -168,9 +166,4 @@ internal class WalletSingleCurrencyLoadedBalanceConverter( fiatCurrencySymbol = appCurrency.symbol, ) } - - data class SingleCurrencyLoadedBalanceModel( - val cryptoCurrencyEither: Either, - val isRefreshing: Boolean, - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt index 15e42f3283..28b5a530bb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory +import androidx.annotation.DrawableRes import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -14,7 +15,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.* import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletSkeletonStateConverter.SkeletonModel import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -33,12 +34,12 @@ internal class WalletSkeletonStateConverter( ) : Converter { override fun convert(value: SkeletonModel): WalletState.ContentState { - val cardTypeResolver = value.wallets[value.selectedWalletIndex].scanResponse.cardTypesResolver + val selectedWallet = value.wallets[value.selectedWalletIndex] - return if (cardTypeResolver.isMultiwalletAllowed()) { + return if (selectedWallet.isMultiCurrency) { createMultiCurrencyState(value = value) } else { - createSingleCurrencyState(value = value, currencyName = cardTypeResolver.getBlockchain().currency) + createSingleCurrencyState(value = value, currencyName = selectedWallet.getPrimaryCurrencyName()) } } @@ -74,61 +75,70 @@ internal class WalletSkeletonStateConverter( ) } + private fun UserWallet.getPrimaryCurrencyName(): String { + return scanResponse.cardTypesResolver.getBlockchain().currency + } + private fun createTopBarConfig(): WalletTopBarConfig { - return WalletTopBarConfig( - onScanCardClick = clickIntents::onScanCardClick, - onMoreClick = clickIntents::onDetailsClick, - ) + return WalletTopBarConfig(onDetailsClick = clickIntents::onDetailsClick) } private fun createWalletsListConfig(value: SkeletonModel): WalletsListConfig { return WalletsListConfig( selectedWalletIndex = value.selectedWalletIndex, - wallets = value.wallets.map(::createWalletState).toImmutableList(), + wallets = value.wallets.mapIndexed(::createWalletCardState).toImmutableList(), onWalletChange = clickIntents::onWalletChange, ) } - private fun createWalletState(wallet: UserWallet): WalletCardState { - val state = currentStateProvider() - - // If it isn't first initialization (example, when user unlocks wallet) - return if (state is WalletState.ContentState) { - val initializedWallet = state.walletsListConfig.wallets.first { it.id == wallet.walletId } - - // If wallet is initialized, return it, otherwise return loading state - if (initializedWallet !is WalletCardState.Loading) { - initializedWallet.copySealed(title = wallet.name) - } else { - createWalletLoadingState(wallet) - } - } else { - createWalletLoadingState(wallet) - } + /** + * Create wallet card state by [index] and [wallet]. + * If current wallet card state is initialized, then method returns it. + * Otherwise, returns loading wallet card state. + */ + private fun createWalletCardState(index: Int, wallet: UserWallet): WalletCardState { + return currentStateProvider().getInitializedWalletCardState(index) ?: wallet.mapToWalletCardState() } - private fun createWalletLoadingState(wallet: UserWallet): WalletCardState { - val cardTypeResolver = wallet.scanResponse.cardTypesResolver + private fun WalletState.getInitializedWalletCardState(index: Int): WalletCardState? { + return (this as? WalletState.ContentState)?.walletsListConfig?.wallets?.getOrNull(index) + } - return WalletCardState.Loading( - id = wallet.walletId, - title = wallet.name, - additionalInfo = if (cardTypeResolver.isMultiwalletAllowed()) { - WalletAdditionalInfoFactory.resolve(cardTypesResolver = cardTypeResolver, wallet = wallet) - } else { - null - }, - imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), + private fun UserWallet.mapToWalletCardState(): WalletCardState { + return if (isLocked) mapToLockedWalletCardState() else mapToLoadingWalletCardState() + } + + private fun UserWallet.mapToLockedWalletCardState(): WalletCardState { + return WalletCardState.LockedContent( + id = walletId, + title = name, + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = this), + imageResId = createImageResId(), onRenameClick = clickIntents::onRenameClick, onDeleteClick = clickIntents::onDeleteClick, ) } + private fun UserWallet.mapToLoadingWalletCardState(): WalletCardState { + return WalletCardState.Loading( + id = walletId, + title = name, + imageResId = createImageResId(), + onRenameClick = clickIntents::onRenameClick, + onDeleteClick = clickIntents::onDeleteClick, + ) + } + + @DrawableRes + private fun UserWallet.createImageResId(): Int? { + return WalletImageResolver.resolve(userWallet = this) + } + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) } - private fun createButtons(): ImmutableList { + private fun createButtons(): PersistentList { return persistentListOf( WalletManageButton.Buy(enabled = false, onClick = {}), WalletManageButton.Send(enabled = false, onClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt index 512d0d0cc0..7d909ffbde 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -3,6 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory import androidx.paging.PagingData import arrow.core.Either import com.tangem.common.Provider +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.error.CurrencyStatusError @@ -18,11 +22,15 @@ import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetCon import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter +import com.tangem.feature.wallet.presentation.wallet.utils.CurrencyStatusErrorConverter +import com.tangem.feature.wallet.presentation.wallet.utils.HiddenStateConverter +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.Flow @@ -32,25 +40,44 @@ import kotlinx.coroutines.flow.Flow * @property currentStateProvider current ui state provider * @property currentCardTypeResolverProvider current card type resolver * @property currentWalletProvider current wallet + * @property appCurrencyProvider app currency provider * @property clickIntents screen click intents */ +@Suppress("TooManyFunctions") internal class WalletStateFactory( private val currentStateProvider: Provider, private val currentCardTypeResolverProvider: Provider, private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, + private val isBalanceHiddenProvider: Provider, private val clickIntents: WalletClickIntents, ) { - private val tokenActionsProvider by lazy { TokenActionsProvider(currentStateProvider = currentStateProvider) } + private val tokenActionsProvider by lazy { TokenActionsProvider(clickIntents) } + private val skeletonConverter by lazy { WalletSkeletonStateConverter(currentStateProvider, clickIntents) } + private val walletsUnlockStateConverter by lazy { WalletsUnlockStateConverter(currentStateProvider, clickIntents) } + + private val walletRenameStateConverter by lazy { WalletRenameStateConverter(currentStateProvider) } + + private val walletDeleteStateConverter by lazy { WalletDeleteStateConverter(currentStateProvider) } + + private val hiddenStateConverter by lazy { HiddenStateConverter(currentStateProvider) } + + private val tokenListErrorConverter by lazy { + TokenListErrorConverter(currentStateProvider) + } + private val currencyStatusErrorConverter by lazy { + CurrencyStatusErrorConverter(currentStateProvider) + } private val loadedTokensListConverter by lazy { WalletLoadedTokensListConverter( currentStateProvider = currentStateProvider, - cardTypeResolverProvider = currentCardTypeResolverProvider, - currentWalletProvider = currentWalletProvider, + tokenListErrorConverter = tokenListErrorConverter, appCurrencyProvider = appCurrencyProvider, + currentWalletProvider = currentWalletProvider, + isBalanceHiddenProvider = isBalanceHiddenProvider, clickIntents = clickIntents, ) } @@ -73,27 +100,21 @@ internal class WalletStateFactory( private val singleCurrencyLoadedBalanceConverter by lazy { WalletSingleCurrencyLoadedBalanceConverter( currentStateProvider = currentStateProvider, - cardTypeResolverProvider = currentCardTypeResolverProvider, appCurrencyProvider = appCurrencyProvider, currentWalletProvider = currentWalletProvider, + currencyStatusErrorConverter = currencyStatusErrorConverter, ) } private val lockedConverter by lazy { WalletLockedConverter( currentStateProvider = currentStateProvider, - currentCardTypeResolverProvider = currentCardTypeResolverProvider, - currentWalletProvider = currentWalletProvider, clickIntents = clickIntents, ) } private val refreshStateConverter by lazy { - WalletRefreshStateConverter( - currentStateProvider = currentStateProvider, - currentCardTypeResolverProvider = currentCardTypeResolverProvider, - clickIntents = clickIntents, - ) + WalletRefreshStateConverter(currentStateProvider) } private val cryptoCurrencyActionsConverter by lazy { @@ -114,15 +135,29 @@ internal class WalletStateFactory( ) } - fun getStateByTokensList(tokenListEither: Either, isRefreshing: Boolean): WalletState { - return loadedTokensListConverter.convert( - value = WalletLoadedTokensListConverter.LoadedTokensListModel( - tokenListEither = tokenListEither, - isRefreshing = isRefreshing, - ), + fun getStateWithUpdatedWalletName(name: String): WalletState = walletRenameStateConverter.convert(value = name) + + fun getUnlockedState(action: WalletsUpdateActionResolver.Action.UnlockWallet): WalletState { + return walletsUnlockStateConverter.convert(value = action) + } + + fun getStateWithoutDeletedWallet( + cacheState: WalletState.ContentState, + action: WalletsUpdateActionResolver.Action.DeleteWallet, + ): WalletState { + return walletDeleteStateConverter.convert( + value = WalletDeleteStateConverter.DeleteWalletModel(cacheState = cacheState, action = action), ) } + fun getStateByTokensList(maybeTokenList: Either): WalletState { + return loadedTokensListConverter.convert(maybeTokenList) + } + + fun getStateByTokenListError(error: TokenListError): WalletState { + return tokenListErrorConverter.convert(error) + } + fun getStateByNotifications(notifications: ImmutableList): WalletState { return when (val state = currentStateProvider()) { is WalletMultiCurrencyState.Content -> state.copy(notifications = notifications) @@ -131,12 +166,14 @@ internal class WalletStateFactory( } } - fun getStateAfterContentRefreshing(): WalletState = refreshStateConverter.convert(Unit) + fun getRefreshingState(): WalletState = refreshStateConverter.convert(value = true) - fun getStateWithOpenWalletBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletState { + fun getRefreshedState(): WalletState = refreshStateConverter.convert(value = false) + + fun getStateWithOpenWalletBottomSheet(content: TangemBottomSheetConfigContent): WalletState { return when (val state = currentStateProvider() as WalletState.ContentState) { is WalletMultiCurrencyState.Content -> state.copy( - bottomSheetConfig = WalletBottomSheetConfig( + bottomSheetConfig = TangemBottomSheetConfig( isShow = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = content, @@ -147,7 +184,7 @@ internal class WalletStateFactory( onBottomSheetDismiss = clickIntents::onDismissBottomSheet, ) is WalletSingleCurrencyState.Content -> state.copy( - bottomSheetConfig = WalletBottomSheetConfig( + bottomSheetConfig = TangemBottomSheetConfig( isShow = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = content, @@ -173,12 +210,12 @@ internal class WalletStateFactory( } } - fun getStateWithTokenActionBottomSheet(tokenId: String): WalletState { + fun getStateWithTokenActionBottomSheet(tokenActions: TokenActionsState): WalletState { return when (val state = currentStateProvider() as WalletState.ContentState) { is WalletMultiCurrencyState.Content -> state.copy( tokenActionsBottomSheet = ActionsBottomSheetConfig( isShow = true, - actions = tokenActionsProvider.provideActions(tokenId = tokenId), + actions = tokenActionsProvider.provideActions(tokenActions), onDismissRequest = clickIntents::onDismissActionsBottomSheet, ), ) @@ -187,7 +224,9 @@ internal class WalletStateFactory( } fun getLoadingTxHistoryState(itemsCountEither: Either): WalletState { - return loadingTransactionsStateConverter.convert(value = itemsCountEither) + return loadingTransactionsStateConverter.convert( + WalletLoadingTxHistoryConverter.WalletLoadingTxHistoryModel(historyLoadingState = itemsCountEither), + ) } fun getLoadedTxHistoryState( @@ -199,18 +238,41 @@ internal class WalletStateFactory( fun getLockedState(): WalletState = lockedConverter.convert(Unit) fun getSingleCurrencyLoadedBalanceState( - cryptoCurrencyEither: Either, - isRefreshing: Boolean, + maybeCryptoCurrencyStatus: Either, ): WalletState { - return singleCurrencyLoadedBalanceConverter.convert( - value = WalletSingleCurrencyLoadedBalanceConverter.SingleCurrencyLoadedBalanceModel( - cryptoCurrencyEither = cryptoCurrencyEither, - isRefreshing = isRefreshing, - ), - ) + return singleCurrencyLoadedBalanceConverter.convert(maybeCryptoCurrencyStatus) } - fun getSingleCurrencyManageButtonsState(actions: List): WalletState { - return cryptoCurrencyActionsConverter.convert(value = actions) + fun getSingleCurrencyManageButtonsState(actionsState: TokenActionsState): WalletState { + return cryptoCurrencyActionsConverter.convert(value = actionsState) + } + + fun getStateByCurrencyStatusError(error: CurrencyStatusError): WalletState { + return currencyStatusErrorConverter.convert(error) + } + + fun getHiddenBalanceState(isBalanceHidden: Boolean): WalletState { + return hiddenStateConverter.convert(isBalanceHidden) + } + + fun getStateAndTriggerEvent( + state: WalletState, + event: WalletEvent, + setUiState: (WalletState) -> Unit, + ): WalletState { + return when (state) { + is WalletState.ContentState -> state.copySealed( + event = triggeredEvent( + data = event, + onConsume = { + val currentState = currentStateProvider() + if (currentState is WalletState.ContentState) { + setUiState(currentState.copySealed(event = consumedEvent())) + } + }, + ), + ) + is WalletState.Initial -> state + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt new file mode 100644 index 0000000000..39f3eb7cc9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletsUnlockStateConverter.kt @@ -0,0 +1,131 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import com.tangem.common.Provider +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.* +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletsUpdateActionResolver.Action.UnlockWallet as UnlockWalletAction + +/** + * Converter that responds on wallets unlocking action. Returns [WalletState] with unlocked wallets. + * + * @property currentStateProvider current ui state provider + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletsUnlockStateConverter( + private val currentStateProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter { + + override fun convert(value: UnlockWalletAction): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Locked -> state.toMultiCurrencyContentState(value) + is WalletSingleCurrencyState.Locked -> state.toSingleCurrencyContentState(value) + is WalletState.Initial, + is WalletMultiCurrencyState.Content, + is WalletSingleCurrencyState.Content, + -> state + } + } + + private fun WalletMultiCurrencyState.Locked.toMultiCurrencyContentState(action: UnlockWalletAction): WalletState { + return WalletMultiCurrencyState.Content( + onBackClick = onBackClick, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig.unlockWallets(action), + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + tokensListState = WalletTokensListState.Loading(), + notifications = persistentListOf(), + bottomSheetConfig = null, + tokenActionsBottomSheet = null, + onManageTokensClick = clickIntents::onManageTokensClick, + ) + } + + private fun WalletSingleCurrencyState.Locked.toSingleCurrencyContentState(action: UnlockWalletAction): WalletState { + return WalletSingleCurrencyState.Content( + onBackClick = onBackClick, + topBarConfig = topBarConfig.updateCallback(), + walletsListConfig = walletsListConfig.unlockWallets(action), + pullToRefreshConfig = pullToRefreshConfig.stopRefreshing(), + notifications = persistentListOf(), + bottomSheetConfig = null, + buttons = buttons, + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = action.selectedWallet.getPrimaryCurrencyName(), + ), + txHistoryState = TxHistoryState.Content( + contentItems = MutableStateFlow( + value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), + ), + ), + ) + } + + private fun WalletTopBarConfig.updateCallback(): WalletTopBarConfig { + return copy(onDetailsClick = clickIntents::onDetailsClick) + } + + private fun WalletsListConfig.unlockWallets(action: UnlockWalletAction): WalletsListConfig { + return this.copy( + selectedWalletIndex = action.selectedWalletIndex, + wallets = wallets.unlockWallets(action), + ) + } + + private fun List.unlockWallets(action: UnlockWalletAction): ImmutableList { + return this + .map { prevWallet -> + if (prevWallet is WalletCardState.LockedContent && action.isUnlockedWallet(prevWallet.id)) { + prevWallet.mapToLoadingWalletCardState( + userWallet = action.getUnlockWallet(prevWallet.id), + ) + } else { + prevWallet + } + } + .toImmutableList() + } + + private fun UnlockWalletAction.isUnlockedWallet(walletId: UserWalletId): Boolean { + return unlockedWallets.any { it.walletId == walletId } + } + + private fun UnlockWalletAction.getUnlockWallet(walletId: UserWalletId): UserWallet { + return unlockedWallets.firstOrNull { it.walletId == walletId } + ?: error("Unlocked wallet with id $walletId not found") + } + + private fun WalletCardState.mapToLoadingWalletCardState(userWallet: UserWallet): WalletCardState { + return WalletCardState.Loading( + id = id, + title = title, + imageResId = WalletImageResolver.resolve(userWallet = userWallet), + onRenameClick = onRenameClick, + onDeleteClick = onDeleteClick, + ) + } + + private fun WalletPullToRefreshConfig.stopRefreshing(): WalletPullToRefreshConfig { + return copy(isRefreshing = false) + } + + private fun UserWallet.getPrimaryCurrencyName(): String { + return scanResponse.cardTypesResolver.getBlockchain().currency + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt index e585ac71f5..202fcc3ae5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents @@ -41,18 +42,36 @@ internal class WalletLoadedTxHistoryConverter( } private fun convertError(error: TxHistoryListError): WalletState { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = when (error) { - is TxHistoryListError.DataError -> { - TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) - } - }, - ) + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + state.copy( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + TxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + }, + ) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun convert(items: Flow>): WalletState { - return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content).copy( - txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items), - ) + return when (val state = currentStateProvider()) { + is WalletSingleCurrencyState.Content -> { + return state.copy( + txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items) ?: state.txHistoryState, + ) + } + is WalletMultiCurrencyState.Content, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt index b8b5ab1a7d..65e92212f3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory import androidx.paging.PagingData import arrow.core.Either +import com.tangem.common.Converter import com.tangem.common.Provider import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState.* @@ -9,24 +10,26 @@ import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents -import com.tangem.utils.converter.Converter import kotlinx.coroutines.flow.update /** * Converter from loading tx history state to [WalletSingleCurrencyState.Content] * - * @property currentStateProvider current state provider - * @property clickIntents screen click intents + * @property currentStateProvider current state provider + * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletLoadingTxHistoryConverter( private val currentStateProvider: Provider, private val clickIntents: WalletClickIntents, -) : Converter, WalletState> { +) : Converter { - override fun convert(value: Either): WalletState { - return value.fold(ifLeft = ::convertError, ifRight = ::convert) + override fun convert(value: WalletLoadingTxHistoryModel): WalletState { + return value.historyLoadingState.fold( + ifLeft = ::convertError, + ifRight = ::convertRight, + ) } private fun convertError(error: TxHistoryStateError): WalletState { @@ -35,12 +38,8 @@ internal class WalletLoadingTxHistoryConverter( return if (state is WalletSingleCurrencyState.Content) { state.copy( txHistoryState = when (error) { - is TxHistoryStateError.EmptyTxHistories -> { - Empty(onBuyClick = clickIntents::onBuyClick) - } - is TxHistoryStateError.DataError -> { - Error(onReloadClick = clickIntents::onReloadClick) - } + is TxHistoryStateError.EmptyTxHistories -> Empty + is TxHistoryStateError.DataError -> Error(onReloadClick = clickIntents::onReloadClick) is TxHistoryStateError.TxHistoryNotImplemented -> { NotSupported(onExploreClick = clickIntents::onExploreClick) } @@ -51,11 +50,11 @@ internal class WalletLoadingTxHistoryConverter( } } - private fun convert(value: Int): WalletSingleCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) - val txHistoryContent = requireNotNull(state.txHistoryState as? Content) + private fun convertRight(value: Int): WalletState { + val state = currentStateProvider() + val txHistoryContent = (state as? WalletSingleCurrencyState.Content)?.txHistoryState as? Content - txHistoryContent.contentItems.update { + txHistoryContent?.contentItems?.update { PagingData.from( data = listOf(TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick)) + MutableList( @@ -71,4 +70,6 @@ internal class WalletLoadingTxHistoryConverter( return state } + + data class WalletLoadingTxHistoryModel(val historyLoadingState: Either) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt index 5e4a05132f..d1d713271b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -43,7 +43,7 @@ internal class WalletTxHistoryItemFlowConverter( private val currentStateProvider: Provider, private val blockchain: Blockchain, private val clickIntents: WalletClickIntents, -) : Converter>, TxHistoryState> { +) : Converter>, TxHistoryState?> { /** Example, 2 Aug, 2023 */ private val dateFormatter by lazy { @@ -67,9 +67,9 @@ internal class WalletTxHistoryItemFlowConverter( .withLocale(Locale.getDefault()) } - override fun convert(value: Flow>): TxHistoryState { - val state = currentStateProvider() as WalletSingleCurrencyState - val txHistoryContent = state.txHistoryState as TxHistoryState.Content + override fun convert(value: Flow>): TxHistoryState? { + val state = currentStateProvider() as? WalletSingleCurrencyState ?: return null + val txHistoryContent = state.txHistoryState as? TxHistoryState.Content ?: return state.txHistoryState // FIXME: TxHistoryRepository should send loading transactions // [REDACTED_JIRA] diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt new file mode 100644 index 0000000000..7406cf8cab --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -0,0 +1,34 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent + +@Composable +internal fun WalletEventEffect( + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + event: StateEvent, + onAutoScrollSet: () -> Unit, +) { + val resources = LocalContext.current.resources + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is WalletEvent.ChangeWallet -> { + onAutoScrollSet() + walletsListState.animateScrollToItem(index = value.index) + } + is WalletEvent.ShowError -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + } + }, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index fca76604c4..3f8672679f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -1,15 +1,21 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.activity.compose.BackHandler -import androidx.compose.animation.* -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -18,6 +24,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.paging.compose.collectAsLazyPagingItems import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R @@ -25,11 +33,14 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* -import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeButton +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.controlButtons import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator @@ -46,27 +57,46 @@ internal fun WalletScreen(state: WalletState) { BackHandler(onBack = state.onBackClick) when (state) { - is WalletState.ContentState -> WalletContent(state = state) + is WalletState.ContentState -> { + val walletsListState = rememberLazyListState( + initialFirstVisibleItemIndex = state.walletsListConfig.selectedWalletIndex, + ) + val snackbarHostState = remember { SnackbarHostState() } + val isAutoScroll = remember { mutableStateOf(value = false) } + + WalletContent( + state = state, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + isAutoScroll = isAutoScroll, + onAutoScrollReset = { isAutoScroll.value = false }, + ) + + WalletEventEffect( + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + event = state.event, + onAutoScrollSet = { isAutoScroll.value = true }, + ) + } is WalletState.Initial -> Unit } } -@OptIn(ExperimentalMaterialApi::class) @Composable -private fun WalletContent(state: WalletState.ContentState) { - val walletsListState = rememberLazyListState() - - BaseScaffold(state = state) { scaffoldPaddings -> +private fun WalletContent( + state: WalletState.ContentState, + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, +) { + BaseScaffold(state = state, snackbarHostState) { scaffoldPaddings -> val movableItemModifier = Modifier.changeWalletAnimator(walletsListState) - val pullRefreshState = rememberPullRefreshState( - refreshing = state.pullToRefreshConfig.isRefreshing, - onRefresh = state.pullToRefreshConfig.onRefresh, - ) - Box( - modifier = Modifier - .padding(paddingValues = scaffoldPaddings) - .pullRefresh(pullRefreshState), + UpdatableContainer( + pullToRefreshConfig = state.pullToRefreshConfig, + modifier = Modifier.padding(paddingValues = scaffoldPaddings), ) { val txHistoryItems = if (state is WalletSingleCurrencyState && state.txHistoryState is TxHistoryState.Content @@ -110,30 +140,63 @@ private fun WalletContent(state: WalletState.ContentState) { contentItems(state = state, txHistoryItems = txHistoryItems, modifier = movableItemModifier) if (state is WalletMultiCurrencyState) { - val tokensListState = state.tokensListState - if (tokensListState is WalletTokensListState.ContentState) { - organizeButton(onClick = tokensListState.onOrganizeTokensClick, modifier = itemModifier) + val contentTokenListState = state.tokensListState as? WalletTokensListState.ContentState + val organizeTokensButton = contentTokenListState?.organizeTokensButton + + if (organizeTokensButton is OrganizeTokensButtonState.Visible) { + organizeTokensButton( + modifier = itemModifier, + isEnabled = organizeTokensButton.isEnabled, + onClick = organizeTokensButton.onClick, + ) } } } - - WalletPullToRefreshIndicator( - isRefreshing = state.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) } } WalletBottomSheets(state = state) - WalletSideEffects(lazyListState = walletsListState, walletsListConfig = state.walletsListConfig) + WalletsListEffects( + lazyListState = walletsListState, + walletsListConfig = state.walletsListConfig, + isAutoScroll = isAutoScroll, + onAutoScrollReset = onAutoScrollReset, + ) +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +private fun UpdatableContainer( + pullToRefreshConfig: WalletPullToRefreshConfig, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + val pullRefreshState = rememberPullRefreshState( + refreshing = pullToRefreshConfig.isRefreshing, + onRefresh = pullToRefreshConfig.onRefresh, + ) + + Box(modifier = modifier.pullRefresh(pullRefreshState)) { + content() + + WalletPullToRefreshIndicator( + isRefreshing = pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) + } } @Composable -private fun BaseScaffold(state: WalletState.ContentState, content: @Composable (PaddingValues) -> Unit) { +private fun BaseScaffold( + state: WalletState.ContentState, + snackbarHostState: SnackbarHostState, + content: @Composable (PaddingValues) -> Unit, +) { Scaffold( topBar = { WalletTopBar(config = state.topBarConfig) }, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, floatingActionButton = { if (state is WalletMultiCurrencyState.Content) { ManageTokensButton(onManageTokensClick = state.onManageTokensClick) @@ -160,7 +223,15 @@ private fun ManageTokensButton(onManageTokensClick: () -> Unit) { private fun WalletBottomSheets(state: WalletState) { val bottomSheetConfig = (state as? WalletState.ContentState)?.bottomSheetConfig if (bottomSheetConfig != null && bottomSheetConfig.isShow) { - WalletBottomSheet(config = bottomSheetConfig) + when (bottomSheetConfig.content) { + is WalletBottomSheetConfig -> { + WalletBottomSheet(config = bottomSheetConfig) + } + + is TokenReceiveBottomSheetConfig -> { + TokenReceiveBottomSheet(config = bottomSheetConfig) + } + } } (state as? WalletMultiCurrencyState.Content)?.let { multiCurrencyState -> @@ -175,7 +246,7 @@ private fun WalletBottomSheets(state: WalletState) { @Composable private fun WalletScreenPreview_Light(@PreviewParameter(WalletScreenParameterProvider::class) state: WalletState) { TangemTheme { - WalletScreen(state) + WalletScreen(state = state) } } @@ -183,7 +254,7 @@ private fun WalletScreenPreview_Light(@PreviewParameter(WalletScreenParameterPro @Composable private fun WalletScreenPreview_Dark(@PreviewParameter(WalletScreenParameterProvider::class) state: WalletState) { TangemTheme(isDark = true) { - WalletScreen(state) + WalletScreen(state = state) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt new file mode 100644 index 0000000000..482d206fbf --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletsListEffects.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.snapshotFlow +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector +import com.tangem.feature.wallet.presentation.wallet.ui.utils.WalletsListInteractionsCollector + +@Composable +internal fun WalletsListEffects( + lazyListState: LazyListState, + walletsListConfig: WalletsListConfig, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, +) { + LaunchedEffect(key1 = lazyListState, key2 = walletsListConfig.onWalletChange) { + snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } + .collect( + collector = ScrollOffsetCollector( + lazyListState = lazyListState, + walletsListConfig = walletsListConfig, + isAutoScroll = isAutoScroll, + ), + ) + } + + LaunchedEffect(Unit) { + lazyListState.interactionSource.interactions.collect( + collector = WalletsListInteractionsCollector(onDragStart = onAutoScrollReset), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index a79d41bb23..7037bcdd39 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -12,6 +12,7 @@ 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.components.SimpleSettingsRow +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.ActionsBottomSheetConfig @@ -38,7 +39,7 @@ private fun ActionsBottomSheetContent(actions: ImmutableList SimpleSettingsRow( - title = action.text, + title = action.text.resolveReference(), icon = action.iconResId, enabled = action.enabled, onItemsClick = action.onClick, @@ -65,7 +66,7 @@ private fun ActionsBottomSheetContent_Dark( @PreviewParameter(ActionsBottomSheetContentConfigProvider::class) config: ActionsBottomSheetConfig, ) { - TangemTheme(isDark = false) { + TangemTheme(isDark = true) { // Use preview of content because ModalBottomSheet isn't supported in Preview mode ActionsBottomSheetContent(actions = config.actions) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 09acf941f4..9da275d0cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -1,7 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components +import androidx.compose.animation.core.* +import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior +import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -10,19 +14,20 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard +private const val SHORT_SNAP_ELEMENT_COUNT = 50 + /** * Wallets list component * @@ -43,7 +48,7 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState state = lazyListState, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), + flingBehavior = rememberWalletsFlingBehaviour(lazyListState = lazyListState, itemWidth = itemWidth), ) { items( items = config.wallets, @@ -60,6 +65,35 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState } } +/** + * Custom implementation of fling behaviour that overrides 'shortSnapVelocityThreshold'. + * Every user's drag action will similar to a short snap + * if drag offset is less than [SHORT_SNAP_ELEMENT_COUNT] * item width. + * + * @param lazyListState lazy list state + * @param itemWidth list item width + * + * @see rememberSnapFlingBehavior + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun rememberWalletsFlingBehaviour(lazyListState: LazyListState, itemWidth: Dp): SnapFlingBehavior { + val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) } + val density = LocalDensity.current + val highVelocityApproachSpec: DecayAnimationSpec = rememberSplineBasedDecay() + + return remember(key1 = snappingLayout, key2 = highVelocityApproachSpec, key3 = density) { + SnapFlingBehavior( + snapLayoutInfoProvider = snappingLayout, + lowVelocityAnimationSpec = tween(durationMillis = 1000, easing = LinearEasing), + highVelocityAnimationSpec = highVelocityApproachSpec, + snapAnimationSpec = spring(stiffness = Spring.StiffnessMediumLow), + density = density, + shortSnapVelocityThreshold = itemWidth * SHORT_SNAP_ELEMENT_COUNT, + ) + } +} + @Preview @Composable private fun Preview_WalletsList_LightTheme() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt index bf78b633da..8794acf686 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt @@ -15,6 +15,8 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData @@ -27,21 +29,17 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBott * [REDACTED_AUTHOR] */ -@OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun WalletBottomSheet(config: WalletBottomSheetConfig) { - ModalBottomSheet( - onDismissRequest = config.onDismissRequest, - sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), - containerColor = TangemTheme.colors.background.primary, - dragHandle = { BottomSheetDefaults.DragHandle() }, - ) { - BottomSheetContent(config = config.content) +internal fun WalletBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config) { content -> + BottomSheetContent( + config = content as WalletBottomSheetConfig, + ) } } @Composable -private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetContentConfig) { +private fun BottomSheetContent(config: WalletBottomSheetConfig) { Column( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) @@ -90,10 +88,7 @@ private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetConten } @Composable -private fun PrimaryButton( - config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig, - modifier: Modifier = Modifier, -) { +private fun PrimaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) { if (config.iconResId == null) { PrimaryButton( text = config.text.resolveReference(), @@ -111,10 +106,7 @@ private fun PrimaryButton( } @Composable -private fun SecondaryButton( - config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig, - modifier: Modifier = Modifier, -) { +private fun SecondaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) { if (config.iconResId == null) { SecondaryButton( text = config.text.resolveReference(), @@ -139,7 +131,7 @@ private fun WalletBottomSheetContent_Light( ) { TangemTheme(isDark = false) { // Use preview of content because ModalBottomSheet isn't supported in Preview mode - BottomSheetContent(config = config.content) + BottomSheetContent(config = config) } } @@ -151,10 +143,10 @@ private fun WalletBottomSheetContent_Dark( ) { TangemTheme(isDark = false) { // Use preview of content because ModalBottomSheet isn't supported in Preview mode - BottomSheetContent(config = config.content) + BottomSheetContent(config = config) } } -private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider( +private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider( collection = listOf(WalletPreviewData.bottomSheet), ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 494e36cee2..d1c6cfeb73 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -163,22 +164,14 @@ private fun CardContainer( var isRenameWalletDialogVisible by rememberSaveable { mutableStateOf(value = false) } - DropdownMenu( - expanded = isMenuVisible, + ManageWalletContextMenu( + isMenuVisible = isMenuVisible, + pressOffset = pressOffset, + itemHeight = itemHeight, onDismissRequest = { isMenuVisible = false }, - modifier = Modifier.background(color = TangemTheme.colors.background.secondary), - offset = pressOffset.copy(y = pressOffset.y - itemHeight), - ) { - MenuItem( - textResId = R.string.common_rename, - imageVector = Icons.Outlined.Edit, - onClick = { - isMenuVisible = false - isRenameWalletDialogVisible = true - }, - ) - MenuItem(textResId = R.string.common_delete, imageVector = Icons.Outlined.Delete, onClick = onDeleteClick) - } + onShowRenameWalletDialogClick = { isRenameWalletDialogVisible = true }, + onDeleteClick = onDeleteClick, + ) if (isRenameWalletDialogVisible) { RenameWalletDialogContent( @@ -192,6 +185,41 @@ private fun CardContainer( } } +@Suppress("LongParameterList") +@Composable +private fun ManageWalletContextMenu( + isMenuVisible: Boolean, + pressOffset: DpOffset, + itemHeight: Dp, + onDismissRequest: () -> Unit, + onShowRenameWalletDialogClick: () -> Unit, + onDeleteClick: () -> Unit, +) { + DropdownMenu( + expanded = isMenuVisible, + onDismissRequest = onDismissRequest, + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + offset = pressOffset.copy(y = pressOffset.y - itemHeight), + ) { + MenuItem( + textResId = R.string.common_rename, + imageVector = Icons.Outlined.Edit, + onClick = { + onDismissRequest() + onShowRenameWalletDialogClick() + }, + ) + MenuItem( + textResId = R.string.common_delete, + imageVector = Icons.Outlined.Delete, + onClick = { + onDismissRequest() + onDeleteClick() + }, + ) + } +} + @Composable private fun MenuItem(@StringRes textResId: Int, imageVector: ImageVector, onClick: () -> Unit) { DropdownMenuItem( @@ -213,15 +241,6 @@ private fun Title(state: WalletCardState, modifier: Modifier = Modifier) { horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), ) { TitleText(title = state.title) - - AnimatedVisibility(visible = state is WalletCardState.HiddenContent, label = "Update the hidden icon") { - Icon( - modifier = Modifier.size(size = TangemTheme.dimens.size20), - painter = painterResource(id = R.drawable.ic_eye_off_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } } } @@ -230,7 +249,7 @@ private fun TitleText(title: String) { Text( text = title, color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, + style = TangemTheme.typography.button, maxLines = 1, ) } @@ -275,28 +294,26 @@ private fun NonContentBalanceText(text: TextReference) { } private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier { - return size(width = dimens.size102, height = dimens.size32) + return this + .padding(vertical = dimens.spacing4) + .size(width = dimens.size102, height = dimens.size24) } @OptIn(ExperimentalAnimationApi::class) @Composable private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier) { AnimatedContent( - targetState = state.additionalInfo, + targetState = state, label = "Update the additional text", modifier = modifier, - ) { additionalInfo -> - if (additionalInfo != null) { - AdditionalInfoText(text = additionalInfo) - } else { - when (state) { - is WalletCardState.Loading -> { - RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(TangemTheme.dimens)) - } - is WalletCardState.LockedContent -> { - LockedContent(modifier = Modifier.nonContentAdditionalInfoSize(TangemTheme.dimens)) - } - else -> Unit + ) { animatedState -> + when (animatedState) { + is WalletCardState.Content -> AdditionalInfoText(text = animatedState.additionalInfo) + is WalletCardState.LockedContent -> AdditionalInfoText(text = animatedState.additionalInfo) + is WalletCardState.Error -> AdditionalInfoText(text = WalletCardState.EMPTY_BALANCE_TEXT) + is WalletCardState.HiddenContent -> AdditionalInfoText(text = WalletCardState.HIDDEN_BALANCE_TEXT) + is WalletCardState.Loading -> { + RectangleShimmer(modifier = Modifier.nonContentAdditionalInfoSize(dimens = TangemTheme.dimens)) } } } @@ -306,7 +323,7 @@ private fun AdditionalInfo(state: WalletCardState, modifier: Modifier = Modifier private fun AdditionalInfoText(text: TextReference) { Text( text = text.resolveReference(), - color = TangemTheme.colors.text.disabled, + color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index c6ef7f89ef..22c20dcfc7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -20,8 +21,20 @@ import kotlinx.collections.immutable.ImmutableList internal fun LazyListScope.notifications(configs: ImmutableList, modifier: Modifier = Modifier) { items( items = configs, - key = { it.state.title.hashCode() }, - contentType = { it.state::class.java }, - itemContent = { Notification(state = it.state, modifier = modifier.animateItemPlacement()) }, + key = { it::class.java }, + contentType = { it.config::class.java }, + itemContent = { + Notification( + config = it.config, + modifier = modifier.animateItemPlacement(), + iconTint = when (it) { + is WalletNotification.Critical -> TangemTheme.colors.icon.warning + is WalletNotification.MissingAddresses -> TangemTheme.colors.icon.accent + is WalletNotification.RateApp -> TangemTheme.colors.icon.attention + is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + is WalletNotification.Warning -> null + }, + ) + }, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt deleted file mode 100644 index 0d415eaa74..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletSideEffects.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.common - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.snapshotFlow -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig -import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector - -/** - * Wallet screen side effects - * - * @param lazyListState lazy list state - * @param walletsListConfig wallets list config - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun WalletSideEffects(lazyListState: LazyListState, walletsListConfig: WalletsListConfig) { - LaunchedEffect(key1 = walletsListConfig.selectedWalletIndex) { - lazyListState.scrollToItem(walletsListConfig.selectedWalletIndex) - } - - val dragInteraction = lazyListState.interactionSource.interactions.collectAsState(initial = null) - LaunchedEffect(key1 = lazyListState, key2 = walletsListConfig.onWalletChange) { - snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } - .collect( - collector = ScrollOffsetCollector( - lazyListState = lazyListState, - dragInteraction = dragInteraction, - callback = walletsListConfig.onWalletChange, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index 5b809bf2e7..d22e971baf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -12,7 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTopB /** * Wallet screen top bar * - * @param config top bar config + * @param config component config */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -22,11 +22,8 @@ internal fun WalletTopBar(config: WalletTopBarConfig) { Icon(painter = painterResource(id = R.drawable.img_tangem_logo_90_24), contentDescription = null) }, actions = { - IconButton(onClick = config.onScanCardClick) { - Icon(painter = painterResource(id = R.drawable.ic_tap_card_24), contentDescription = "Scan card") - } - IconButton(onClick = config.onMoreClick) { - Icon(painter = painterResource(id = R.drawable.ic_more_vertical_24), contentDescription = "More") + IconButton(onClick = config.onDetailsClick) { + Icon(painter = painterResource(id = R.drawable.ic_more_vertical_24), contentDescription = null) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -42,7 +39,7 @@ internal fun WalletTopBar(config: WalletTopBarConfig) { @Composable private fun Preview_WalletTopBar_LightTheme() { TangemTheme(isDark = false) { - WalletTopBar(config = WalletPreviewData.walletTopBarConfig) + WalletTopBar(config = WalletPreviewData.topBarConfig) } } @@ -50,6 +47,6 @@ private fun Preview_WalletTopBar_LightTheme() { @Composable private fun Preview_WalletTopBar_DarkTheme() { TangemTheme(isDark = true) { - WalletTopBar(config = WalletPreviewData.walletTopBarConfig) + WalletTopBar(config = WalletPreviewData.topBarConfig) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 350cb95ecf..1330515bf4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -44,12 +44,7 @@ private fun LazyListScope.contentItems( ) { itemsIndexed( items = items, - key = { _, item -> - when (item) { - is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> item.value.hashCode() - is WalletTokensListState.TokensListItemState.Token -> item.state.id - } - }, + key = { _, item -> item.id }, contentType = { _, item -> item::class.java }, itemContent = { index, item -> MultiCurrencyContentItem( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt index a9ac43d4ab..882548c609 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -19,7 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletToke internal fun MultiCurrencyContentItem(state: WalletTokensListState.TokensListItemState, modifier: Modifier = Modifier) { when (state) { is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> { - NetworkGroupItem(networkName = state.value.resolveReference(), modifier = modifier) + NetworkGroupItem(networkName = state.name.resolveReference(), modifier = modifier) } is WalletTokensListState.TokensListItemState.Token -> { TokenItem(state = state.state, modifier = modifier) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt index 210bfd423c..8b981deeb7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt @@ -1,8 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.wallet.impl.R private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton" @@ -14,9 +17,20 @@ private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton" * [REDACTED_AUTHOR] */ -@OptIn(ExperimentalFoundationApi::class) -internal fun LazyListScope.organizeButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { +internal fun LazyListScope.organizeTokensButton( + isEnabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) { - OrganizeTokensButton(onClick = onClick, modifier = modifier.animateItemPlacement()) + RoundedActionButton( + modifier = modifier, + config = ActionButtonConfig( + text = resourceReference(id = R.string.organize_tokens_title), + iconResId = R.drawable.ic_filter_24, + onClick = onClick, + enabled = isEnabled, + ), + ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt deleted file mode 100644 index 53aeef026b..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig -import com.tangem.core.ui.components.buttons.actions.RoundedActionButton -import com.tangem.core.ui.extensions.TextReference -import com.tangem.feature.wallet.impl.R - -/** - * Organize tokens button - * - * @param onClick callback, if null button is disabled - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun OrganizeTokensButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { - RoundedActionButton( - config = ActionButtonConfig( - text = TextReference.Res(id = R.string.organize_tokens_title), - iconResId = R.drawable.ic_filter_24, - onClick = onClick ?: {}, - enabled = onClick != null, - ), - modifier = modifier, - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt index 1cfc37798f..60f81583b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ScrollOffsetCollector.kt @@ -1,40 +1,58 @@ package com.tangem.feature.wallet.presentation.wallet.ui.utils -import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.lazy.LazyListItemInfo import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.State +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig import kotlinx.coroutines.flow.FlowCollector import kotlin.math.abs /** * Flow collector for scroll items tracking. - * If first visible item offset is greater than half item size, then [callback] be invoked. - * If last visible item offset is greater than half item size, then [callback] be invoked. + * If first visible item offset is greater than half item size, then change selected wallet index. + * If last visible item offset is greater than half item size, then change selected wallet index. * - * @property lazyListState lazy list state - * @property dragInteraction current drag interaction - * @property callback lambda be invoked when current scroll items is changed + * @property lazyListState lazy list state + * @property walletsListConfig wallets list config + * @property isAutoScroll check if last scrolling is auto scroll * [REDACTED_AUTHOR] */ internal class ScrollOffsetCollector( private val lazyListState: LazyListState, - private val dragInteraction: State, - private val callback: (Int) -> Unit, + private val walletsListConfig: WalletsListConfig, + private val isAutoScroll: State, ) : FlowCollector> { private val LazyListItemInfo.halfItemSize get() = size.div(other = 2) + private var currentIndex = walletsListConfig.selectedWalletIndex + set(value) { + if (field != value) { + field = value + } + } + override suspend fun emit(value: List) { - if (!lazyListState.isScrollInProgress || dragInteraction.value == null || value.size <= 1) return + // Auto scroll must not change wallet + if (isAutoScroll.value) { + currentIndex = walletsListConfig.selectedWalletIndex + return + } + + if (!lazyListState.isScrollInProgress || value.size <= 1) return + val firstItem = value.firstOrNull() ?: return val lastItem = value.lastOrNull() ?: return if (abs(firstItem.offset) > firstItem.halfItemSize) { - callback(firstItem.index + 1) + val newIndex = firstItem.index + 1 + currentIndex = newIndex + walletsListConfig.onWalletChange(newIndex) } else if (abs(lastItem.offset) > lastItem.halfItemSize) { - callback(lastItem.index - 1) + val newIndex = lastItem.index - 1 + currentIndex = newIndex + walletsListConfig.onWalletChange(newIndex) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsListInteractionsCollector.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsListInteractionsCollector.kt new file mode 100644 index 0000000000..49ade64a90 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/WalletsListInteractionsCollector.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.utils + +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction +import kotlinx.coroutines.flow.FlowCollector + +internal class WalletsListInteractionsCollector( + private val onDragStart: () -> Unit, +) : FlowCollector { + + override suspend fun emit(value: Interaction?) { + if (value is DragInteraction.Start) onDragStart() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt index b19dcf646a..5a7fc06b24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -2,22 +2,23 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider import com.tangem.core.ui.components.marketprice.PriceChangeConfig -import com.tangem.core.ui.extensions.iconResId -import com.tangem.core.ui.extensions.networkBadgeIconResId import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.common.utils.CryptoCurrencyToIconStateConverter import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import java.math.BigDecimal internal class CryptoCurrencyStatusToTokenItemConverter( private val appCurrencyProvider: Provider, - private val isWalletContentHidden: Boolean, + private val isBalanceHiddenProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { is CryptoCurrencyStatus.Loading -> TokenItemState.Loading(id = value.currency.id.value) @@ -29,6 +30,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.NoAccount, is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, -> value.mapToUnreachableTokenItemState() } } @@ -37,22 +39,16 @@ internal class CryptoCurrencyStatusToTokenItemConverter( return TokenItemState.Content( id = currency.id.value, name = currency.name, - tokenIconUrl = currency.iconUrl, - tokenIconResId = currency.iconResId, - networkBadgeIconResId = currency.networkBadgeIconResId, + icon = iconStateConverter.convert(value = this), amount = getFormattedAmount(), hasPending = value.hasCurrentNetworkTransactions, - tokenOptions = if (isWalletContentHidden) { - TokenItemState.TokenOptionsState.Hidden(getPriceChangeConfig()) - } else { - TokenItemState.TokenOptionsState.Visible( - fiatAmount = getFormattedFiatAmount(), - config = getPriceChangeConfig(), - ) - }, - isTestnet = currency.network.isTestnet, + tokenOptions = TokenItemState.TokenOptionsState( + fiatAmount = getFormattedFiatAmount(), + config = getPriceChangeConfig(), + isBalanceHidden = isBalanceHiddenProvider(), + ), onItemClick = { clickIntents.onTokenItemClick(currency) }, - onItemLongClick = { clickIntents.onTokenItemLongClick(currency) }, + onItemLongClick = { clickIntents.onTokenItemLongClick(cryptoCurrencyStatus = this) }, ) } @@ -72,9 +68,7 @@ internal class CryptoCurrencyStatusToTokenItemConverter( private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( id = currency.id.value, name = currency.name, - tokenIconUrl = currency.iconUrl, - tokenIconResId = currency.iconResId, - networkBadgeIconResId = currency.networkBadgeIconResId, + icon = iconStateConverter.convert(value = this), ) private fun CryptoCurrencyStatus.getPriceChangeConfig(): PriceChangeConfig { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CurrencyStatusErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CurrencyStatusErrorConverter.kt new file mode 100644 index 0000000000..352f9f4739 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CurrencyStatusErrorConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.common.Converter +import com.tangem.common.Provider +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState + +// TODO: Implement this +internal class CurrencyStatusErrorConverter( + private val currentStateProvider: Provider, +) : Converter { + + override fun convert(value: CurrencyStatusError): WalletSingleCurrencyState.Content { + return requireNotNull(currentStateProvider() as? WalletSingleCurrencyState.Content) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt index 05340d37ed..89e3ff8be5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList.FiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory @@ -12,10 +11,9 @@ import com.tangem.utils.converter.Converter internal class FiatBalanceToWalletCardConverter( private val currentState: WalletCardState, - private val cardTypeResolverProvider: Provider, private val appCurrencyProvider: Provider, private val currentWalletProvider: Provider, - private val isWalletContentHidden: Boolean, + private val isBalanceHiddenProvider: Provider, ) : Converter { override fun convert(value: FiatBalance): WalletCardState { @@ -27,7 +25,7 @@ internal class FiatBalanceToWalletCardConverter( } private fun WalletCardState.toLoadingWalletCardState(): WalletCardState { - return WalletCardState.Loading(id, title, additionalInfo, imageResId, onRenameClick, onDeleteClick) + return WalletCardState.Loading(id, title, imageResId, onRenameClick, onDeleteClick) } private fun WalletCardState.toErrorWalletCardState(): WalletCardState { @@ -37,33 +35,31 @@ internal class FiatBalanceToWalletCardConverter( imageResId = imageResId, onDeleteClick = onDeleteClick, onRenameClick = onRenameClick, - additionalInfo = WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolverProvider(), - wallet = currentWalletProvider(), - ), ) } private fun FiatBalance.Loaded.convertToWalletCardState(): WalletCardState { - return if (isWalletContentHidden) { + val appCurrency = appCurrencyProvider() + + return if (isBalanceHiddenProvider()) { WalletCardState.HiddenContent( id = currentState.id, title = currentState.title, - additionalInfo = currentState.additionalInfo ?: WalletCardState.HIDDEN_BALANCE_TEXT, imageResId = currentState.imageResId, onRenameClick = currentState.onRenameClick, onDeleteClick = currentState.onDeleteClick, + balance = formatFiatAmount( + fiatAmount = this.amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = currentWalletProvider()), ) } else { - val appCurrency = appCurrencyProvider() - WalletCardState.Content( id = currentState.id, title = currentState.title, - additionalInfo = WalletAdditionalInfoFactory.resolve( - cardTypesResolver = cardTypeResolverProvider(), - wallet = currentWalletProvider(), - ), + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = currentWalletProvider()), imageResId = currentState.imageResId, onRenameClick = currentState.onRenameClick, onDeleteClick = currentState.onDeleteClick, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt new file mode 100644 index 0000000000..a5f5ea9808 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/HiddenStateConverter.kt @@ -0,0 +1,70 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.common.Converter +import com.tangem.common.Provider +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenItemHiddenStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import kotlinx.collections.immutable.toImmutableList + +internal class HiddenStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + private val walletHiddenBalanceStateConverter by lazy { WalletHiddenBalanceStateConverter() } + + private val tokenItemHiddenStateConverter by lazy { TokenItemHiddenStateConverter() } + + override fun convert(value: Boolean): WalletState { + return when (val state = currentStateProvider() as? WalletState.ContentState) { + is WalletMultiCurrencyState.Content -> { + val updatedTokensList = (state.tokensListState as? WalletTokensListState.Content)?.let { content -> + content.copy( + items = content.items.map { tokenListItemState -> + if (tokenListItemState is WalletTokensListState.TokensListItemState.Token) { + if (tokenListItemState.state is TokenItemState.Content) { + tokenListItemState.copy( + state = tokenListItemState.state.copy( + tokenOptions = tokenItemHiddenStateConverter.updateHiddenState( + optionsState = tokenListItemState.state.tokenOptions, + isBalanceHidden = value, + ), + ), + ) + } else { + tokenListItemState + } + } else { + tokenListItemState + } + }.toImmutableList(), + ) + } ?: state.tokensListState + + state.copy( + walletsListConfig = state.walletsListConfig.copy( + wallets = state.walletsListConfig.wallets.map { + walletHiddenBalanceStateConverter.updateHiddenState(it, value) + }.toImmutableList(), + ), + tokensListState = updatedTokensList, + ) + } + + is WalletSingleCurrencyState.Content -> { + state.copy( + walletsListConfig = state.walletsListConfig.copy( + wallets = state.walletsListConfig.wallets.map { + walletHiddenBalanceStateConverter.updateHiddenState(it, value) + }.toImmutableList(), + ), + ) + } + + else -> currentStateProvider() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt index 82709f83a7..100ea1b8bf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorConverter.kt @@ -19,7 +19,7 @@ internal class TokenListErrorConverter( state.copy( tokensListState = WalletTokensListState.Content( items = persistentListOf(), - onOrganizeTokensClick = null, + organizeTokensButton = WalletTokensListState.OrganizeTokensButtonState.Hidden, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt index ca50986a37..24d808bd72 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -1,14 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.OrganizeTokensButtonState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState.TokensListItemState -import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList @@ -17,37 +17,26 @@ import kotlinx.collections.immutable.persistentListOf internal class TokenListToContentItemsConverter( appCurrencyProvider: Provider, - isWalletContentHidden: Boolean, + isBalanceHiddenProvider: Provider, private val clickIntents: WalletClickIntents, ) : Converter { private val tokenStatusConverter = CryptoCurrencyStatusToTokenItemConverter( - isWalletContentHidden = isWalletContentHidden, + isBalanceHiddenProvider = isBalanceHiddenProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) override fun convert(value: TokenList): WalletTokensListState { - val isEmptyList = when (value) { - is TokenList.GroupedByNetwork -> value.groups.isEmpty() - is TokenList.NotInitialized -> false - is TokenList.Ungrouped -> value.currencies.isEmpty() - } - - return if (isEmptyList) { - WalletTokensListState.Empty - } else { - WalletTokensListState.Content( - items = when (value) { - is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems() - is TokenList.Ungrouped -> value.mapToMultiCurrencyItems() - is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens() - }, - onOrganizeTokensClick = if (value.totalFiatBalance is TokenList.FiatBalance.Loaded) { - clickIntents::onOrganizeTokensClick - } else { - null - }, + return when (value) { + is TokenList.NotInitialized -> WalletTokensListState.Loading() + is TokenList.GroupedByNetwork -> WalletTokensListState.Content( + items = value.mapToMultiCurrencyItems(), + organizeTokensButton = value.mapToOrganizeTokensButtonState(), + ) + is TokenList.Ungrouped -> WalletTokensListState.Content( + items = value.mapToMultiCurrencyItems(), + organizeTokensButton = value.mapToOrganizeTokensButtonState(), ) } } @@ -64,8 +53,27 @@ internal class TokenListToContentItemsConverter( } } + private fun TokenList.GroupedByNetwork.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState { + return getOrganizeTokensButtonState( + isLoading = totalFiatBalance is TokenList.FiatBalance.Loading, + currenciesSize = groups.flatMap(NetworkGroup::currencies).size, + ) + } + + private fun TokenList.Ungrouped.mapToOrganizeTokensButtonState(): OrganizeTokensButtonState { + return getOrganizeTokensButtonState( + isLoading = totalFiatBalance is TokenList.FiatBalance.Loading, + currenciesSize = currencies.size, + ) + } + private fun MutableList.addGroup(group: NetworkGroup): List { - this.add(TokensListItemState.NetworkGroupTitle(TextReference.Str(group.network.name))) + val groupTitle = TokensListItemState.NetworkGroupTitle( + id = group.network.hashCode(), + name = stringReference(group.network.name), + ) + + this.add(groupTitle) group.currencies.forEach { token -> this.addToken(token) @@ -81,4 +89,15 @@ internal class TokenListToContentItemsConverter( return this } + + private fun getOrganizeTokensButtonState(isLoading: Boolean, currenciesSize: Int): OrganizeTokensButtonState { + return if (currenciesSize > 1) { + OrganizeTokensButtonState.Visible( + isEnabled = !isLoading, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + OrganizeTokensButtonState.Hidden + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt index 35aa99598f..eabeaf9611 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -2,13 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.utils import com.tangem.common.Provider import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState +import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState import com.tangem.feature.wallet.presentation.wallet.state.WalletState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletsListConfig -import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList @@ -16,30 +15,32 @@ import kotlinx.collections.immutable.toPersistentList @Suppress("LongParameterList") internal class TokenListToWalletStateConverter( private val currentStateProvider: Provider, - private val cardTypeResolverProvider: Provider, private val currentWalletProvider: Provider, private val appCurrencyProvider: Provider, - private val isWalletContentHidden: Boolean, + private val isBalanceHiddenProvider: Provider, clickIntents: WalletClickIntents, -) : Converter { +) : Converter { private val tokenListToContentConverter = TokenListToContentItemsConverter( - isWalletContentHidden = isWalletContentHidden, + isBalanceHiddenProvider = isBalanceHiddenProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, ) - override fun convert(value: TokensListModel): WalletMultiCurrencyState.Content { - val state = requireNotNull(currentStateProvider() as? WalletMultiCurrencyState.Content) - return state.copy( - walletsListConfig = state.updateSelectedWallet(fiatBalance = value.tokenList.totalFiatBalance), - pullToRefreshConfig = if (value.isRefreshing) { - state.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus(tokenList = value.tokenList)) - } else { - state.pullToRefreshConfig - }, - tokensListState = tokenListToContentConverter.convert(value = value.tokenList), - ) + override fun convert(value: TokenList): WalletState { + return when (val state = currentStateProvider()) { + is WalletMultiCurrencyState.Content -> { + state.copy( + walletsListConfig = state.updateSelectedWallet(fiatBalance = value.totalFiatBalance), + tokensListState = tokenListToContentConverter.convert(value = value), + ) + } + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Content, + is WalletSingleCurrencyState.Locked, + is WalletState.Initial, + -> state + } } private fun WalletMultiCurrencyState.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { @@ -48,9 +49,8 @@ internal class TokenListToWalletStateConverter( val converter = FiatBalanceToWalletCardConverter( currentState = selectedWalletCard, currentWalletProvider = currentWalletProvider, - cardTypeResolverProvider = cardTypeResolverProvider, appCurrencyProvider = appCurrencyProvider, - isWalletContentHidden = isWalletContentHidden, + isBalanceHiddenProvider = isBalanceHiddenProvider, ) return walletsListConfig.copy( @@ -58,10 +58,4 @@ internal class TokenListToWalletStateConverter( .set(index = selectedWalletIndex, element = converter.convert(fiatBalance)), ) } - - private fun getRefreshingStatus(tokenList: TokenList): Boolean { - return tokenList.totalFiatBalance is TokenList.FiatBalance.Loading - } - - data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt new file mode 100644 index 0000000000..7f29843cea --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletHiddenBalanceStateConverter.kt @@ -0,0 +1,42 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState + +internal class WalletHiddenBalanceStateConverter { + + fun updateHiddenState(walletCardState: WalletCardState, hiddenBalance: Boolean): WalletCardState { + return when { + walletCardState is WalletCardState.Content && hiddenBalance -> { + contentToHidden(walletCardState) + } + walletCardState is WalletCardState.HiddenContent && !hiddenBalance -> { + hiddenToContent(walletCardState) + } + else -> walletCardState + } + } + + private fun contentToHidden(content: WalletCardState.Content): WalletCardState.HiddenContent { + return WalletCardState.HiddenContent( + id = content.id, + title = content.title, + additionalInfo = content.additionalInfo, + imageResId = content.imageResId, + onRenameClick = content.onRenameClick, + onDeleteClick = content.onDeleteClick, + balance = content.balance, + ) + } + + private fun hiddenToContent(hiddenContent: WalletCardState.HiddenContent): WalletCardState.Content { + return WalletCardState.Content( + id = hiddenContent.id, + title = hiddenContent.title, + additionalInfo = hiddenContent.additionalInfo, + imageResId = hiddenContent.imageResId, + onRenameClick = hiddenContent.onRenameClick, + onDeleteClick = hiddenContent.onDeleteClick, + balance = hiddenContent.balance, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index c3ba714c76..e0da1fe275 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -1,30 +1,28 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels -import com.tangem.core.ui.components.transactions.intents.TxHistoryClickIntents +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId @Suppress("TooManyFunctions") -internal interface WalletClickIntents : TxHistoryClickIntents { +internal interface WalletClickIntents { fun onBackClick() - fun onScanCardClick() + fun onGenerateMissedAddressesClick() + + fun onScanToUnlockWalletClick() fun onDetailsClick() fun onBackupCardClick() - fun onCriticalWarningAlreadySignedHashesClick() - - fun onCloseWarningAlreadySignedHashesClick() + fun onMultiWalletSignedHashesNotificationClick() fun onLikeTangemAppClick() fun onRateTheAppClick() - fun onShareClick() - fun onWalletChange(index: Int) fun onRefreshSwipe() @@ -39,7 +37,7 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onTokenItemClick(currency: CryptoCurrency) - fun onTokenItemLongClick(currency: CryptoCurrency) + fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) fun onDismissActionsBottomSheet() @@ -47,11 +45,21 @@ internal interface WalletClickIntents : TxHistoryClickIntents { fun onDeleteClick(userWalletId: UserWalletId) - fun onSendClick() + fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus? = null) - fun onReceiveClick() + fun onMultiCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onSellClick() + fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) fun onManageTokensClick() + + fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + + fun onReloadClick() + + fun onExploreClick() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index fb70c489e4..e6a3d1a22c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -1,9 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels +import com.tangem.domain.card.GetCardWasScannedUseCase import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup -import com.tangem.domain.tokens.model.TokenList import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -13,126 +14,166 @@ import kotlinx.coroutines.flow.flow /** * Wallet notifications list factory * - * @property wasCardScannedCallback callback that check if card was scanned - * @property isUserAlreadyRateAppCallback callback that check if card is user already rate app - * @property isDemoCardCallback callback that check if card is demo - * @property clickIntents screen click intents + * @property isDemoCardUseCase use case that check if card is demo + * @property isUserAlreadyRateAppUseCase use case that check if card is user already rate app + * @property getCardWasScannedUseCase use case that check if card was scanned + * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ internal class WalletNotificationsListFactory( - private val wasCardScannedCallback: suspend (String) -> Boolean, - private val isUserAlreadyRateAppCallback: suspend () -> Boolean, - private val isDemoCardCallback: (String) -> Boolean, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, + private val getCardWasScannedUseCase: GetCardWasScannedUseCase, private val clickIntents: WalletClickIntents, ) { - fun create(cardTypesResolver: CardTypesResolver, tokenList: TokenList?): Flow> { - // TODO: [REDACTED_JIRA] order + fun create( + cardTypesResolver: CardTypesResolver, + cryptoCurrencyList: List, + ): Flow> { return flow { emit( buildList { - if (cardTypesResolver.isTestCard()) { - add(element = WalletNotification.TestCard) - return@buildList - } + addCriticalNotifications(cardTypesResolver) - addRemainingSignaturesLeftNotifications(cardTypesResolver) + addMissingAddressesNotification(cryptoCurrencyList) - val isDemo = isDemoCardCallback(cardTypesResolver.getCardId()) - if (!cardTypesResolver.isReleaseFirmwareType()) { - add(element = WalletNotification.DevCard) - } else { - addReleaseSpecialNotifications(cardTypesResolver = cardTypesResolver, isDemo = isDemo) - } + addRateTheAppNotification() - if (isDemo) { - add(element = WalletNotification.DemoCard) - } - - if (hasUnreachableNetworks(tokenList)) { - add(element = WalletNotification.UnreachableNetworks) - } - - if (!cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup()) { - add(element = WalletNotification.BackupCard(onClick = clickIntents::onBackupCardClick)) - } - - if (tokenList != null && tokenList.hasMissedDerivations()) { - add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardClick)) - } - - if (isUserAlreadyRateAppCallback()) { - add(element = WalletNotification.LikeTangemApp(onClick = clickIntents::onLikeTangemAppClick)) - } - }.toImmutableList(), + addWarningNotifications(cardTypesResolver, cryptoCurrencyList) + } + .toImmutableList(), ) } } - private fun MutableList.addRemainingSignaturesLeftNotifications( - cardTypesResolver: CardTypesResolver, - ) { - val remainingSignatures = cardTypesResolver.getRemainingSignatures() - if (remainingSignatures != null && remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT) { - add(element = WalletNotification.RemainingSignaturesLeft(remainingSignatures)) + private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { + addIf( + element = WalletNotification.Critical.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotification.Critical.DemoCard, + condition = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + + addIf( + element = WalletNotification.Critical.TestNetCard, + condition = cardTypesResolver.isTestCard(), + ) + + addIf( + element = WalletNotification.Critical.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotification.Critical.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) } } - private suspend fun MutableList.addReleaseSpecialNotifications( - cardTypesResolver: CardTypesResolver, - isDemo: Boolean, + private fun MutableList.addMissingAddressesNotification( + cryptoCurrencyList: List, ) { - if (!wasCardScannedCallback(cardTypesResolver.getCardId()) && cardTypesResolver.isMultiwalletAllowed() && - !isDemo - ) { - if (cardTypesResolver.isBackupForbidden() && cardTypesResolver.hasWalletSignedHashes()) { - add( - element = WalletNotification.CriticalWarningAlreadySignedHashes( - onClick = clickIntents::onCriticalWarningAlreadySignedHashesClick, - ), + val missedAddressesCount = cryptoCurrencyList.getMissedAddressesCount() + + addIf( + element = WalletNotification.MissingAddresses( + missingAddressesCount = missedAddressesCount, + onGenerateClick = clickIntents::onGenerateMissedAddressesClick, + ), + condition = missedAddressesCount > 0, + ) + } + + private fun List.getMissedAddressesCount(): Int { + return filterIsInstance().count() + } + + // TODO: [REDACTED_JIRA] + private suspend fun MutableList.addRateTheAppNotification() { + addIf( + element = WalletNotification.RateApp( + onPositiveClick = {}, + onNegativeClick = {}, + onCloseClick = {}, + ), + condition = isUserAlreadyRateAppUseCase(), + ) + } + + private suspend fun MutableList.addWarningNotifications( + cardTypesResolver: CardTypesResolver, + cryptoCurrencyList: List, + ) { + addIf( + element = WalletNotification.Warning.MissingBackup( + onStartBackupClick = clickIntents::onBackupCardClick, + ), + condition = !cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup(), + ) + + val isDemo = isDemoCardUseCase(cardId = cardTypesResolver.getCardId()) + if (cardTypesResolver.isMultiwalletAllowed()) { + addIf( + element = WalletNotification.Warning.SomeNetworksUnreachable, + condition = cryptoCurrencyList.hasUnreachableNetworks(), + ) + + if (cardTypesResolver.isBackupForbidden()) { + addIf( + element = WalletNotification.Warning.NumberOfSignedHashesIncorrect, + condition = checkSignedHashes(cardTypesResolver = cardTypesResolver, isDemo = isDemo), ) - } else if (cardTypesResolver.hasWalletSignedHashes()) { - add( - element = WalletNotification.WarningAlreadySignedHashes( - onClick = clickIntents::onCloseWarningAlreadySignedHashesClick, + } else { + addIf( + element = WalletNotification.Warning.MultiWalletSignedHashesIncorrect( + onClick = clickIntents::onMultiWalletSignedHashesNotificationClick, ), + condition = checkSignedHashes(cardTypesResolver = cardTypesResolver, isDemo = isDemo), ) } - } + } else { + addIf( + element = WalletNotification.Warning.NetworksUnreachable, + condition = cryptoCurrencyList.hasUnreachableNetworks(), + ) - if (cardTypesResolver.isAttestationFailed()) { - add(element = WalletNotification.CardVerificationFailed) + // TODO: [REDACTED_JIRA] + addIf( + element = WalletNotification.Warning.TopUpNote( + errorMessage = "To activate card top up it with at least 1 XLM", + ), + condition = cryptoCurrencyList.hasNoAccountStatus(), + ) + + addIf( + element = WalletNotification.Warning.NumberOfSignedHashesIncorrect, + condition = checkSignedHashes(cardTypesResolver, isDemo), + ) } } - private fun hasUnreachableNetworks(tokenList: TokenList?): Boolean { - return when (tokenList) { - is TokenList.GroupedByNetwork -> { - tokenList.groups - .flatMap(NetworkGroup::currencies) - .map(CryptoCurrencyStatus::value) - .any { it is CryptoCurrencyStatus.Unreachable } - } - is TokenList.Ungrouped -> { - tokenList.currencies - .map(CryptoCurrencyStatus::value) - .any { it is CryptoCurrencyStatus.Unreachable } - } - is TokenList.NotInitialized, - null, - -> false - } + private fun MutableList.addIf(element: WalletNotification, condition: Boolean) { + if (condition) add(element = element) } - private fun TokenList.hasMissedDerivations(): Boolean { - val statuses = when (this) { - is TokenList.GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies).map(CryptoCurrencyStatus::value) - is TokenList.Ungrouped -> currencies.map(CryptoCurrencyStatus::value) - TokenList.NotInitialized -> emptyList() - } + private fun List.hasUnreachableNetworks(): Boolean { + return any { it is CryptoCurrencyStatus.Unreachable } + } - return statuses.any { it is CryptoCurrencyStatus.MissedDerivation } + private fun List.hasNoAccountStatus(): Boolean { + return any { it is CryptoCurrencyStatus.NoAccount } + } + + private suspend fun checkSignedHashes(cardTypesResolver: CardTypesResolver, isDemo: Boolean): Boolean { + return cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.hasWalletSignedHashes() && + !isDemo && !getCardWasScannedUseCase(cardId = cardTypesResolver.getCardId()) } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt index c5d4e10424..f508bacc5a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletStateCache.kt @@ -10,13 +10,13 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletState */ internal object WalletStateCache { - private val states = mutableMapOf() + private val states = mutableMapOf() /** Get state by [userWalletId] */ - fun getState(userWalletId: UserWalletId): WalletState? = states[userWalletId] + fun getState(userWalletId: UserWalletId): WalletState.ContentState? = states[userWalletId] /** Add or update [state] by [userWalletId] */ - fun update(userWalletId: UserWalletId, state: WalletState) { + fun update(userWalletId: UserWalletId, state: WalletState.ContentState) { states[userWalletId] = state } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 509377d260..6e932219ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -3,45 +3,43 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.common.Provider import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.navigation.AppScreen +import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase +import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.card.* import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.settings.CanUseBiometryUseCase -import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase -import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase -import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.settings.* +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.Network import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.domain.userwallets.UserWalletBuilder +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.* import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState -import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState -import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState -import com.tangem.feature.wallet.presentation.wallet.state.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.components.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent +import com.tangem.feature.wallet.presentation.wallet.state.* import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.components.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletStateFactory @@ -49,9 +47,11 @@ 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.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject import kotlin.properties.Delegates @@ -63,9 +63,10 @@ import kotlin.properties.Delegates @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @HiltViewModel internal class WalletViewModel @Inject constructor( + // region Parameters private val getWalletsUseCase: GetWalletsUseCase, private val saveWalletUseCase: SaveWalletUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, + getSelectedWalletUseCase: GetSelectedWalletUseCase, private val selectWalletUseCase: SelectWalletUseCase, private val updateWalletUseCase: UpdateWalletUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, @@ -73,10 +74,10 @@ internal class WalletViewModel @Inject constructor( private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, private val getTokenListUseCase: GetTokenListUseCase, - private val getPrimaryCurrencyUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, - private val getCardWasScannedUseCase: GetCardWasScannedUseCase, - private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, - private val isDemoCardUseCase: IsDemoCardUseCase, + private val fetchTokenListUseCase: FetchTokenListUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val scanCardProcessor: ScanCardProcessor, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, @@ -87,19 +88,28 @@ internal class WalletViewModel @Inject constructor( private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase, + private val listenToFlipsUseCase: ListenToFlipsUseCase, + private val walletManagersFacade: WalletManagersFacade, private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventsHandler: AnalyticsEventHandler, + getCardWasScannedUseCase: GetCardWasScannedUseCase, + isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, + isDemoCardUseCase: IsDemoCardUseCase, + // endregion Parameters ) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { /** Feature router */ var router: InnerWalletRouter by Delegates.notNull() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + private var isBalanceHidden = true private val notificationsListFactory = WalletNotificationsListFactory( - wasCardScannedCallback = getCardWasScannedUseCase::invoke, - isUserAlreadyRateAppCallback = isUserAlreadyRateAppUseCase::invoke, - isDemoCardCallback = isDemoCardUseCase::invoke, + getCardWasScannedUseCase = getCardWasScannedUseCase, + isUserAlreadyRateAppUseCase = isUserAlreadyRateAppUseCase, + isDemoCardUseCase = isDemoCardUseCase, clickIntents = this, ) @@ -114,6 +124,7 @@ internal class WalletViewModel @Inject constructor( wallets[requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex] }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + isBalanceHiddenProvider = Provider { isBalanceHidden }, clickIntents = this, ) @@ -121,14 +132,23 @@ internal class WalletViewModel @Inject constructor( var uiState: WalletState by uiStateHolder(initialState = stateFactory.getInitialState()) private var wallets: List by Delegates.notNull() - private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var singleWalletCryptoCurrencyStatus: CryptoCurrencyStatus? = null private val tokensJobHolder = JobHolder() private val marketPriceJobHolder = JobHolder() private val buttonsJobHolder = JobHolder() private val notificationsJobHolder = JobHolder() + private val refreshContentJobHolder = JobHolder() + private val onWalletChangeJobHolder = JobHolder() + + private val walletsUpdateActionResolver = WalletsUpdateActionResolver( + currentStateProvider = Provider { uiState }, + getSelectedWalletUseCase = getSelectedWalletUseCase, + ) override fun onCreate(owner: LifecycleOwner) { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.ScreenOpened) + viewModelScope.launch(dispatchers.main) { delay(timeMillis = 1_800) @@ -143,181 +163,134 @@ internal class WalletViewModel @Inject constructor( .onEach(::updateWallets) .flowOn(dispatchers.io) .launchIn(viewModelScope) + + isBalanceHiddenUseCase() + .flowWithLifecycle(owner.lifecycle) + .onEach { hidden -> + isBalanceHidden = hidden + uiState = stateFactory.getHiddenBalanceState(isBalanceHidden = hidden) + } + .launchIn(viewModelScope) + + viewModelScope.launch { + listenToFlipsUseCase() + .flowWithLifecycle(owner.lifecycle) + .collect() + } } private fun updateWallets(sourceList: List) { - if (sourceList.isEmpty()) return - wallets = sourceList - val currentState = uiState - val selectedWalletIndex = if (currentState is WalletLockedState) { - currentState.getSelectedWalletIndex() + if (sourceList.isEmpty()) return + + when (val action = walletsUpdateActionResolver.resolve(sourceList)) { + is WalletsUpdateActionResolver.Action.Initialize -> { + initializeAndLoadState(selectedWalletIndex = action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.UpdateWalletName -> { + uiState = stateFactory.getStateWithUpdatedWalletName(name = action.name) + } + is WalletsUpdateActionResolver.Action.UnlockWallet -> { + uiState = stateFactory.getUnlockedState(action) + + getContentItemsUpdates(index = action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.DeleteWallet -> { + deleteWalletAndUpdateState(action = action) + } + is WalletsUpdateActionResolver.Action.AddWallet -> { + scrollAndUpdateState(action.selectedWalletIndex) + } + is WalletsUpdateActionResolver.Action.Unknown -> Unit + } + } + + private fun initializeAndLoadState(selectedWalletIndex: Int) { + uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = selectedWalletIndex) + + getContentItemsUpdates(index = selectedWalletIndex) + } + + private fun deleteWalletAndUpdateState(action: WalletsUpdateActionResolver.Action.DeleteWallet) { + val cacheState = WalletStateCache.getState(userWalletId = action.selectedWalletId) + if (cacheState != null) { + uiState = stateFactory.getStateWithoutDeletedWallet(cacheState, action) + + if (cacheState.isLoadingState()) { + uiState = stateFactory.getStateAndTriggerEvent( + state = uiState, + event = WalletEvent.ChangeWallet(action.selectedWalletIndex), + setUiState = { uiState = it }, + ) + getContentItemsUpdates(action.selectedWalletIndex) + } } else { - val selectedWallet = getSelectedWalletUseCase().fold( - ifLeft = { error("Selected wallet is null") }, - ifRight = { it }, - ) - sourceList.indexOfFirst { it.walletId == selectedWallet.walletId } - } - - uiState = stateFactory.getSkeletonState(wallets = sourceList, selectedWalletIndex = selectedWalletIndex) - - updateContentItems(index = selectedWalletIndex) - } - - private fun updateContentItems(index: Int, isRefreshing: Boolean = false) { - val cardTypeResolver = getCardTypeResolver(index) - when { - getWallet(index).isLocked -> uiState = stateFactory.getLockedState() - cardTypeResolver.isMultiwalletAllowed() -> updateMultiCurrencyContent(index, isRefreshing) - !cardTypeResolver.isMultiwalletAllowed() -> updateSingleCurrencyContent(index, isRefreshing) + /* It's impossible case because user can delete only visible state, but we support this case */ + scrollAndUpdateState(selectedWalletIndex = action.selectedWalletIndex) } } - private fun updateMultiCurrencyContent(index: Int, isRefreshing: Boolean = false) { - val state = requireNotNull(uiState as? WalletMultiCurrencyState) { - "Impossible to update tokens list if state isn't WalletMultiCurrencyState" - } - - getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[index].id) - .distinctUntilChanged() - .onEach { tokenListEither -> - uiState = stateFactory.getStateByTokensList( - tokenListEither = tokenListEither, - isRefreshing = isRefreshing, - ) - - updateNotifications( - index = index, - tokenList = tokenListEither.fold(ifLeft = { null }, ifRight = { it }), - ) - } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(tokensJobHolder) - } - - private fun updateSingleCurrencyContent(index: Int, isRefreshing: Boolean) { - val wallet = getWallet(index) - val blockchain = getCardTypeResolver(index).getBlockchain() - updateButtons(userWalletId = wallet.walletId, currencyId = blockchain.id) - updateTxHistory( - blockchain = blockchain, - derivationStyle = wallet.scanResponse.derivationStyleProvider.getDerivationStyle(), + private fun scrollAndUpdateState(selectedWalletIndex: Int) { + uiState = stateFactory.getSkeletonState( + wallets = wallets, + selectedWalletIndex = selectedWalletIndex, ) - updateMarketPrice(userWalletId = wallet.walletId, isRefreshing = isRefreshing) - updateNotifications(index) - } - private fun updateTxHistory(blockchain: Blockchain, derivationStyle: DerivationStyle?) { - viewModelScope.launch(dispatchers.io) { - val derivationPath = blockchain.derivationPath(style = derivationStyle)?.rawPath - - val txHistoryItemsCountEither = txHistoryItemsCountUseCase( - networkId = Network.ID(blockchain.id), - derivationPath = derivationPath, - ) - - uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) - - txHistoryItemsCountEither.onRight { - uiState = stateFactory.getLoadedTxHistoryState( - txHistoryEither = txHistoryItemsUseCase( - networkId = Network.ID(blockchain.id), - derivationPath = derivationPath, - ).map { - it.cachedIn(viewModelScope) - }, - ) - } - } - } - - // It also update wallet balance - private fun updateMarketPrice(userWalletId: UserWalletId, isRefreshing: Boolean) { - getPrimaryCurrencyUseCase(userWalletId = userWalletId) - .distinctUntilChanged() - .onEach { either -> - uiState = stateFactory.getSingleCurrencyLoadedBalanceState( - cryptoCurrencyEither = either, - isRefreshing = isRefreshing, - ) - - either.onRight { status -> cryptoCurrencyStatus = status } - } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(marketPriceJobHolder) - } - - private fun updateButtons(userWalletId: UserWalletId, currencyId: String) { - getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, tokenId = currencyId) - .distinctUntilChanged() - .onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actions = it.states) } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(buttonsJobHolder) - } - - private fun updateNotifications(index: Int, tokenList: TokenList? = null) { - notificationsListFactory.create( - cardTypesResolver = getCardTypeResolver(index = index), - tokenList = tokenList, + uiState = stateFactory.getStateAndTriggerEvent( + state = uiState, + event = WalletEvent.ChangeWallet(index = selectedWalletIndex), + setUiState = { uiState = it }, ) - .distinctUntilChanged() - .onEach { uiState = stateFactory.getStateByNotifications(notifications = it) } - .flowOn(dispatchers.io) - .launchIn(viewModelScope) - .saveIn(notificationsJobHolder) - } - override fun onStop(owner: LifecycleOwner) { - viewModelScope.launch(dispatchers.io) { - saveSelectedWallet() - } + getContentItemsUpdates(index = selectedWalletIndex) } - private suspend fun saveSelectedWallet() { - val state = uiState - if (state is WalletState.ContentState) { - selectWalletUseCase(getWallet(index = state.walletsListConfig.selectedWalletIndex).walletId) - } - } - - private fun getWallet(index: Int): UserWallet { - return requireNotNull( - value = wallets.getOrNull(index), - lazyMessage = { "WalletsList doesn't contain element with index = $index" }, - ) - } - - private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver - override fun onBackClick() { viewModelScope.launch(dispatchers.main) { router.popBackStack(screen = if (shouldSaveUserWalletsUseCase()) AppScreen.Welcome else AppScreen.Home) } } - override fun onScanCardClick() { + override fun onGenerateMissedAddressesClick() { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeScanYourCardTapped) + + scanToUpdateSelectedWallet( + onSuccessSave = { + // Reload currencies with missed derivation + fetchTokenListUseCase(userWalletId = it.walletId) + }, + ) + } + + override fun onScanToUnlockWalletClick() { + scanToUpdateSelectedWallet() + } + + private fun scanToUpdateSelectedWallet(onSuccessSave: suspend (UserWallet) -> Unit = {}) { + val state = uiState as? WalletState.ContentState ?: return + val prevRequestPolicyStatus = getBiometricsStatusUseCase() // Update access the code policy according access code saving status setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase()) viewModelScope.launch(dispatchers.io) { - scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) + scanCardProcessor.scan( + cardId = getWallet(state.walletsListConfig.selectedWalletIndex).cardId, + allowsRequestAccessCodeFromRepository = true, + ) .doOnSuccess { // If card's public key is null then user wallet will be null val userWallet = UserWalletBuilder(scanResponse = it).build() if (userWallet != null) { - saveWalletUseCase(userWallet) + saveWalletUseCase(userWallet = userWallet, canOverride = true) .onLeft { // Rollback policy if card saving was failed setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) } + .onRight { onSuccessSave(userWallet) } } else { // Rollback policy if card saving was failed setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) @@ -332,113 +305,98 @@ internal class WalletViewModel @Inject constructor( override fun onDetailsClick() = router.openDetailsScreen() - override fun onBackupCardClick() = router.openOnboardingScreen() - - override fun onCriticalWarningAlreadySignedHashesClick() { - uiState = stateFactory.getStateWithOpenWalletBottomSheet( - content = WalletBottomSheetConfig.BottomSheetContentConfig.CriticalWarningAlreadySignedHashes( - onOkClick = {}, - onCancelClick = {}, - ), - ) + override fun onBackupCardClick() { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeBackupYourWalletTapped) + router.openOnboardingScreen() } - override fun onCloseWarningAlreadySignedHashesClick() { + override fun onMultiWalletSignedHashesNotificationClick() { // TODO: [REDACTED_JIRA] } override fun onLikeTangemAppClick() { - uiState = stateFactory.getStateWithOpenWalletBottomSheet( - content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( - onRateTheAppClick = ::onRateTheAppClick, - onShareClick = ::onShareClick, - ), - ) + // TODO: [REDACTED_JIRA] } override fun onRateTheAppClick() { // TODO: [REDACTED_JIRA] } - override fun onShareClick() { - // TODO: [REDACTED_JIRA] - } - override fun onWalletChange(index: Int) { - val state = requireNotNull(uiState as? WalletState.ContentState) { - "Impossible to change wallet if state isn't WalletState.ContentState" - } + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.WalletSwipe) + val state = uiState as? WalletState.ContentState ?: return if (state.walletsListConfig.selectedWalletIndex == index) return - /* - * When wallet is changed it's necessary to stop the last jobs. - * If jobs aren't stopped and wallet is changed then it will update state for the prev wallet. - */ - tokensJobHolder.update(job = null) - marketPriceJobHolder.update(job = null) - buttonsJobHolder.update(job = null) - notificationsJobHolder.update(job = null) + // Reset the job to avoid a redundant state updating + onWalletChangeJobHolder.update(null) - val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id) - if (cacheState != null) { - uiState = if (cacheState is WalletState.ContentState) { - cacheState.copySealed( - walletsListConfig = state.walletsListConfig.copy(selectedWalletIndex = index), - pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = false), - ) - } else { - cacheState + viewModelScope.launch(dispatchers.main) { + withContext(dispatchers.io) { + selectWalletUseCase(userWalletId = state.walletsListConfig.wallets[index].id) } - if (cacheState.isLoadingState()) updateContentItems(index) - } else { - uiState = stateFactory.getSkeletonState(wallets = wallets, selectedWalletIndex = index) - updateContentItems(index = index) + val cacheState = WalletStateCache.getState(userWalletId = state.walletsListConfig.wallets[index].id) + if (cacheState != null && cacheState !is WalletLockedState) { + uiState = cacheState.copySealed( + walletsListConfig = state.walletsListConfig.copy( + selectedWalletIndex = index, + wallets = state.walletsListConfig.wallets + .mapIndexed { mapIndex, currentWallet -> + val cacheWallet = cacheState.walletsListConfig.wallets.getOrNull(mapIndex) + + if (currentWallet is WalletCardState.Loading && cacheWallet != null && + cacheWallet.isLoaded() + ) { + cacheWallet + } else { + currentWallet + } + } + .toImmutableList(), + ), + pullToRefreshConfig = cacheState.pullToRefreshConfig.copy(isRefreshing = false), + ) + + if (cacheState.isLoadingState()) { + getContentItemsUpdates(index) + } + } else { + initializeAndLoadState(selectedWalletIndex = index) + } } + .saveIn(onWalletChangeJobHolder) } - private fun WalletState.isLoadingState(): Boolean { - // Check the base components - if (this is WalletState.ContentState) { - walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading || - notifications.isEmpty() - } - - // Check the special components - return when (this) { - is WalletMultiCurrencyState -> { - val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState && - (tokensListState as WalletTokensListState.ContentState).items - .filterIsInstance() - .any { it.state is TokenItemState.Loading } - - tokensListState is WalletTokensListState.Loading || hasLoadingTokens - } - is WalletSingleCurrencyState -> { - this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading - } - is WalletState.Initial -> false - } + private fun WalletCardState.isLoaded(): Boolean { + return this !is WalletCardState.Loading && this !is WalletCardState.LockedContent } override fun onRefreshSwipe() { - if (uiState is WalletState.Initial || uiState is WalletLockedState) return + val selectedWalletIndex = (uiState as? WalletState.ContentState) + ?.walletsListConfig + ?.selectedWalletIndex + ?: return - viewModelScope.launch(dispatchers.io) { - uiState = stateFactory.getStateAfterContentRefreshing() - - // TODO: [REDACTED_JIRA] - delay(timeMillis = 500) - - updateContentItems( - index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, - isRefreshing = true, - ) + when (uiState) { + is WalletMultiCurrencyState.Content -> { + analyticsEventsHandler.send(PortfolioEvent.Refreshed) + refreshMultiCurrencyContent(selectedWalletIndex) + } + is WalletSingleCurrencyState.Content -> { + analyticsEventsHandler.send(PortfolioEvent.Refreshed) + refreshSingleCurrencyContent(selectedWalletIndex) + } + is WalletState.Initial, + is WalletMultiCurrencyState.Locked, + is WalletSingleCurrencyState.Locked, + -> Unit } } override fun onOrganizeTokensClick() { + analyticsEventsHandler.send(PortfolioEvent.OrganizeTokens) + val state = requireNotNull(uiState as? WalletState.ContentState) val index = state.walletsListConfig.selectedWalletIndex val walletId = state.walletsListConfig.wallets[index].id @@ -446,49 +404,117 @@ internal class WalletViewModel @Inject constructor( router.openOrganizeTokensScreen(walletId) } - override fun onBuyClick() { + override fun onBuyClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { val state = uiState as? WalletState.ContentState ?: return - val status = cryptoCurrencyStatus ?: return val wallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) reduxStateHolder.dispatch( TradeCryptoAction.New.Buy( userWallet = wallet, - cryptoCurrencyStatus = status, + cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = selectedAppCurrencyFlow.value.code, ), ) } - override fun onSendClick() { - reduxStateHolder.dispatch(TradeCryptoAction.New.Send) + override fun onSwapClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // todo implement onSwapClick [REDACTED_JIRA] } - override fun onReceiveClick() { - // TODO: [REDACTED_JIRA] - } + override fun onSingleCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + val state = uiState as? WalletState.ContentState ?: return - override fun onSellClick() { - val status = cryptoCurrencyStatus ?: return + val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + val coinStatus = if (userWallet.isMultiCurrency) cryptoCurrencyStatus else singleWalletCryptoCurrencyStatus + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendCoin( + userWallet = userWallet, + coinStatus = coinStatus ?: return, + ), + ) + } + + override fun onMultiCurrencySendClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + if (cryptoCurrencyStatus.currency is CryptoCurrency.Coin) { + onSingleCurrencySendClick(cryptoCurrencyStatus = cryptoCurrencyStatus) + return + } + + val state = uiState as? WalletState.ContentState ?: return + + viewModelScope.launch(dispatchers.io) { + val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + + getNetworkCoinStatusUseCase( + userWalletId = userWallet.walletId, + networkId = cryptoCurrencyStatus.currency.network.id, + ) + .take(count = 1) + .collectLatest { + it.onRight { coinStatus -> + reduxStateHolder.dispatch( + action = TradeCryptoAction.New.SendToken( + userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex), + tokenStatus = cryptoCurrencyStatus, + coinFiatRate = coinStatus.value.fiatRate, + ), + ) + } + } + } + } + + override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val state = uiState as? WalletState.ContentState ?: return + + viewModelScope.launch(dispatchers.io) { + val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex) + + val addresses = walletManagersFacade.getAddress( + userWalletId = userWallet.walletId, + network = cryptoCurrencyStatus.currency.network, + ) + + val currency = cryptoCurrencyStatus.currency + uiState = stateFactory.getStateWithOpenWalletBottomSheet( + content = TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = addresses.map { + AddressModel( + value = it.value, + type = AddressModel.Type.valueOf(it.type.name), + ) + }, + ), + ) + } + } + + override fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { reduxStateHolder.dispatch( TradeCryptoAction.New.Sell( - cryptoCurrencyStatus = status, + cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrencyCode = selectedAppCurrencyFlow.value.code, ), ) } override fun onManageTokensClick() { + analyticsEventsHandler.send(PortfolioEvent.ButtonManageTokens) + reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) router.openManageTokensScreen() } override fun onReloadClick() { - uiState = stateFactory.getStateAfterContentRefreshing() - updateSingleCurrencyContent( - index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, - isRefreshing = true, - ) + val selectedWalletIndex = (uiState as? WalletSingleCurrencyState) + ?.walletsListConfig + ?.selectedWalletIndex + ?: return + + refreshSingleCurrencyContent(selectedWalletIndex) } override fun onExploreClick() { @@ -496,18 +522,24 @@ internal class WalletViewModel @Inject constructor( val wallet = getWallet( index = requireNotNull(uiState as? WalletState.ContentState).walletsListConfig.selectedWalletIndex, ) - router.openTxHistoryWebsite( - url = getExploreUrlUseCase( - userWalletId = wallet.walletId, - networkId = Network.ID( - value = wallet.scanResponse.cardTypesResolver.getBlockchain().id, + val currencyStatus = getPrimaryCurrencyStatusUpdatesUseCase(wallet.walletId) + .firstOrNull() + ?.getOrNull() + + if (currencyStatus != null) { + router.openTxHistoryWebsite( + url = getExploreUrlUseCase( + userWalletId = wallet.walletId, + network = currencyStatus.currency.network, ), - ), - ) + ) + } } } override fun onUnlockWalletClick() { + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.NoticeWalletLocked) + viewModelScope.launch(dispatchers.io) { unlockWalletsUseCase() } @@ -527,13 +559,21 @@ internal class WalletViewModel @Inject constructor( } override fun onTokenItemClick(currency: CryptoCurrency) { + analyticsEventsHandler.send(PortfolioEvent.TokenTapped) router.openTokenDetails(currency = currency) } - override fun onTokenItemLongClick(currency: CryptoCurrency) { - uiState = stateFactory.getStateWithTokenActionBottomSheet( - tokenId = currency.id.value, - ) + override fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val state = uiState as? WalletState.ContentState ?: return + val userWallet = getWallet(state.walletsListConfig.selectedWalletIndex) + viewModelScope.launch(dispatchers.io) { + getCryptoCurrencyActionsUseCase + .invoke(userWallet.walletId, cryptoCurrencyStatus) + .take(count = 1) + .collectLatest { + uiState = stateFactory.getStateWithTokenActionBottomSheet(it) + } + } } override fun onRenameClick(userWalletId: UserWalletId, name: String) { @@ -543,26 +583,15 @@ internal class WalletViewModel @Inject constructor( } override fun onDeleteClick(userWalletId: UserWalletId) { + val state = uiState as? WalletState.ContentState ?: return + viewModelScope.launch(dispatchers.io) { val either = deleteWalletUseCase(userWalletId) - val state = requireNotNull(uiState as? WalletState.ContentState) if (state.walletsListConfig.wallets.size <= 1 && either.isRight()) onBackClick() } } - private fun createSelectedAppCurrencyFlow(): StateFlow { - return getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - } - override fun onDismissBottomSheet() { uiState = stateFactory.getStateWithClosedBottomSheet() } @@ -576,4 +605,192 @@ internal class WalletViewModel @Inject constructor( ) } } + + private fun getContentItemsUpdates(index: Int) { + /* + * When wallet is changed it's necessary to stop the last jobs. + * If jobs aren't stopped and wallet is changed then it will update state for the prev wallet. + */ + tokensJobHolder.update(job = null) + marketPriceJobHolder.update(job = null) + buttonsJobHolder.update(job = null) + notificationsJobHolder.update(job = null) + refreshContentJobHolder.update(job = null) + + val wallet = getWallet(index) + + when { + wallet.isLocked -> { + uiState = stateFactory.getLockedState() + } + wallet.isMultiCurrency -> getMultiCurrencyContent(index) + !wallet.isMultiCurrency -> getSingleCurrencyContent(index) + } + } + + private fun getMultiCurrencyContent(walletIndex: Int) { + val state = requireNotNull(uiState as? WalletMultiCurrencyState) { + "Impossible to get a token list updates if state isn't WalletMultiCurrencyState" + } + + getTokenListUseCase(userWalletId = state.walletsListConfig.wallets[walletIndex].id) + .distinctUntilChanged() + .onEach { maybeTokenList -> + uiState = stateFactory.getStateByTokensList(maybeTokenList) + + updateNotifications( + index = walletIndex, + tokenList = maybeTokenList.fold(ifLeft = { null }, ifRight = { it }), + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(tokensJobHolder) + } + + private fun getSingleCurrencyContent(index: Int) { + val wallet = getWallet(index) + updatePrimaryCurrencyStatus(userWalletId = wallet.walletId) + updateNotifications(index) + } + + private fun updateTxHistory(network: Network) { + viewModelScope.launch(dispatchers.io) { + val txHistoryItemsCountEither = txHistoryItemsCountUseCase(network) + + uiState = stateFactory.getLoadingTxHistoryState( + itemsCountEither = txHistoryItemsCountEither, + ) + + txHistoryItemsCountEither.onRight { + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase( + network, + ).map { + it.cachedIn(viewModelScope) + }, + ) + } + } + } + + private fun updatePrimaryCurrencyStatus(userWalletId: UserWalletId) { + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId) + .distinctUntilChanged() + .onEach { maybeCryptoCurrencyStatus -> + uiState = stateFactory.getSingleCurrencyLoadedBalanceState(maybeCryptoCurrencyStatus) + + maybeCryptoCurrencyStatus.onRight { status -> + singleWalletCryptoCurrencyStatus = status + updateButtons(userWalletId = userWalletId, currencyStatus = status) + updateTxHistory(status.currency.network) + } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(marketPriceJobHolder) + } + + private fun updateButtons(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { + getCryptoCurrencyActionsUseCase(userWalletId = userWalletId, cryptoCurrencyStatus = currencyStatus) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getSingleCurrencyManageButtonsState(actionsState = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(buttonsJobHolder) + } + + private fun updateNotifications(index: Int, tokenList: TokenList? = null) { + notificationsListFactory.create( + cardTypesResolver = getCardTypeResolver(index = index), + cryptoCurrencyList = if (tokenList != null) { + when (tokenList) { + is TokenList.GroupedByNetwork -> { + tokenList.groups + .flatMap(NetworkGroup::currencies) + .map(CryptoCurrencyStatus::value) + } + is TokenList.Ungrouped -> tokenList.currencies.map(CryptoCurrencyStatus::value) + is TokenList.NotInitialized -> emptyList() + } + } else { + listOfNotNull(singleWalletCryptoCurrencyStatus?.value) + }, + ) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getStateByNotifications(notifications = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(notificationsJobHolder) + } + + private fun refreshMultiCurrencyContent(walletIndex: Int) { + uiState = stateFactory.getRefreshingState() + val wallet = getWallet(walletIndex) + + viewModelScope.launch(dispatchers.io) { + val result = fetchTokenListUseCase(wallet.walletId, refresh = true) + + uiState = stateFactory.getRefreshedState() + uiState = result.fold(stateFactory::getStateByTokenListError) { uiState } + }.saveIn(refreshContentJobHolder) + } + + private fun refreshSingleCurrencyContent(walletIndex: Int) { + uiState = stateFactory.getRefreshingState() + val wallet = getWallet(walletIndex) + + viewModelScope.launch(dispatchers.io) { + val result = fetchCurrencyStatusUseCase(wallet.walletId, refresh = true) + + uiState = stateFactory.getRefreshedState() + uiState = result.fold(stateFactory::getStateByCurrencyStatusError) { uiState } + }.saveIn(refreshContentJobHolder) + } + + private fun createSelectedAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + } + + private fun WalletState.isLoadingState(): Boolean { + // Check the base components + if (this is WalletState.ContentState && + walletsListConfig.wallets[walletsListConfig.selectedWalletIndex] is WalletCardState.Loading + ) { + return true + } + + // Check the special components + return when (this) { + is WalletMultiCurrencyState -> { + val hasLoadingTokens = tokensListState is WalletTokensListState.ContentState && + (tokensListState as WalletTokensListState.ContentState).items + .filterIsInstance() + .any { it.state is TokenItemState.Loading } + + tokensListState is WalletTokensListState.Loading || hasLoadingTokens + } + is WalletSingleCurrencyState -> { + this is WalletSingleCurrencyState.Content && marketPriceBlockState is MarketPriceBlockState.Loading + } + is WalletState.Initial -> false + } + } + + private fun getWallet(index: Int): UserWallet { + return requireNotNull( + value = wallets.getOrNull(index), + lazyMessage = { "WalletsList doesn't contain element with index = $index" }, + ) + } + + private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt new file mode 100644 index 0000000000..34c5eea823 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -0,0 +1,161 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.common.Provider +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.state.WalletLockedState +import com.tangem.feature.wallet.presentation.wallet.state.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.components.WalletCardState + +/** + * Resolver that determines which update action will be performed + * + * @property currentStateProvider current state provider + * @property getSelectedWalletUseCase use case that returns selected wallet + */ +internal class WalletsUpdateActionResolver( + private val currentStateProvider: Provider, + private val getSelectedWalletUseCase: GetSelectedWalletUseCase, +) { + + fun resolve(wallets: List): Action { + val selectedWallet = wallets.getSelectedWallet() + + return when (val state = currentStateProvider()) { + is WalletState.Initial -> { + Action.Initialize( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + ) + } + is WalletState.ContentState -> { + getActionToUpdateContent(state = state, wallets = wallets, selectedWallet = selectedWallet) + } + } + } + + private fun List.getSelectedWallet(): UserWallet { + val hasUnlockedWallet = any { !it.isLocked } + return if (hasUnlockedWallet) { + val selectedWalletId = getSelectedWalletUseCase().fold(ifLeft = ::error, ifRight = UserWallet::walletId) + + firstOrNull { it.walletId == selectedWalletId } + ?: error("Wallets don't contain a wallet with id: $selectedWalletId") + } else { + lastOrNull() ?: error("Wallets is empty") + } + } + + private fun getActionToUpdateContent( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + return if (isWalletsCountChanged(state, wallets)) { + getActionToChangeWallets(state = state, wallets = wallets, selectedWallet = selectedWallet) + } else { + getActionToUpdateCurrentWallet(state = state, wallets = wallets, selectedWallet = selectedWallet) + } + } + + private fun isWalletsCountChanged(state: WalletState.ContentState, wallets: List): Boolean { + val prevWalletsSize = state.walletsListConfig.wallets.size + val walletsSize = wallets.size + + return prevWalletsSize != walletsSize + } + + private fun getActionToChangeWallets( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val prevWalletsSize = state.walletsListConfig.wallets.size + + return when { + prevWalletsSize > wallets.size -> { + Action.DeleteWallet( + selectedWalletId = selectedWallet.walletId, + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + deletedWalletId = state.walletsListConfig.wallets.getDeletedWalletId(wallets), + ) + } + prevWalletsSize < wallets.size -> { + Action.AddWallet( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + ) + } + else -> Action.Unknown + } + } + + private fun List.getDeletedWalletId(wallets: List): UserWalletId { + return this + .map(WalletCardState::id) + .firstOrNull { !wallets.map(UserWallet::walletId).contains(it) } + ?: error("Deleted wallet id is not found. Wallets contains all previous wallets ids") + } + + private fun getActionToUpdateCurrentWallet( + state: WalletState.ContentState, + wallets: List, + selectedWallet: UserWallet, + ): Action { + val selectedWalletName = selectedWallet.name + + if (state.getPrevSelectedWalletName() != selectedWalletName) { + return Action.UpdateWalletName(selectedWalletName) + } + + if (state is WalletLockedState && !selectedWallet.isLocked) { + return Action.UnlockWallet( + selectedWalletIndex = wallets.indexOfWallet(id = selectedWallet.walletId), + selectedWallet = selectedWallet, + unlockedWallets = wallets.filterNot(UserWallet::isLocked), + ) + } + + return Action.Unknown + } + + private fun WalletState.ContentState.getPrevSelectedWalletName(): String { + val prevSelectedWalletIndex = walletsListConfig.selectedWalletIndex + val prevSelectedWallet = walletsListConfig.wallets.getOrNull(prevSelectedWalletIndex) + ?: error("Previous selected wallet is not found") + + return prevSelectedWallet.title + } + + private fun List.indexOfWallet(id: UserWalletId): Int { + val selectedIndex = indexOfFirst { it.walletId == id } + + return if (selectedIndex == -1) { + error("Wallets don't contain a wallet with id: $id") + } else { + selectedIndex + } + } + + sealed class Action { + + data class Initialize(val selectedWalletIndex: Int) : Action() + + data class UpdateWalletName(val name: String) : Action() + + data class UnlockWallet( + val selectedWalletIndex: Int, + val selectedWallet: UserWallet, + val unlockedWallets: List, + ) : Action() + + data class DeleteWallet( + val selectedWalletId: UserWalletId, + val selectedWalletIndex: Int, + val deletedWalletId: UserWalletId, + ) : Action() + + data class AddWallet(val selectedWalletIndex: Int) : Action() + + object Unknown : Action() + } +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index d4c76f2594..7b54e17033 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -139,6 +139,7 @@ lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", v # region Compose compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-runtime" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-runtime" } +compose-ui-utils = { module = "androidx.compose.ui:ui-util", version.ref = "compose-runtime" } compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose-runtime" } compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" } diff --git a/settings.gradle.kts b/settings.gradle.kts index b2c3126321..00b22db5d1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -112,11 +112,15 @@ include(":domain:app-currency") include(":domain:app-currency:models") include(":domain:app-theme") include(":domain:app-theme:models") +include(":domain:balance-hiding") +include(":domain:balance-hiding:models") + // endregion Domain modules // region Data modules include(":data:app-currency") include(":data:app-theme") +include(":data:balance-hiding") include(":data:common") include(":data:card") include(":data:tokens")