Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-21 11:08:25 +03:00
commit f0a85e8b5b
508 changed files with 12810 additions and 8013 deletions

View file

@ -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)

View file

@ -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
}

View file

@ -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)

View file

@ -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<String> = 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")
}
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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<String>,
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<String>,
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,
) {}
}
}

View file

@ -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,
)
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
typealias ValueCallback<T> = (T) -> Unit

View file

@ -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()
}

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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,
)
}
}

View file

@ -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)
}
}

View file

@ -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<AppState>.dispatchWalletAction(action: WalletAction) {
val walletFeatureToggles = state.daggerGraphState.get(DaggerGraphState::walletFeatureToggles)
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
dispatch(action = action)
}
}
private fun setupWalletConnectV2(userWallet: UserWallet) {

View file

@ -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<CurrenciesFromJson> =
MoshiConverter.networkMoshi.adapter(CurrenciesFromJson::class.java)
suspend fun getSupportedTokens(
isTestNet: Boolean,
supportedBlockchains: List<Blockchain>,
page: Int,
searchInput: String?,
): Result<LoadedCoins> {
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<Blockchain>,
offset: Int,
searchInput: String?,
): Result<CoinsResponse> {
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<Currency> {
val json = assetReader.readJson(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}
private fun List<Currency>.filter(searchInput: String?): List<Currency> {
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<Currency>,
val moreAvailable: Boolean,
)

View file

@ -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
}

View file

@ -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")
}

View file

@ -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<AddCustomTokenState> {
private var state: MutableState<AddCustomTokenState> = 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<Toolbar>(R.id.toolbar)?.setTitle(R.string.add_custom_token_title)
val closePopupTrigger = initClosingPopupTriggerEvent()
view.findViewById<ComposeView>(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()
},
)
}
}

View file

@ -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<AddCustomTokenState>, 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<AddCustomTokenState>, 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<AddCustomTokenState>, 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<AddCustomTokenError.Warning>) {
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<AddCustomTokenState>) {
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()
}
}
}
}

View file

@ -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<Blockchain>,
selectedItem: Field.Data<Blockchain>,
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,
)
}

View file

@ -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<DomainDialog?>(null) }
val subscriber = remember {
object : StoreSubscriber<DomainGlobalState> {
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<DomainDialog?>) {
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<CoinsResponse.Coin.Network>,
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))
}
},
)
}

View file

@ -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()
}

View file

@ -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<Keyboard>,
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() }
}

View file

@ -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 <T> OutlinedSpinner(
label: String,
itemList: List<T>,
selectedItem: Field.Data<T>,
onItemSelected: ValueCallback<T>,
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 = {},
)
}

View file

@ -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 ?: "") }
}

View file

@ -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<Pair<String, String>>, 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) }
}

View file

@ -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) }),
;
}

View file

@ -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")
}

View file

@ -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")
}

View file

@ -0,0 +1,6 @@
package com.tangem.tap.features.details.featuretoggles
interface DetailsFeatureToggles {
val isRedesignedAppCurrencySelectorEnabled: Boolean
}

View file

@ -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)
}
}

View file

@ -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<Wallet>,
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()

View file

@ -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<AppState> = { _, 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<Unit> {
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)
}
}

View file

@ -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

View file

@ -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<Wallet> = 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 }

View file

@ -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,
)
}
}

View file

@ -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()
}

View file

@ -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<Currency>,
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<AppCurrencySelectorState>(
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

View file

@ -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<Int>
abstract val items: PersistentList<Currency>
abstract val onCurrencyClick: (Currency) -> Unit
abstract val onTopBarActionClick: () -> Unit
fun copySealed(
selectedId: String? = this.selectedId,
items: PersistentList<Currency> = this.items,
scrollToSelected: StateEvent<Int> = 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<Currency>,
override val onCurrencyClick: (Currency) -> Unit,
override val onBackClick: () -> Unit,
override val onTopBarActionClick: () -> Unit,
override val scrollToSelected: StateEvent<Int>,
) : Content()
data class Search(
override val selectedId: String,
override val items: PersistentList<Currency>,
override val scrollToSelected: StateEvent<Int>,
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,
)
}

View file

@ -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<AppCurrencySelectorState.Currency> = persistentListOf()
private val stateFlowInternal: MutableStateFlow<AppCurrencySelectorState> = MutableStateFlow(getInitialState())
private val currencyConverter by lazy(mode = LazyThreadSafetyMode.NONE) { CurrencyConverter() }
val stateFlow: StateFlow<AppCurrencySelectorState> = stateFlowInternal
.onSubscription { onSubscription() }
.stateIn(
scope = stateFlowScope,
started = SharingStarted.WhileSubscribed(),
initialValue = getInitialState(),
)
fun updateStateWithAvailableCurrencies(currencies: List<AppCurrency>) {
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<AppCurrencySelectorState.Currency>,
selectedCurrencyId: String = "",
) = AppCurrencySelectorState.Default(
selectedId = selectedCurrencyId,
items = items,
onBackClick = intents::onBackClick,
onCurrencyClick = intents::onCurrencyClick,
onTopBarActionClick = intents::onSearchClick,
scrollToSelected = consumedEvent(),
)
private fun getSearchState(
filteredItems: PersistentList<AppCurrencySelectorState.Currency>,
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())
}
}
}
}

View file

@ -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<AppCurrencySelectorState> = 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)
}
}
}
}
}
}

View file

@ -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<AppCurrency, AppCurrencySelectorState.Currency> {
override fun convert(value: AppCurrency): AppCurrencySelectorState.Currency {
val fullCurrencyName = with(value) { "$name ($code) - $symbol" }
return AppCurrencySelectorState.Currency(value.code, fullCurrencyName)
}
}

View file

@ -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,
)
}
}

View file

@ -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<DetailsState> {
private val viewModel = AppSettingsViewModel(store)
private var screenState: MutableState<AppSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState))
@AndroidEntryPoint
internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
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)
}
}
override fun newState(state: DetailsState) {
if (activity == null || view == null) return
viewModel.updateState(state)
}
}

View file

@ -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,
)
}
}

View file

@ -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<AppSetting?>(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<AppSettingsScreenState>(
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

View file

@ -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<AppSetting, Boolean> = emptyMap(),
val showEnrollBiometricsCard: Boolean = false,
val isTogglesEnabled: Boolean = false,
val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ },
val onEnrollBiometrics: () -> Unit = { /* no-op */ },
)
@Immutable
internal sealed class AppSettingsScreenState {
object Loading : AppSettingsScreenState()
data class Content(
val items: ImmutableList<Item>,
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<TextReference>,
val onSelect: (Int) -> Unit,
override val onDismiss: () -> Unit,
) : Dialog()
}
}

View file

@ -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<AppState>) {
internal class AppSettingsViewModel(
private val store: Store<AppState>,
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<AppState>) {
),
)
}
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
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
}
}
}

View file

@ -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<Dialog.Alert>(
collection = buildList {
val dialogsFactory = AppSettingsDialogsFactory()
dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add)
dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add)
},
)
// endregion Preview

View file

@ -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<Item.Button>(
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
itemsFactory.createSelectAppCurrencyButton(
currentAppCurrencyName = "US Dollar",
onClick = { /* no-op */ },
).let(::add)
},
)
// endregion Preview

View file

@ -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<Item.Card>(
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
itemsFactory.createEnrollBiometricsCard(
onClick = { /* no-op */ },
).let(::add)
},
)
// endregion Preview

View file

@ -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<Dialog.Selector>(
collection = listOf(
AppSettingsDialogsFactory().createThemeModeSelectorDialog(
selectedModeIndex = 0,
onSelect = {},
onDismiss = {},
),
),
)
// endregion Preview

View file

@ -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<Item.Switch>(
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

View file

@ -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<DetailsState> {
@AndroidEntryPoint
internal class CardSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel = CardSettingsViewModel(store)
private var screenState: MutableState<CardSettingsScreenState> =
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() {

View file

@ -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 = {}) {}, {})
}
}
@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

View file

@ -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<CardInfo>? = 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<Any> = 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

View file

@ -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<AppState>) {
internal class CardSettingsViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): CardSettingsScreenState {
return if (state?.manageSecurityState == null) {

View file

@ -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,
)
}
}

View file

@ -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,

View file

@ -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<DetailsState> {
@AndroidEntryPoint
internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
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() {

View file

@ -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<SettingsItem>) {
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<SocialNetworkLink>, 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<SocialNetworkLink>, 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<TextReference>) {
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<DetailsScreenState>(
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 = {},
)
}
).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

View file

@ -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<SettingsElement>,
val tangemLinks: List<SocialNetworkLink>,
internal data class DetailsScreenState(
val elements: ImmutableList<SettingsItem>,
val tangemLinks: ImmutableList<SocialNetworkLink>,
val tangemVersion: String,
val appCurrency: String,
val onItemsClick: (SettingsElement) -> Unit,
val showSnackbar: StateEvent<TextReference>,
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
val showErrorSnackbar: MutableState<EventError> = 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<SocialNetworkLink> = listOf(
internal object TangemSocialAccounts {
val accountsEn: ImmutableList<SocialNetworkLink> = 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<SocialNetworkLink> = listOf(
val accountsRu: ImmutableList<SocialNetworkLink> = 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"),
)
}

View file

@ -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<AppState>) {
internal class DetailsViewModel(
private val store: Store<AppState>,
private val darkThemeFeatureToggle: DarkThemeFeatureToggle,
) { // TODO: change to Android ViewModel
var detailsScreenState: MutableState<DetailsScreenState> = 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<SettingsItem> {
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<SocialNetworkLink> {
val locale = LocaleRegionProvider().getRegion()
return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) {
TangemSocialAccounts.accountsRu
private fun triggerErrorSnackbarIfNeeded(text: TextReference?): StateEvent<TextReference> {
return if (text == null) {
consumedEvent()
} else {
TangemSocialAccounts.accountsEn
triggeredEvent(text) {
store.dispatch(DetailsAction.DismissError)
}
}
}
@ -118,4 +119,79 @@ class DetailsViewModel(private val store: Store<AppState>) {
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<SocialNetworkLink> {
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)
}
}

View file

@ -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<DetailsState> {
@AndroidEntryPoint
internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel = ResetCardViewModel(store)
private var screenState: MutableState<ResetCardScreenState> =
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() {

View file

@ -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,

View file

@ -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,

View file

@ -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<AppState>) {
internal class ResetCardViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): ResetCardScreenState {
val descriptionText = state?.cardInfo

View file

@ -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<DetailsState> {
@AndroidEntryPoint
internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel = SecurityModeViewModel(store)
private var screenState: MutableState<SecurityModeScreenState> =
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() {

View file

@ -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()

View file

@ -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<SecurityOption>,
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

View file

@ -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<AppState>) {
internal class SecurityModeViewModel(val store: Store<AppState>) {
fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
if (state == null) {

View file

@ -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<WalletConnectState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
private val viewModel = WalletConnectViewModel(store)
private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(viewModel.updateState(store.state.walletConnectState))
@ -26,32 +32,31 @@ class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
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() {

View file

@ -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,
)
}
}

View file

@ -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<WcSessionForScreen>,
val isLoading: Boolean = false,
val onRemoveSession: (String) -> Unit = {},

View file

@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.Store
import timber.log.Timber
class WalletConnectViewModel(private val store: Store<AppState>) {
internal class WalletConnectViewModel(private val store: Store<AppState>) {
fun updateState(state: WalletConnectState): WalletConnectScreenState {
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions

View file

@ -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())
}
}

View file

@ -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

View file

@ -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(),

View file

@ -105,7 +105,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
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<TwinCardsState>() {
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)
}

View file

@ -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(),

View file

@ -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() {

View file

@ -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))
}

View file

@ -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<SaveWalletState> {
@HiltViewModel
internal class SaveWalletViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), StoreSubscriber<SaveWalletState> {
private val stateInternal = MutableStateFlow(SaveWalletScreenState())
val state: StateFlow<SaveWalletScreenState> = stateInternal
@ -22,10 +30,12 @@ internal class SaveWalletViewModel : ViewModel(), StoreSubscriber<SaveWalletStat
}
fun saveWallet() {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))
store.dispatch(SaveWalletAction.Save)
}
fun cancelOrClose() {
analyticsEventHandler.send(WalletScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.Off))
store.dispatch(SaveWalletAction.Dismiss)
}

View file

@ -102,6 +102,7 @@ sealed class AmountAction : SendScreenAction {
data class SetAmount(val amountCrypto: BigDecimal, val isUserInput: Boolean) : AmountAction()
data class SetAmountError(val error: TapError?) : AmountAction()
data class SetDecimalSeparator(val separator: String) : AmountAction()
data class HideBalance(val hide: Boolean) : AmountAction()
}
// Fee

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.common.Strings.STARS
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.AmountAction
@ -38,8 +39,8 @@ class AmountReducer : SendInternalReducer {
val viewValue = state.restoreDecimalSeparator(fiatToSend.stripZeroPlainString())
state.copy(
viewAmountValue = InputViewValue(viewValue),
viewBalanceValue = rescaledBalance.stripZeroPlainString(),
mainCurrency = state.createMainCurrency(currency, currencyCanBeSwitched),
viewBalanceValue = if (state.hideBalance) STARS else rescaledBalance.stripZeroPlainString(),
mainCurrency = state.createMainCurrency(currency, true),
maxLengthOfAmount = sendState.getDecimals(currency),
cursorAtTheSamePosition = false,
)
@ -80,6 +81,14 @@ class AmountReducer : SendInternalReducer {
}
is AmountAction.SetAmountError -> 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)
}

View file

@ -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 = ".",

View file

@ -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<SendViewModel>()
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()
}
}

View file

@ -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()
}
}
}

View file

@ -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))
}

View file

@ -35,7 +35,6 @@ internal object TokensListInteractorModule {
reduxStateHolder = reduxStateHolder,
testnetTokensStorage = testnetTokensStorage,
),
reduxStateHolder = reduxStateHolder,
)
}
}

View file

@ -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()
}

View file

@ -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<PagingData<Token>> {
return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null }))
}
override suspend fun saveChanges(tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain>) {
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<WalletDataModel>.toNonCustomTokensWithBlockchains(
derivationStyle: DerivationStyle?,
): List<TokenWithBlockchain> {
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<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
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<TokenWithBlockchain>,
blockchains: List<Blockchain>,
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<TokenWithBlockchain>,
blockchains: List<Blockchain>,
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<Currency>) {
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<ByteArrayKey, MutableList<DerivationPath>> {
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<Currency>) {
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<TokenWithBlockchain>,
blockchains: List<Blockchain>,
derivationStyle: DerivationStyle?,
): List<Currency> {
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,
)
},
)
}
}

View file

@ -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<PagingData<Token>>
/**
* Save added tokens
*
* @param tokens tokens list that need to save
* @param blockchains blockchains list that need to save
*/
suspend fun saveChanges(tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain>)
}

View file

@ -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<TokenWithBlockchain> get() = store.state.tokensState.addedTokens
/** Blockchains list that accessible from the main screen */
val mainScreenBlockchainList: List<Blockchain> get() = store.state.tokensState.addedBlockchains
}

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