Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-11 11:07:03 +03:00
commit f0d03e3648
286 changed files with 6353 additions and 5898 deletions

View file

@ -0,0 +1,56 @@
package com.tangem.tap
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.SystemClock
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
@ExperimentalCoroutinesApi
class DeviceFlipDetector(context: Context) {
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
private var gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
private val zAxisThreshold = -6
private val throttleTimeMs = 3000
private var lastTriggerTime = 0L
private var isScreenDown = false
fun deviceFlipEvents(): Flow<Unit> = callbackFlow {
val listener = object : SensorEventListener {
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
/* no-op */
}
override fun onSensorChanged(event: SensorEvent?) {
event?.let {
val currentTime = SystemClock.elapsedRealtime()
val zAxisValue = it.values[2]
if (zAxisValue < zAxisThreshold && !isScreenDown) {
isScreenDown = true
lastTriggerTime = currentTime
} else if (zAxisValue >= zAxisThreshold) {
if (isScreenDown && currentTime - lastTriggerTime <= throttleTimeMs) {
lastTriggerTime = currentTime
trySend(Unit)
}
isScreenDown = false
}
}
}
}
gravitySensor?.let {
sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_NORMAL)
}
awaitClose { sensorManager.unregisterListener(listener) }
}
}

View file

@ -22,10 +22,11 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config import com.tangem.datasource.config.models.Config
import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.DomainLayer
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.LogConfig import com.tangem.domain.common.LogConfig
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.domain.wallets.legacy.WalletManagersRepository
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
@ -169,6 +170,12 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject @Inject
lateinit var walletManagersFacade: WalletManagersFacade lateinit var walletManagersFacade: WalletManagersFacade
@Inject
lateinit var currenciesRepository: CurrenciesRepository
@Inject
lateinit var appThemeModeRepository: AppThemeModeRepository
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -189,6 +196,9 @@ class TapApplication : Application(), ImageLoaderFactory {
scanCardProcessor = scanCardProcessor, scanCardProcessor = scanCardProcessor,
appCurrencyRepository = appCurrencyRepository, appCurrencyRepository = appCurrencyRepository,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
appStateHolder = appStateHolder,
currenciesRepository = currenciesRepository,
appThemeModeRepository = appThemeModeRepository,
), ),
), ),
) )
@ -208,7 +218,6 @@ class TapApplication : Application(), ImageLoaderFactory {
activityResultCaller = foregroundActivityObserver activityResultCaller = foregroundActivityObserver
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
DomainLayer.init()
preferencesStorage = preferencesDataSource preferencesStorage = preferencesDataSource
walletConnectRepository = WalletConnectRepository(this) 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

@ -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,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.FragmentShareTransition import com.tangem.core.navigation.FragmentShareTransition
import com.tangem.feature.referral.ReferralFragment import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment 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.appsettings.AppSettingsFragment 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.CardSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
@ -33,7 +33,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R import com.tangem.wallet.R
import timber.log.Timber import timber.log.Timber
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment as RedesignedAddCustomTokenFragment
fun FragmentActivity.openFragment( fun FragmentActivity.openFragment(
screen: AppScreen, screen: AppScreen,
@ -155,18 +154,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment() AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
AppScreen.Disclaimer -> DisclaimerFragment() AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> TokensListFragment() AppScreen.AddTokens -> TokensListFragment()
AppScreen.AddCustomToken -> AddCustomTokenFragment()
AppScreen.AddCustomToken -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::customTokenFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) {
RedesignedAddCustomTokenFragment()
} else {
AddCustomTokenFragment()
}
}
AppScreen.WalletDetails -> { AppScreen.WalletDetails -> {
val featureToggles = store.state.daggerGraphState.get( val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::tokenDetailsFeatureToggles, getDependency = DaggerGraphState::tokenDetailsFeatureToggles,

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.common.core.TangemError
import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig 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.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.analytics.topup.TopUpController 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 UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
} }

View file

@ -131,6 +131,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
sellService = makeSellExchangeService(config), sellService = makeSellExchangeService(config),
primaryRules = CardExchangeRules(cardProvider), 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.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update) store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
} }

View file

@ -97,6 +97,9 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
userWalletsListManager = action.manager, userWalletsListManager = action.manager,
) )
} }
is GlobalAction.ChangeAppThemeMode -> globalState.copy(
appThemeMode = action.appThemeMode,
)
else -> globalState else -> globalState
} }
} }

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.redux.global package com.tangem.tap.common.redux.global
import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.analytics.topup.TopUpController import com.tangem.tap.common.analytics.topup.TopUpController
@ -29,6 +30,7 @@ data class GlobalState(
val userCountryCode: String? = null, val userCountryCode: String? = null,
val userWalletsListManager: UserWalletsListManager? = null, val userWalletsListManager: UserWalletsListManager? = null,
val topUpController: TopUpController? = null, val topUpController: TopUpController? = null,
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
) : StateType ) : StateType
typealias CryptoCurrencyName = String typealias CryptoCurrencyName = String

View file

@ -3,8 +3,11 @@ package com.tangem.tap.di
import android.content.Context import android.content.Context
import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository 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 com.tangem.tap.userTokensRepository
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
@ -40,4 +43,10 @@ internal object ActivityModule {
), ),
) )
} }
@Provides
@Singleton
fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager {
return DefaultRampManager(appStateHolder.exchangeService)
}
} }

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.domain package com.tangem.tap.di.domain
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.* import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.CurrenciesRepository 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.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -108,8 +110,32 @@ internal object TokensDomainModule {
@Provides @Provides
@ViewModelScoped @ViewModelScoped
fun provideGetCryptoCurrencyActionsUseCase( fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase { ): 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

@ -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 { interface CustomTokenFeatureToggles {
/** Availability of redesigned screen (internal feature) */
val isRedesignedScreenEnabled: Boolean
val isNewCardScanningEnabled: Boolean val isNewCardScanningEnabled: Boolean
} }

View file

@ -14,9 +14,6 @@ internal class DefaultCustomTokenFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager, private val featureTogglesManager: FeatureTogglesManager,
) : CustomTokenFeatureToggles { ) : CustomTokenFeatureToggles {
override val isRedesignedScreenEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED")
override val isNewCardScanningEnabled: Boolean override val isNewCardScanningEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED") 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

@ -1,8 +1,8 @@
package com.tangem.tap.features.details.redux package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.Action import org.rekotlin.Action
@ -11,7 +11,6 @@ sealed class DetailsAction : Action {
data class PrepareScreen( data class PrepareScreen(
val scanResponse: ScanResponse, val scanResponse: ScanResponse,
val wallets: List<Wallet>,
) : DetailsAction() ) : DetailsAction()
object ReCreateTwinsWallet : DetailsAction() object ReCreateTwinsWallet : DetailsAction()
@ -70,6 +69,10 @@ sealed class DetailsAction : Action {
data class BiometricsStatusChanged( data class BiometricsStatusChanged(
val needEnrollBiometrics: Boolean, val needEnrollBiometrics: Boolean,
) : AppSettings() ) : AppSettings()
data class ChangeAppThemeMode(
val appThemeMode: AppThemeMode,
) : AppSettings()
} }
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction() data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()

View file

@ -9,6 +9,7 @@ import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
@ -221,6 +222,9 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.EnrollBiometrics -> { is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics() enrollBiometrics()
} }
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
changeAppThemeMode(action.appThemeMode)
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success, is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure, is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged, is DetailsAction.AppSettings.BiometricsStatusChanged,
@ -252,6 +256,14 @@ class DetailsMiddleware {
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
} }
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository)
scope.launch {
repository.changeAppThemeMode(appThemeMode)
}
}
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
// Nothing to change // Nothing to change
if (preferencesStorage.shouldSaveUserWallets == enable) { if (preferencesStorage.shouldSaveUserWallets == enable) {

View file

@ -9,7 +9,7 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager import com.tangem.tap.tangemSdkManager
import org.rekotlin.Action import org.rekotlin.Action
import java.util.* import java.util.EnumSet
object DetailsReducer { object DetailsReducer {
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state) fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
@ -39,8 +39,11 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.AppSettings -> { is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState) handlePrivacyAction(action, detailsState)
} }
is DetailsAction.ChangeAppCurrency -> is DetailsAction.ChangeAppCurrency -> detailsState.copy(
detailsState.copy(appCurrency = action.fiatCurrency) appSettingsState = detailsState.appSettingsState.copy(
selectedFiatCurrency = action.fiatCurrency,
),
)
is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState) is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
else -> detailsState else -> detailsState
} }
@ -49,13 +52,13 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState { private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState {
return DetailsState( return DetailsState(
scanResponse = action.scanResponse, scanResponse = action.scanResponse,
wallets = action.wallets,
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup, createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appCurrency = store.state.globalState.appCurrency,
appSettingsState = AppSettingsState( appSettingsState = AppSettingsState(
isBiometricsAvailable = tangemSdkManager.canUseBiometry, isBiometricsAvailable = tangemSdkManager.canUseBiometry,
saveWallets = preferencesStorage.shouldSaveUserWallets, saveWallets = preferencesStorage.shouldSaveUserWallets,
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes, saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
selectedFiatCurrency = store.state.globalState.appCurrency,
selectedThemeMode = store.state.globalState.appThemeMode,
), ),
) )
} }
@ -194,6 +197,11 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
needEnrollBiometrics = action.needEnrollBiometrics, needEnrollBiometrics = action.needEnrollBiometrics,
), ),
) )
is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy(
appSettingsState = state.appSettingsState.copy(
selectedThemeMode = action.appThemeMode,
),
)
is DetailsAction.AppSettings.EnrollBiometrics, is DetailsAction.AppSettings.EnrollBiometrics,
is DetailsAction.AppSettings.CheckBiometricsStatus, is DetailsAction.AppSettings.CheckBiometricsStatus,
-> state -> state

View file

@ -1,20 +1,18 @@
package com.tangem.tap.features.details.redux package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.StateType import org.rekotlin.StateType
import java.util.* import java.util.EnumSet
data class DetailsState( data class DetailsState(
val scanResponse: ScanResponse? = null, val scanResponse: ScanResponse? = null,
val wallets: List<Wallet> = emptyList(),
val cardSettingsState: CardSettingsState? = null, val cardSettingsState: CardSettingsState? = null,
val privacyPolicyUrl: String? = null, val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false, val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default,
val appSettingsState: AppSettingsState = AppSettingsState(), val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType ) : StateType
@ -57,6 +55,8 @@ data class AppSettingsState(
val isBiometricsAvailable: Boolean = false, val isBiometricsAvailable: Boolean = false,
val needEnrollBiometrics: Boolean = false, val needEnrollBiometrics: Boolean = false,
val isInProgress: Boolean = false, val isInProgress: Boolean = false,
val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default,
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
) )
enum class SecurityOption { LongTap, PassCode, AccessCode } enum class SecurityOption { LongTap, PassCode, AccessCode }

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,50 +1,51 @@
package com.tangem.tap.features.details.ui.appsettings package com.tangem.tap.features.details.ui.appsettings
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import androidx.compose.runtime.Composable
import android.view.View import androidx.compose.ui.Modifier
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.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.tangem.core.navigation.NavigationAction 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.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = AppSettingsViewModel(store) private val viewModel = AppSettingsViewModel(store)
private var screenState: MutableState<AppSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState)) @Composable
override fun ScreenContent(modifier: Modifier) {
AppSettingsScreen(
modifier = modifier,
state = viewModel.uiState,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
viewModel.checkBiometricsStatus() viewModel.checkBiometricsStatus()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun TransitionInflater.inflateTransitions(): Boolean {
return ComposeView(requireContext()).apply { enterTransition = inflateTransition(R.transition.fade)
setContent { exitTransition = inflateTransition(R.transition.fade)
isTransitionGroup = true
TangemTheme { return true
AppSettingsScreen(
state = screenState.value,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
}
}
} }
override fun onStart() { override fun onStart() {
@ -68,6 +69,6 @@ class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
override fun newState(state: DetailsState) { override fun newState(state: DetailsState) {
if (activity == null || view == null) return if (activity == null || view == null) return
screenState.value = viewModel.updateState(state) viewModel.updateState(state)
} }
} }

View file

@ -0,0 +1,76 @@
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 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,109 @@
package com.tangem.tap.features.details.ui.appsettings 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.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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerH24 import androidx.compose.ui.tooling.preview.PreviewParameter
import com.tangem.core.ui.components.SpacerH32 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.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.redux.AppSetting import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog 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.SettingsScreensScaffold
import com.tangem.tap.features.details.ui.common.TangemSwitch
import com.tangem.wallet.R import com.tangem.wallet.R
import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit) { internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold( 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, titleRes = R.string.app_settings_title,
onBackClick = onBackClick, onBackClick = onBackClick,
) )
} }
@Composable @Composable
private fun AppSettings(state: AppSettingsScreenState) { private fun AppSettings(state: AppSettingsScreenState.Content) {
var dialogType by remember { mutableStateOf<AppSetting?>(null) } val dialog by rememberUpdatedState(newValue = state.dialog)
val onDialogStateChange: (AppSetting?) -> Unit = { dialogType = it } when (val safeDialog = dialog) {
is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog)
dialogType?.let { is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog)
SettingsAlertDialog( null -> Unit
element = it,
onDialogStateChange = onDialogStateChange,
onSettingToggle = { state.onSettingToggled(it, false) },
)
} }
Column(modifier = Modifier.fillMaxSize()) { LazyColumn {
if (state.showEnrollBiometricsCard) { items(
EnrollBiometricsCard(onClick = state.onEnrollBiometrics) items = state.items,
SpacerH24() key = Item::id,
} ) { item ->
when (item) {
AppSettingsElement( is Item.Card -> SettingsCardItem(
state = state, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
setting = AppSetting.SaveWallets, item = item,
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,
) )
}, is Item.Button -> SettingsButtonItem(
) modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
} item = item,
} )
is Item.Switch -> SettingsSwitchItem(
private fun onCheckedChange( modifier = Modifier.padding(
element: AppSetting, vertical = TangemTheme.dimens.spacing16,
enabled: Boolean, horizontal = TangemTheme.dimens.spacing20,
onSettingToggled: (AppSetting, Boolean) -> Unit, ),
onDialogStateChange: (AppSetting?) -> Unit, item = item,
) { )
// Show warning if user wants to disable the switch }
if (!enabled) { }
onDialogStateChange(element)
} else {
onSettingToggled(element, true)
} }
} }
// region Preview // 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) @Preview(showBackground = true, widthDp = 360)
@Composable @Composable
private fun AppSettingsScreenPreview_Light() { private fun AppSettingsScreenPreview_Light(
@PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState,
) {
TangemTheme { TangemTheme {
AppSettingsScreenSample() AppSettingsScreen(state = state, onBackClick = {})
} }
} }
@Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360)
@Composable @Composable
private fun AppSettingsScreenPreview_Dark() { private fun AppSettingsScreenPreview_Dark(
@PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState,
) {
TangemTheme(isDark = true) { TangemTheme(isDark = true) {
AppSettingsScreenSample() AppSettingsScreen(state = state, onBackClick = {})
} }
} }
@Composable private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider<AppSettingsScreenState>(
private fun AppSettingsScreen_EnrollBiometrics_Sample(modifier: Modifier = Modifier) { collection = buildList {
Column(modifier = modifier.background(TangemTheme.colors.background.primary)) { val itemsFactory = AppSettingsItemsFactory()
AppSettingsScreen( val items = persistentListOf(
state = AppSettingsScreenState( itemsFactory.createEnrollBiometricsCard {},
settings = mapOf( itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
AppSetting.SaveWallets to true, itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }),
AppSetting.SaveAccessCode to false, itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
), itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
showEnrollBiometricsCard = true,
isTogglesEnabled = false,
onSettingToggled = { _, _ -> },
onEnrollBiometrics = {},
),
onBackClick = { },
) )
}
}
@Preview(showBackground = true, widthDp = 360) AppSettingsScreenState.Content(
@Composable items = items,
private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() { dialog = null,
TangemTheme { ).let(::add)
AppSettingsScreen_EnrollBiometrics_Sample() },
} )
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() {
TangemTheme(isDark = true) {
AppSettingsScreen_EnrollBiometrics_Sample()
}
}
// endregion Preview // endregion Preview

View file

@ -1,11 +1,70 @@
package com.tangem.tap.features.details.ui.appsettings 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( @Immutable
val settings: Map<AppSetting, Boolean> = emptyMap(), internal sealed class AppSettingsScreenState {
val showEnrollBiometricsCard: Boolean = false,
val isTogglesEnabled: Boolean = false, object Loading : AppSettingsScreenState()
val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ },
val onEnrollBiometrics: () -> Unit = { /* no-op */ }, 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,35 +1,33 @@
package com.tangem.tap.features.details.ui.appsettings package com.tangem.tap.features.details.ui.appsettings
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.AppSetting 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.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.wallet.redux.WalletAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.Store import org.rekotlin.Store
class AppSettingsViewModel(private val store: Store<AppState>) { internal class AppSettingsViewModel(private val store: Store<AppState>) {
fun updateState(state: DetailsState): AppSettingsScreenState { private val itemsFactory = AppSettingsItemsFactory()
return with(state.appSettingsState) { private val dialogsFactory = AppSettingsDialogsFactory()
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 fun onSettingsToggled(setting: AppSetting, enable: Boolean) { var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading)
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) private set
fun updateState(state: DetailsState) {
uiState = AppSettingsScreenState.Content(
items = buildItems(state.appSettingsState),
dialog = (uiState as? AppSettingsScreenState.Content)?.dialog,
)
} }
fun checkBiometricsStatus() { fun checkBiometricsStatus() {
@ -39,4 +37,113 @@ class AppSettingsViewModel(private val store: Store<AppState>) {
fun refreshBiometricsStatus() { fun refreshBiometricsStatus() {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = true)) store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = true))
} }
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.createSelectThemeModeButton(state.selectedThemeMode) {
showThemeModeSelector(state.selectedThemeMode)
}.let(::add)
}
return items.toImmutableList()
}
private fun enrollBiometrics() {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
}
private fun showAppCurrencySelector() {
store.dispatchOnMain(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
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 dismissDialog() {
updateContentState { copy(dialog = null) }
}
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 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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextButton import androidx.compose.ui.tooling.preview.PreviewParameter
import com.tangem.core.ui.components.WarningTextButton 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.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 import com.tangem.wallet.R
@Composable @Composable
internal fun SettingsAlertDialog( internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
element: AppSetting, BasicDialog(
onDialogStateChange: (AppSetting?) -> Unit, title = dialog.title.resolveReference(),
onSettingToggle: () -> Unit, message = dialog.description.resolveReference(),
) { isDismissable = false,
val text = when (element) { confirmButton = DialogButton(
AppSetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message title = dialog.confirmText.resolveReference(),
AppSetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message warning = true,
} onClick = dialog.onConfirm,
),
AlertDialog( dismissButton = DialogButton(
onDismissRequest = { onDialogStateChange(null) }, title = stringResource(id = R.string.common_cancel),
confirmButton = { onClick = dialog.onDismiss,
TextButton( ),
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), onDismissDialog = dialog.onDismiss,
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,
) )
} }
// region Preview // 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) @Preview(showBackground = true, widthDp = 360)
@Composable @Composable
private fun SettingsAlertDialogPreview_Light() { private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
TangemTheme { TangemTheme {
SettingsAlertDialogSample() SettingsAlertDialog(dialog = dialog)
} }
} }
@Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360)
@Composable @Composable
private fun SettingsAlertDialogPreview_Dark() { private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
TangemTheme(isDark = true) { 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 // 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 package com.tangem.tap.features.details.ui.appsettings.components
import androidx.compose.foundation.background import androidx.compose.foundation.layout.*
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.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.Surface import androidx.compose.material.Surface
@ -14,24 +9,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource 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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW16 import com.tangem.core.ui.components.SpacerW16
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme 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) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
internal fun EnrollBiometricsCard(onClick: () -> Unit) { internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) {
Surface( Surface(
modifier = Modifier modifier = modifier.fillMaxWidth(),
.padding(horizontal = TangemTheme.dimens.spacing8) color = TangemTheme.colors.button.disabled,
.fillMaxWidth(),
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersLarge, shape = TangemTheme.shapes.roundedCornersLarge,
onClick = onClick, onClick = item.onClick,
) { ) {
Row( Row(
modifier = Modifier.padding(all = 16.dp), modifier = Modifier.padding(all = 16.dp),
@ -39,20 +35,20 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) {
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_alert_circle_24), painter = painterResource(id = item.iconResId),
tint = TangemTheme.colors.icon.attention, tint = TangemTheme.colors.icon.attention,
contentDescription = null, contentDescription = null,
) )
SpacerW16() SpacerW16()
Column { Column {
Text( Text(
text = stringResource(id = R.string.app_settings_enable_biometrics_title), text = item.title.resolveReference(),
style = TangemTheme.typography.subtitle1, style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
) )
SpacerH4() SpacerH4()
Text( Text(
text = stringResource(id = R.string.app_settings_enable_biometrics_description), text = item.description.resolveReference(),
style = TangemTheme.typography.body2, style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary, color = TangemTheme.colors.text.secondary,
) )
@ -62,28 +58,29 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) {
} }
// region Preview // region Preview
@Composable
private fun EnrollBiometricsCardSample(modifier: Modifier = Modifier) {
Column(
modifier = modifier.background(TangemTheme.colors.background.secondary),
) {
EnrollBiometricsCard(onClick = {})
}
}
@Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360)
@Composable @Composable
private fun EnrollBiometricsCardPreview_Light() { private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
TangemTheme { TangemTheme {
EnrollBiometricsCardSample() SettingsCardItem(item = item)
} }
} }
@Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360)
@Composable @Composable
private fun EnrollBiometricsCardPreview_Dark() { private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
TangemTheme(isDark = true) { 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 // 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 package com.tangem.tap.features.details.ui.cardsettings
import android.os.Bundle import androidx.compose.runtime.Composable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.Modifier
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.tangem.core.navigation.NavigationAction 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.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber 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 val viewModel = CardSettingsViewModel(store)
private var screenState: MutableState<CardSettingsScreenState> = private var screenState: MutableState<CardSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
override fun onCreate(savedInstanceState: Bundle?) { @Composable
super.onCreate(savedInstanceState) override fun ScreenContent(modifier: Modifier) {
CardSettingsScreen(
val inflater = TransitionInflater.from(requireContext()) modifier = modifier,
enterTransition = inflater.inflateTransition(android.R.transition.fade) state = screenState.value,
exitTransition = inflater.inflateTransition(android.R.transition.fade) onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun TransitionInflater.inflateTransitions(): Boolean {
return ComposeView(requireContext()).apply { enterTransition = inflateTransition(R.transition.fade)
setContent { exitTransition = inflateTransition(R.transition.fade)
isTransitionGroup = true
TangemTheme { return true
CardSettingsScreen(
state = screenState.value,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
}
}
} }
override fun onStart() { 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.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.*
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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -18,7 +12,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview 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 import com.tangem.wallet.R
@Composable @Composable
fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) { internal fun CardSettingsScreen(
state: CardSettingsScreenState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val needReadCard = state.cardDetails == null val needReadCard = state.cardDetails == null
SettingsScreensScaffold( SettingsScreensScaffold(
modifier = modifier,
content = { content = {
if (needReadCard) { if (needReadCard) {
CardSettingsReadCard(state.onScanCardClick) CardSettingsReadCard(state.onScanCardClick)
@ -41,14 +39,13 @@ fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit)
} }
}, },
titleRes = R.string.card_settings_title, titleRes = R.string.card_settings_title,
backgroundColor = TangemTheme.colors.background.secondary,
onBackClick = onBackClick, onBackClick = onBackClick,
) )
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
@Composable @Composable
fun CardSettingsReadCard(onScanCardClick: () -> Unit) { private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
@ -84,13 +81,13 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
) { ) {
Text( Text(
text = stringResource(id = R.string.scan_card_settings_title), 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, style = TangemTheme.typography.h3,
) )
Spacer(modifier = Modifier.size(20.dp)) Spacer(modifier = Modifier.size(20.dp))
Text( Text(
text = stringResource(id = R.string.scan_card_settings_message), 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, style = TangemTheme.typography.body1,
modifier = Modifier modifier = Modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
@ -107,7 +104,7 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
@Suppress("ComplexMethod") @Suppress("ComplexMethod")
@Composable @Composable
fun CardSettings(state: CardSettingsScreenState) { private fun CardSettings(state: CardSettingsScreenState) {
if (state.cardDetails == null) return if (state.cardDetails == null) return
LazyColumn( LazyColumn(
@ -166,8 +163,25 @@ fun CardSettings(state: CardSettingsScreenState) {
} }
} }
// region Preview
@Composable @Composable
@Preview private fun CardSettingsScreenStateSample() {
private fun CardSettingsPreview() {
CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {}) 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.wallet.R
import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
data class CardSettingsScreenState( internal data class CardSettingsScreenState(
val cardDetails: List<CardInfo>? = null, val cardDetails: List<CardInfo>? = null,
val accessCodeRecoveryState: AccessCodeRecoveryState? = null, val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit, val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit, val onElementClick: (CardInfo) -> Unit,
) )
sealed class CardInfo( internal sealed class CardInfo(
val titleRes: TextReference, val titleRes: TextReference,
val subtitle: TextReference, val subtitle: TextReference,
val clickable: Boolean = false, val clickable: Boolean = false,
@ -68,7 +68,7 @@ sealed class CardInfo(
} }
// TODO("Remove and use the same from coreUI") // 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 { class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList()) constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
} }
@ -78,7 +78,7 @@ sealed interface TextReference {
@Composable @Composable
@ReadOnlyComposable @ReadOnlyComposable
fun TextReference.resolveReference(): String { internal fun TextReference.resolveReference(): String {
return when (this) { return when (this) {
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray()) is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
is TextReference.Str -> this.value 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 com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store import org.rekotlin.Store
class CardSettingsViewModel(private val store: Store<AppState>) { internal class CardSettingsViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): CardSettingsScreenState { fun updateState(state: CardSettingsState?): CardSettingsScreenState {
return if (state?.manageSecurityState == null) { return if (state?.manageSecurityState == null) {

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.ui.common package com.tangem.tap.features.details.ui.common
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.material.* import androidx.compose.material.*
@ -12,20 +13,24 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R import com.tangem.wallet.R
@Composable @Composable
fun SettingsScreensScaffold( internal 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,
onBackClick: () -> Unit, onBackClick: () -> Unit,
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
fab: @Composable (() -> Unit)? = null,
@StringRes titleRes: Int? = null,
) { ) {
BackHandler(true, onBackClick) val backgroundColor = TangemTheme.colors.background.secondary
BackHandler(onBack = onBackClick)
SystemBarsEffect {
setSystemBarsColor(backgroundColor)
}
Scaffold( Scaffold(
topBar = { topBar = {
@ -37,33 +42,30 @@ fun SettingsScreensScaffold(
modifier = modifier.systemBarsPadding(), modifier = modifier.systemBarsPadding(),
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
floatingActionButton = { fab?.invoke() }, floatingActionButton = { fab?.invoke() },
) { ) { paddings ->
if (titleRes != null) { Column(
Box(modifier = modifier.fillMaxSize()) { modifier = Modifier
background?.invoke() .padding(paddings)
.fillMaxSize(),
Column(modifier = modifier.fillMaxWidth()) { ) {
Text( if (titleRes != null) {
text = stringResource(id = titleRes), Text(
modifier = modifier.padding( text = stringResource(id = titleRes),
start = TangemTheme.dimens.spacing20, modifier = Modifier
end = TangemTheme.dimens.spacing20, .padding(horizontal = TangemTheme.dimens.spacing20)
bottom = TangemTheme.dimens.spacing54, .padding(bottom = TangemTheme.dimens.spacing36),
), style = TangemTheme.typography.h1,
style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1,
color = TangemTheme.colors.text.primary1, )
)
content()
}
} }
} else {
content() content()
} }
} }
} }
@Composable @Composable
fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
Text( Text(
text = stringResource(id = titleRes), text = stringResource(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp), modifier = modifier.padding(start = 20.dp, end = 20.dp),
@ -73,7 +75,7 @@ fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
} }
@Composable @Composable
fun EmptyTopBarWithNavigation( internal fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit, onBackClick: () -> Unit,
backgroundColor: Color = TangemTheme.colors.background.primary, backgroundColor: Color = TangemTheme.colors.background.primary,
) { ) {
@ -95,7 +97,12 @@ fun EmptyTopBarWithNavigation(
} }
@Composable @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( PrimaryButtonIconEnd(
text = title, text = title,
enabled = enabled, enabled = enabled,
@ -107,7 +114,7 @@ fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = M
} }
@Composable @Composable
fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) { internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -1,45 +1,48 @@
package com.tangem.tap.features.details.ui.details package com.tangem.tap.features.details.ui.details
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import androidx.compose.runtime.Composable
import android.view.View import androidx.compose.ui.Modifier
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction 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.common.analytics.events.Settings
import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber 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) private val detailsViewModel = DetailsViewModel(store)
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Analytics.send(Settings.ScreenOpened()) 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 { @Composable
return ComposeView(requireContext()).apply { override fun ScreenContent(modifier: Modifier) {
setContent { DetailsScreen(
isTransitionGroup = true modifier = modifier,
TangemTheme { state = detailsViewModel.detailsScreenState.value,
DetailsScreen( onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
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() { override fun onStart() {

View file

@ -1,18 +1,7 @@
package com.tangem.tap.features.details.ui.details package com.tangem.tap.features.details.ui.details
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
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.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -27,12 +16,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.common.ScreenTitle import com.tangem.tap.features.details.ui.common.ScreenTitle
@ -41,58 +30,69 @@ import com.tangem.wallet.R
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) { internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SystemBarsEffect {
setSystemBarsColor(color = TangemColorPalette.Light1)
}
SettingsScreensScaffold( SettingsScreensScaffold(
modifier = modifier,
content = { Content(state = state) }, content = { Content(state = state) },
onBackClick = onBackClick, onBackClick = onBackClick,
) )
} }
@Composable @Composable
fun Content(state: DetailsScreenState) { private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) {
Box { Box(modifier = modifier) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp)) ScreenTitle(titleRes = R.string.details_title)
state.elements.map { element -> SpacerH(height = TangemTheme.dimens.spacing36)
if (element == SettingsElement.WalletConnect) { SettingsItems(
WalletConnectDetailsItem(onItemsClick = state.onItemsClick) items = state.elements,
} else { onItemsClick = state.onItemsClick,
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),
) )
SpacerHMax()
TangemSocialAccounts(
links = state.tangemLinks,
onSocialNetworkClick = state.onSocialNetworkClick,
)
SpacerH(height = TangemTheme.dimens.spacing16)
TangemAppVersion(
appNameRes = state.appNameRes,
version = state.tangemVersion,
)
SpacerH(height = TangemTheme.dimens.spacing24)
} }
ShowSnackbarIfNeeded(state.showErrorSnackbar.value) ShowSnackbarIfNeeded(state.showErrorSnackbar.value)
} }
} }
@Composable @Composable
fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) { private fun SettingsItems(items: List<SettingsElement>, onItemsClick: (SettingsElement) -> Unit) {
items.forEach { item ->
val onItemClick = remember(item) {
{ onItemsClick(item) }
}
if (item == SettingsElement.WalletConnect) {
WalletConnectDetailsItem(onItemClick)
} else {
DetailsItem(
item = item,
onItemClick = onItemClick,
)
}
}
}
@Composable
private fun WalletConnectDetailsItem(onItemClick: () -> Unit) {
Row( Row(
modifier = Modifier modifier = Modifier
.defaultMinSize(minHeight = 84.dp) .defaultMinSize(minHeight = 84.dp)
.fillMaxWidth() .fillMaxWidth()
.clickable { onItemsClick(SettingsElement.WalletConnect) }, .clickable(onClick = onItemClick),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -100,7 +100,7 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
painter = painterResource(id = R.drawable.ic_walletconnect), painter = painterResource(id = R.drawable.ic_walletconnect),
contentDescription = stringResource(id = R.string.wallet_connect_title), contentDescription = stringResource(id = R.string.wallet_connect_title),
modifier = Modifier.padding(start = 20.dp, end = 20.dp), modifier = Modifier.padding(start = 20.dp, end = 20.dp),
tint = colorResource(id = R.color.all_colors_azure), tint = TangemColorPalette.Azure,
) )
Column( Column(
modifier = Modifier.defaultMinSize(minHeight = 56.dp), modifier = Modifier.defaultMinSize(minHeight = 56.dp),
@ -111,25 +111,25 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
text = stringResource(id = R.string.wallet_connect_title), text = stringResource(id = R.string.wallet_connect_title),
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTheme.typography.h3, style = TangemTheme.typography.h3,
color = colorResource(id = R.color.text_primary_1), color = TangemTheme.colors.text.primary1,
) )
Text( Text(
text = stringResource(id = R.string.wallet_connect_subtitle), text = stringResource(id = R.string.wallet_connect_subtitle),
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp), modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTheme.typography.body1, style = TangemTheme.typography.body1,
color = colorResource(id = R.color.text_secondary), color = TangemTheme.colors.text.secondary,
) )
} }
} }
} }
@Composable @Composable
fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) { private fun DetailsItem(item: SettingsElement, onItemClick: () -> Unit) {
Row( Row(
modifier = Modifier modifier = Modifier
.height(56.dp) .height(56.dp)
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onItemsClick), .clickable(onClick = onItemClick),
horizontalArrangement = Arrangement.Start, horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@ -137,28 +137,21 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () ->
painter = painterResource(id = item.iconRes), painter = painterResource(id = item.iconRes),
contentDescription = stringResource(id = item.titleRes), contentDescription = stringResource(id = item.titleRes),
modifier = Modifier.padding(start = 20.dp, end = 20.dp), modifier = Modifier.padding(start = 20.dp, end = 20.dp),
tint = colorResource(id = R.color.icon_secondary), tint = TangemTheme.colors.icon.secondary,
) )
Column(modifier = Modifier.padding(end = 20.dp)) { Column(modifier = Modifier.padding(end = 20.dp)) {
Text( Text(
text = stringResource(id = item.titleRes), text = stringResource(id = item.titleRes),
modifier = Modifier, modifier = Modifier,
style = TangemTheme.typography.subtitle1, style = TangemTheme.typography.subtitle1,
color = colorResource(id = R.color.text_primary_1), color = TangemTheme.colors.text.primary1,
) )
if (item == SettingsElement.AppCurrency) {
Text(
text = appCurrency,
style = TangemTheme.typography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
} }
} }
} }
@Composable @Composable
fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) { private fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
LazyRow( LazyRow(
modifier = Modifier.padding(start = 8.dp, end = 8.dp), modifier = Modifier.padding(start = 8.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -170,14 +163,14 @@ fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (
modifier = Modifier modifier = Modifier
.padding(8.dp) .padding(8.dp)
.clickable { onSocialNetworkClick(it) }, .clickable { onSocialNetworkClick(it) },
tint = colorResource(id = R.color.icon_informative), tint = TangemTheme.colors.icon.informative,
) )
} }
} }
} }
@Composable @Composable
fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) { private fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
SnackbarHost( SnackbarHost(
@ -207,17 +200,43 @@ fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
} }
@Composable @Composable
@Preview private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) {
private fun Preview() { Text(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
text = "${stringResource(id = appNameRes)} $version",
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.tertiary,
)
}
// region Preview
@Composable
private fun DetailsScreenContentSample() {
DetailsScreen( DetailsScreen(
state = DetailsScreenState( state = DetailsScreenState(
elements = SettingsElement.values().toList(), elements = SettingsElement.values().toList(),
tangemLinks = TangemSocialAccounts.accountsEn, tangemLinks = TangemSocialAccounts.accountsEn,
tangemVersion = "Tangem 2.14.12 (343)", tangemVersion = "Tangem 2.14.12 (343)",
appCurrency = "Dollar",
onItemsClick = {}, onItemsClick = {},
onSocialNetworkClick = {}, onSocialNetworkClick = {},
), ),
onBackClick = {}, onBackClick = {},
) )
} }
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun DetailsScreenContentPreview_Light() {
TangemTheme(isDark = false) {
DetailsScreenContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun DetailsScreenContentPreview_Dark() {
TangemTheme(isDark = true) {
DetailsScreenContentSample()
}
}
// endregion Preview

View file

@ -6,11 +6,10 @@ import androidx.compose.runtime.mutableStateOf
import com.tangem.wallet.R import com.tangem.wallet.R
@Immutable @Immutable
data class DetailsScreenState( internal data class DetailsScreenState(
val elements: List<SettingsElement>, val elements: List<SettingsElement>,
val tangemLinks: List<SocialNetworkLink>, val tangemLinks: List<SocialNetworkLink>,
val tangemVersion: String, val tangemVersion: String,
val appCurrency: String,
val onItemsClick: (SettingsElement) -> Unit, val onItemsClick: (SettingsElement) -> Unit,
val onSocialNetworkClick: (SocialNetworkLink) -> Unit, val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
val showErrorSnackbar: MutableState<EventError> = mutableStateOf(EventError.Empty), val showErrorSnackbar: MutableState<EventError> = mutableStateOf(EventError.Empty),
@ -19,30 +18,29 @@ data class DetailsScreenState(
} }
@Immutable @Immutable
enum class SettingsElement( internal enum class SettingsElement(
val iconRes: Int, val iconRes: Int,
val titleRes: Int, val titleRes: Int,
) { ) {
WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title), WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title),
Chat(R.drawable.ic_chat, R.string.details_chat), LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback),
ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title), ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title),
CardSettings(R.drawable.ic_card_settings, R.string.card_settings_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), AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), Chat(R.drawable.ic_chat, R.string.details_chat),
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback),
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), PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy),
TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu), TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu),
} }
@Immutable @Immutable
data class SocialNetworkLink( internal data class SocialNetworkLink(
val network: SocialNetwork, val network: SocialNetwork,
val url: String, val url: String,
) )
sealed class EventError { internal sealed class EventError {
object Empty : EventError() object Empty : EventError()
data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError() data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError()
} }
@ -58,7 +56,7 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) {
object Discord : SocialNetwork("Discord", R.drawable.ic_discord) object Discord : SocialNetwork("Discord", R.drawable.ic_discord)
} }
object TangemSocialAccounts { internal object TangemSocialAccounts {
val accountsEn: List<SocialNetworkLink> = listOf( val accountsEn: List<SocialNetworkLink> = listOf(
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"), SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"), SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),

View file

@ -7,52 +7,63 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.analytics.events.Settings 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.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.home.LocaleRegionProvider import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
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 import org.rekotlin.Store
class DetailsViewModel(private val store: Store<AppState>) { internal class DetailsViewModel(private val store: Store<AppState>) {
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState)) var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
private set private set
@Suppress("ComplexMethod") init {
bootstrapScreenState()
}
fun updateState(state: DetailsState): DetailsScreenState { fun updateState(state: DetailsState): DetailsScreenState {
val cardTypesResolver = state.scanResponse?.cardTypesResolver return DetailsScreenState(
val settings = SettingsElement.values().mapNotNull { elements = createSettingsItems(state),
tangemLinks = getSocialLinks(),
tangemVersion = getTangemAppVersion(),
onItemsClick = { handleClickingSettingsItem(it) },
onSocialNetworkClick = { handleSocialNetworkClick(it) },
)
}
@Suppress("ComplexMethod")
private fun createSettingsItems(state: DetailsState): List<SettingsElement> {
val scanResponse = state.scanResponse ?: return emptyList()
val cardTypesResolver = scanResponse.cardTypesResolver
return SettingsElement.values().mapNotNull {
when (it) { when (it) {
SettingsElement.WalletConnect -> { SettingsElement.WalletConnect -> if (cardTypesResolver.isMultiwalletAllowed()) it else null
if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null
}
SettingsElement.SendFeedback -> it SettingsElement.SendFeedback -> it
SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null
SettingsElement.PrivacyPolicy -> { SettingsElement.PrivacyPolicy -> if (state.privacyPolicyUrl != null) it else null
if (state.privacyPolicyUrl != null) it else null
}
SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) 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()) it else null
SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null
SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null
else -> it else -> it
} }
} }
return DetailsScreenState(
elements = settings,
tangemLinks = getSocialLinks(),
tangemVersion = getTangemAppVersion(),
appCurrency = state.appCurrency.name,
onItemsClick = { handleClickingSettingsItem(it) },
onSocialNetworkClick = { handleSocialNetworkClick(it) },
)
} }
private fun handleSocialNetworkClick(link: SocialNetworkLink) { private fun handleSocialNetworkClick(link: SocialNetworkLink) {
@ -78,9 +89,6 @@ class DetailsViewModel(private val store: Store<AppState>) {
Analytics.send(Settings.ButtonCardSettings()) Analytics.send(Settings.ButtonCardSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings)) store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
} }
SettingsElement.AppCurrency -> {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
SettingsElement.AppSettings -> { SettingsElement.AppSettings -> {
Analytics.send(Settings.ButtonAppSettings()) Analytics.send(Settings.ButtonAppSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings)) store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
@ -118,4 +126,14 @@ class DetailsViewModel(private val store: Store<AppState>) {
val versionName: String = BuildConfig.VERSION_NAME val versionName: String = BuildConfig.VERSION_NAME
return "$versionName ($versionCode)" return "$versionName ($versionCode)"
} }
private fun bootstrapScreenState() {
userWalletsListManager.selectedUserWallet
.distinctUntilChanged()
.onEach { selectedUserWallet ->
store.dispatchWithMain(DetailsAction.PrepareScreen(selectedUserWallet.scanResponse))
}
.flowOn(Dispatchers.IO)
.launchIn(scope)
}
} }

View file

@ -1,47 +1,45 @@
package com.tangem.tap.features.details.ui.resetcard package com.tangem.tap.features.details.ui.resetcard
import android.os.Bundle import androidx.compose.runtime.Composable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.Modifier
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.tangem.core.navigation.NavigationAction 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.features.details.redux.DetailsState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber 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 val viewModel = ResetCardViewModel(store)
private var screenState: MutableState<ResetCardScreenState> = private var screenState: MutableState<ResetCardScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState)) mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
override fun onCreate(savedInstanceState: Bundle?) { @Composable
super.onCreate(savedInstanceState) override fun ScreenContent(modifier: Modifier) {
ResetCardScreen(
val inflater = TransitionInflater.from(requireContext()) modifier = modifier,
enterTransition = inflater.inflateTransition(android.R.transition.fade) state = screenState.value,
exitTransition = inflater.inflateTransition(android.R.transition.fade) onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun TransitionInflater.inflateTransitions(): Boolean {
return ComposeView(requireContext()).apply { enterTransition = inflateTransition(R.transition.fade)
setContent { exitTransition = inflateTransition(R.transition.fade)
isTransitionGroup = true
TangemTheme { return true
ResetCardScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
} }
override fun onStart() { override fun onStart() {

View file

@ -1,26 +1,12 @@
package com.tangem.tap.features.details.ui.resetcard package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.foundation.Image import androidx.compose.foundation.*
import androidx.compose.foundation.background import androidx.compose.foundation.layout.*
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconToggleButton import androidx.compose.material.IconToggleButton
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -34,17 +20,17 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R import com.tangem.wallet.R
@Composable @Composable
fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit) { internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold( SettingsScreensScaffold(
modifier = modifier,
content = { ResetCardView(state = state) }, content = { ResetCardView(state = state) },
onBackClick = onBackClick, onBackClick = onBackClick,
backgroundColor = Color.Transparent,
) )
} }
@Suppress("LongMethod", "MagicNumber") @Suppress("LongMethod", "MagicNumber")
@Composable @Composable
fun ResetCardView(state: ResetCardScreenState) { private fun ResetCardView(state: ResetCardScreenState) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.details.ui.resetcard
import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.details.ui.cardsettings.TextReference
data class ResetCardScreenState( internal data class ResetCardScreenState(
val accepted: Boolean = false, val accepted: Boolean = false,
val descriptionText: TextReference, val descriptionText: TextReference,
val onAcceptWarningToggleClick: (Boolean) -> Unit, 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 com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import org.rekotlin.Store import org.rekotlin.Store
class ResetCardViewModel(private val store: Store<AppState>) { internal class ResetCardViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): ResetCardScreenState { fun updateState(state: CardSettingsState?): ResetCardScreenState {
val descriptionText = state?.cardInfo val descriptionText = state?.cardInfo

View file

@ -1,47 +1,45 @@
package com.tangem.tap.features.details.ui.securitymode package com.tangem.tap.features.details.ui.securitymode
import android.os.Bundle import androidx.compose.runtime.Composable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.Modifier
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.tangem.core.navigation.NavigationAction 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.features.details.redux.DetailsState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber 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 val viewModel = SecurityModeViewModel(store)
private var screenState: MutableState<SecurityModeScreenState> = private var screenState: MutableState<SecurityModeScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState)) mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
override fun onCreate(savedInstanceState: Bundle?) { @Composable
super.onCreate(savedInstanceState) override fun ScreenContent(modifier: Modifier) {
SecurityModeScreen(
val inflater = TransitionInflater.from(requireContext()) modifier = modifier,
enterTransition = inflater.inflateTransition(android.R.transition.fade) state = screenState.value,
exitTransition = inflater.inflateTransition(android.R.transition.fade) onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun TransitionInflater.inflateTransitions(): Boolean {
return ComposeView(requireContext()).apply { enterTransition = inflateTransition(R.transition.fade)
setContent { exitTransition = inflateTransition(R.transition.fade)
isTransitionGroup = true
TangemTheme { return true
SecurityModeScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
} }
override fun onStart() { override fun onStart() {

View file

@ -1,10 +1,6 @@
package com.tangem.tap.features.details.ui.securitymode package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.*
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.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -20,8 +16,13 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R import com.tangem.wallet.R
@Composable @Composable
fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) { internal fun SecurityModeScreen(
state: SecurityModeScreenState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold( SettingsScreensScaffold(
modifier = modifier,
content = { SecurityModeOptions(state = state) }, content = { SecurityModeOptions(state = state) },
// titleRes = R.string.card_settings_security_mode, // titleRes = R.string.card_settings_security_mode,
onBackClick = onBackClick, onBackClick = onBackClick,
@ -29,7 +30,7 @@ fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit)
} }
@Composable @Composable
fun SecurityModeOptions(state: SecurityModeScreenState) { private fun SecurityModeOptions(state: SecurityModeScreenState) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@ -55,7 +56,7 @@ fun SecurityModeOptions(state: SecurityModeScreenState) {
} }
@Composable @Composable
fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) { private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
val selected = option == state.selectedSecurityMode val selected = option == state.selectedSecurityMode
val title = option.toTitleRes() 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.tap.features.details.redux.SecurityOption
import com.tangem.wallet.R import com.tangem.wallet.R
data class SecurityModeScreenState( internal data class SecurityModeScreenState(
val availableOptions: List<SecurityOption>, val availableOptions: List<SecurityOption>,
val selectedSecurityMode: SecurityOption, val selectedSecurityMode: SecurityOption,
val isSaveChangesEnabled: Boolean, val isSaveChangesEnabled: Boolean,
@ -11,7 +11,7 @@ data class SecurityModeScreenState(
val onSaveChangesClicked: () -> Unit, val onSaveChangesClicked: () -> Unit,
) )
fun SecurityOption.toTitleRes(): Int { internal fun SecurityOption.toTitleRes(): Int {
return when (this) { return when (this) {
SecurityOption.LongTap -> R.string.details_manage_security_long_tap SecurityOption.LongTap -> R.string.details_manage_security_long_tap
SecurityOption.PassCode -> R.string.details_manage_security_passcode 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 com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Store import org.rekotlin.Store
class SecurityModeViewModel(val store: Store<AppState>) { internal class SecurityModeViewModel(val store: Store<AppState>) {
fun updateState(state: ManageSecurityState?): SecurityModeScreenState { fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
if (state == null) { if (state == null) {

View file

@ -1,24 +1,30 @@
package com.tangem.tap.features.details.ui.walletconnect package com.tangem.tap.features.details.ui.walletconnect
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import androidx.compose.runtime.Composable
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.Modifier
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction 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.common.analytics.events.WalletConnect
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber 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 val viewModel = WalletConnectViewModel(store)
private var screenState: MutableState<WalletConnectScreenState> = private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(viewModel.updateState(store.state.walletConnectState)) mutableStateOf(viewModel.updateState(store.state.walletConnectState))
@ -26,32 +32,31 @@ class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Analytics.send(WalletConnect.ScreenOpened()) 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 { @Composable
return ComposeView(requireContext()).apply { override fun ScreenContent(modifier: Modifier) {
setContent { WalletConnectScreen(
isTransitionGroup = true modifier = modifier,
TangemTheme { state = screenState.value,
WalletConnectScreen( onBackClick = {
state = screenState.value, if (screenState.value.isLoading) {
onBackClick = { store.dispatch(
if (screenState.value.isLoading) { WalletConnectAction.FailureEstablishingSession(
store.dispatch( store.state.walletConnectState.newSessionData?.session?.session,
WalletConnectAction.FailureEstablishingSession( ),
store.state.walletConnectState.newSessionData?.session?.session,
),
)
}
store.dispatch(NavigationAction.PopBackTo())
},
) )
} }
} store.dispatch(NavigationAction.PopBackTo())
} },
)
}
override fun TransitionInflater.inflateTransitions(): Boolean {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
return true
} }
override fun onStart() { override fun onStart() {

View file

@ -26,10 +26,15 @@ import com.tangem.wallet.R
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit) { internal fun WalletConnectScreen(
state: WalletConnectScreenState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current val context = LocalContext.current
SettingsScreensScaffold( SettingsScreensScaffold(
modifier = modifier,
content = { content = {
if (state.sessions.isEmpty()) { if (state.sessions.isEmpty()) {
EmptyScreen(state) EmptyScreen(state)

View file

@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.walletconnect
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class WalletConnectScreenState( internal data class WalletConnectScreenState(
val sessions: ImmutableList<WcSessionForScreen>, val sessions: ImmutableList<WcSessionForScreen>,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val onRemoveSession: (String) -> Unit = {}, val onRemoveSession: (String) -> Unit = {},

View file

@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.Store import org.rekotlin.Store
import timber.log.Timber import timber.log.Timber
class WalletConnectViewModel(private val store: Store<AppState>) { internal class WalletConnectViewModel(private val store: Store<AppState>) {
fun updateState(state: WalletConnectState): WalletConnectScreenState { fun updateState(state: WalletConnectState): WalletConnectScreenState {
Timber.d("WC2 Sessions: ${state.wc2Sessions}") Timber.d("WC2 Sessions: ${state.wc2Sessions}")
val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + 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() { override fun onStart() {
super.onStart() super.onStart()
setStatusBarColor(R.color.backgroundLightGray) setStatusBarColor(R.color.background_secondary)
webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) } webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) }
store.subscribe(subscriber = this) { state -> store.subscribe(subscriber = this) { state ->

View file

@ -105,7 +105,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
setStatusBarColor(R.color.backgroundWhite) setStatusBarColor(R.color.background_primary)
} }
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) = private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =

View file

@ -17,7 +17,7 @@ object ResetBackupCardDialog {
setPositiveButton(R.string.common_cancel) { _, _ -> setPositiveButton(R.string.common_cancel) { _, _ ->
Analytics.send(Onboarding.Backup.ResetCancelEvent) Analytics.send(Onboarding.Backup.ResetCancelEvent)
} }
setNegativeButton(R.string.common_reset) { _, _ -> setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ ->
Analytics.send(Onboarding.Backup.ResetPerformEvent) Analytics.send(Onboarding.Backup.ResetPerformEvent)
store.dispatch(BackupAction.ResetBackupCard(cardId)) store.dispatch(BackupAction.ResetBackupCard(cardId))
} }

View file

@ -260,7 +260,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0 val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 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)) tvAmountCurrency.setTextColor(fg.getColor(textColor))
} }

View file

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

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.tokens.impl.di 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.DefaultTokensListRouter
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
import dagger.Module import dagger.Module
@ -18,7 +17,5 @@ internal object TokensListRouterModule {
@Provides @Provides
@ViewModelScoped @ViewModelScoped
fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter { fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter()
return DefaultTokensListRouter(customTokenFeatureToggles)
}
} }

View file

@ -1,252 +1,17 @@
package com.tangem.tap.features.tokens.impl.domain package com.tangem.tap.features.tokens.impl.domain
import androidx.paging.PagingData 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.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.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 kotlinx.coroutines.flow.Flow
import timber.log.Timber
/** /**
* Default implementation of tokens list interactor * Default implementation of tokens list interactor
* FIXME("Necessary to avoid using redux actions")
* *
* @property repository repository of tokens list feature * @property repository repository of tokens list feature
* @property reduxStateHolder redux state holder
*/ */
internal class DefaultTokensListInteractor( internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor {
private val repository: TokensListRepository,
private val reduxStateHolder: AppStateHolder,
) : TokensListInteractor {
override fun getTokensList(searchText: String): Flow<PagingData<Token>> { override fun getTokensList(searchText: String): Flow<PagingData<Token>> {
return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null })) 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 derivations = currencies.mapNotNull {
val curve = config.primaryCurve(it.blockchain)
curve?.let { getDerivations(curve, scanResponse, currencies) }
}.associate(transform = TokensMiddleware.DerivationData::derivations)
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,
currencyList: List<Currency>,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val manageTokensCandidates = currencyList
.map(Currency::blockchain)
.distinct()
.filter { it.getSupportedCurves().contains(curve) }
.mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) }
val customTokensCandidates = currencyList
.filter { it.blockchain.getSupportedCurves().contains(curve) }
.mapNotNull(Currency::derivationPath)
.map(::DerivationPath)
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency ->
currency.derivationPath?.let {
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
}
}
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot(alreadyDerivedPaths::contains)
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 package com.tangem.tap.features.tokens.impl.domain
import androidx.paging.PagingData 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.impl.domain.models.Token
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
/** /**
@ -15,12 +13,4 @@ internal interface TokensListInteractor {
/** Get tokens list using filter by text [searchText] */ /** Get tokens list using filter by text [searchText] */
fun getTokensList(searchText: String): Flow<PagingData<Token>> 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
}

View file

@ -6,8 +6,6 @@ import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R import com.tangem.wallet.R
@ -18,20 +16,14 @@ import com.tangem.wallet.R
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal class DefaultTokensListRouter( internal class DefaultTokensListRouter : TokensListRouter {
private val customTokenFeatureToggles: CustomTokenFeatureToggles,
) : TokensListRouter {
override fun popBackStack() { override fun popBackStack() {
store.dispatch(NavigationAction.PopBackTo()) store.dispatch(NavigationAction.PopBackTo())
} }
override fun openAddCustomTokenScreen() { override fun openAddCustomTokenScreen() {
if (customTokenFeatureToggles.isRedesignedScreenEnabled) { store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
} else {
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
}
} }
override fun showAddressCopiedNotification() { override fun showAddressCopiedNotification() {

View file

@ -0,0 +1,9 @@
package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.tokens.TokenWithBlockchain
internal data class TokensListCryptoCurrencies(
val coins: List<Blockchain>,
val tokens: List<TokenWithBlockchain>,
)

View file

@ -0,0 +1,192 @@
package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import arrow.core.Either
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import timber.log.Timber
import kotlin.properties.Delegates
/**
* Class that divide a new and legacy logic when user uses tokens list screen
*
* @property walletFeatureToggles wallet feature toggles
* @property getSelectedWalletUseCase use case that returns selected wallet
* @property getCurrenciesUseCase use case that returns crypto currencies of a specified wallet
*/
internal class TokensListMigration(
private val walletFeatureToggles: WalletFeatureToggles,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
) {
private var currentNewCoins: List<CryptoCurrency.Coin> by Delegates.notNull()
private var currentNewTokens: List<CryptoCurrency.Token> by Delegates.notNull()
private var currentUserWallet: UserWallet by Delegates.notNull()
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
suspend fun getCurrentCryptoCurrencies(): TokensListCryptoCurrencies {
return if (walletFeatureToggles.isRedesignedScreenEnabled) {
getNewCryptoCurrencies()
} else {
getLegacyCryptoCurrencies()
}
}
private suspend fun getNewCryptoCurrencies(): TokensListCryptoCurrencies {
return when (val selectedWalletEither = getSelectedWalletUseCase()) {
is Either.Left -> {
Timber.e(selectedWalletEither.value.toString())
TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList())
}
is Either.Right -> {
currentUserWallet = selectedWalletEither.value
val derivationStyle = currentUserWallet.scanResponse.derivationStyleProvider.getDerivationStyle()
when (val currenciesEither = getCurrenciesUseCase(userWalletId = selectedWalletEither.value.walletId)) {
is Either.Left -> {
Timber.e(currenciesEither.value.toString())
TokensListCryptoCurrencies(coins = emptyList(), tokens = emptyList())
}
is Either.Right -> {
TokensListCryptoCurrencies(
coins = currenciesEither.value
.filterIsInstance<CryptoCurrency.Coin>()
.filterNot { it.isCustomCurrency(derivationStyle) }
.also { currentNewCoins = it }
.map { Blockchain.fromId(it.network.id.value) },
tokens = currenciesEither.value
.filterIsInstance<CryptoCurrency.Token>()
.filterNot(CryptoCurrency.Token::isCustom)
.also { currentNewTokens = it }
.map { token ->
TokenWithBlockchain(
token = Token(
name = token.name,
symbol = token.symbol,
contractAddress = token.contractAddress,
decimals = token.decimals,
id = token.id.rawCurrencyId,
),
blockchain = Blockchain.fromId(token.network.id.value),
)
},
)
}
}
}
}
}
private fun CryptoCurrency.Coin.isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
if (derivationPath == null || derivationStyle == null) return false
return derivationPath != Blockchain.fromId(network.id.value).derivationPath(derivationStyle)?.rawPath
}
private fun getLegacyCryptoCurrencies(): TokensListCryptoCurrencies {
val wallets = store.state.walletState.walletsDataFromStores
val derivationStyle = store.state.globalState.scanResponse?.derivationStyleProvider?.getDerivationStyle()
return TokensListCryptoCurrencies(
coins = wallets.toNonCustomBlockchains(derivationStyle),
tokens = wallets.toNonCustomTokensWithBlockchains(derivationStyle),
)
}
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return this
.mapNotNull { walletDataModel ->
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) {
null
} else {
(walletDataModel.currency as? Currency.Blockchain)?.blockchain
}
}
.distinct()
}
private fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
derivationStyle: DerivationStyle?,
): List<TokenWithBlockchain> {
return this
.mapNotNull { walletDataModel ->
if (walletDataModel.currency !is Currency.Token) return@mapNotNull null
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain)
}
.distinct()
}
fun onSaveButtonClick(
currentTokensList: List<TokenWithBlockchain>,
currentBlockchainList: List<Blockchain>,
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
if (walletFeatureToggles.isRedesignedScreenEnabled) {
saveByNewWay(changedTokensList = changedTokensList, changedBlockchainList = changedBlockchainList)
} else {
saveByOldWay(currentTokensList, currentBlockchainList, changedTokensList, changedBlockchainList)
}
}
private fun saveByNewWay(
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
store.dispatch(
action = TokensAction.NewSaveChanges(
currentTokens = currentNewTokens,
currentCoins = currentNewCoins,
changedTokens = changedTokensList.mapNotNull {
cryptoCurrencyFactory.createToken(
sdkToken = it.token,
blockchain = it.blockchain,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
changedCoins = changedBlockchainList.mapNotNull {
cryptoCurrencyFactory.createCoin(
blockchain = it,
derivationStyleProvider = currentUserWallet.scanResponse.derivationStyleProvider,
)
},
userWallet = currentUserWallet,
),
)
}
private fun saveByOldWay(
currentTokensList: List<TokenWithBlockchain>,
currentBlockchainList: List<Blockchain>,
changedTokensList: MutableList<TokenWithBlockchain>,
changedBlockchainList: List<Blockchain>,
) {
val scanResponse = store.state.globalState.scanResponse ?: return
store.dispatch(
action = TokensAction.LegacySaveChanges(
currentTokens = currentTokensList,
currentBlockchains = currentBlockchainList,
changedTokens = changedTokensList,
changedBlockchains = changedBlockchainList,
scanResponse = scanResponse,
),
)
}
}

View file

@ -19,6 +19,10 @@ import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.supportedTokens import com.tangem.domain.common.extensions.supportedTokens
import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.fullNameWithoutTestnet
import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.common.extensions.getNetworkName import com.tangem.tap.common.extensions.getNetworkName
@ -26,14 +30,11 @@ import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor
import com.tangem.tap.features.tokens.impl.domain.models.Token import com.tangem.tap.features.tokens.impl.domain.models.Token
import com.tangem.tap.features.tokens.impl.domain.models.Token.Network import com.tangem.tap.features.tokens.impl.domain.models.Token.Network
import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState
import com.tangem.tap.features.tokens.impl.presentation.models.TokensListArgs
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
@ -43,9 +44,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import kotlin.properties.Delegates
import com.tangem.blockchain.common.Token as BlockchainToken import com.tangem.blockchain.common.Token as BlockchainToken
/** /**
@ -59,6 +62,7 @@ import com.tangem.blockchain.common.Token as BlockchainToken
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@Suppress("LongParameterList")
@HiltViewModel @HiltViewModel
internal class TokensListViewModel @Inject constructor( internal class TokensListViewModel @Inject constructor(
private val interactor: TokensListInteractor, private val interactor: TokensListInteractor,
@ -66,9 +70,12 @@ internal class TokensListViewModel @Inject constructor(
private val dispatchers: AppCoroutineDispatcherProvider, private val dispatchers: AppCoroutineDispatcherProvider,
private val reduxStateHolder: AppStateHolder, private val reduxStateHolder: AppStateHolder,
analyticsEventHandler: AnalyticsEventHandler, analyticsEventHandler: AnalyticsEventHandler,
getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
walletFeatureToggles: WalletFeatureToggles,
) : ViewModel(), DefaultLifecycleObserver { ) : ViewModel(), DefaultLifecycleObserver {
private val args = TokensListArgs() private val isManageAccess = store.state.tokensState.isManageAccess
private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler) private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler)
private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer()) private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer())
@ -76,15 +83,36 @@ internal class TokensListViewModel @Inject constructor(
var uiState by mutableStateOf(value = getInitialUiState()) var uiState by mutableStateOf(value = getInitialUiState())
private set private set
private val changedTokensList: MutableList<TokenWithBlockchain> = args.mainScreenTokenList.toMutableList() private var currentTokensList: List<TokenWithBlockchain> by Delegates.notNull()
private val changedBlockchainList: MutableList<Blockchain> = args.mainScreenBlockchainList.toMutableList() private var currentBlockchainList: List<Blockchain> by Delegates.notNull()
private var changedTokensList: MutableList<TokenWithBlockchain> by Delegates.notNull()
private var changedBlockchainList: MutableList<Blockchain> by Delegates.notNull()
private val tokensListMigration = TokensListMigration(
walletFeatureToggles = walletFeatureToggles,
getSelectedWalletUseCase = getSelectedWalletUseCase,
getCurrenciesUseCase = getCurrenciesUseCase,
)
init {
viewModelScope.launch(dispatchers.main) {
val (currentCoins, currentTokens) = tokensListMigration.getCurrentCryptoCurrencies()
currentBlockchainList = currentCoins
currentTokensList = currentTokens
changedBlockchainList = currentCoins.toMutableList()
changedTokensList = currentTokens.toMutableList()
}
}
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
if (args.isManageAccess) analyticsSender.sendWhenScreenOpened() if (isManageAccess) analyticsSender.sendWhenScreenOpened()
} }
private fun getInitialUiState(): TokensListStateHolder { private fun getInitialUiState(): TokensListStateHolder {
return if (args.isManageAccess) { return if (isManageAccess) {
TokensListStateHolder.ManageContent( TokensListStateHolder.ManageContent(
toolbarState = getInitialToolbarState(), toolbarState = getInitialToolbarState(),
isLoading = true, isLoading = true,
@ -105,7 +133,7 @@ internal class TokensListViewModel @Inject constructor(
} }
private fun getInitialToolbarState(): TokensListToolbarState { private fun getInitialToolbarState(): TokensListToolbarState {
return if (args.isManageAccess) { return if (isManageAccess) {
TokensListToolbarState.Title.Manage( TokensListToolbarState.Title.Manage(
titleResId = R.string.add_tokens_title, titleResId = R.string.add_tokens_title,
onBackButtonClick = actionsHandler::onBackButtonClick, onBackButtonClick = actionsHandler::onBackButtonClick,
@ -130,7 +158,7 @@ internal class TokensListViewModel @Inject constructor(
return interactor.getTokensList(searchText = searchText).map { return interactor.getTokensList(searchText = searchText).map {
it.map { token -> it.map { token ->
if (args.isManageAccess) createManageTokenContent(token) else createReadTokenContent(token) if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token)
} }
} }
} }
@ -264,7 +292,12 @@ internal class TokensListViewModel @Inject constructor(
fun onSaveButtonClick() { fun onSaveButtonClick() {
analyticsSender.sendWhenSaveButtonClicked() analyticsSender.sendWhenSaveButtonClicked()
store.dispatch(TokensAction.SaveChanges(changedTokensList, changedBlockchainList)) tokensListMigration.onSaveButtonClick(
currentTokensList = currentTokensList,
currentBlockchainList = currentBlockchainList,
changedTokensList = changedTokensList,
changedBlockchainList = changedBlockchainList,
)
} }
private fun onSearchValueChange(newValue: String) { private fun onSearchValueChange(newValue: String) {
@ -291,7 +324,7 @@ internal class TokensListViewModel @Inject constructor(
if (isRemoveAction) { if (isRemoveAction) {
val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain } val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain }
val isAddedOnMainScreen = args.mainScreenBlockchainList.contains(blockchain) val isAddedOnMainScreen = currentBlockchainList.contains(blockchain)
if (isTokenWithSameBlockchainFound) { if (isTokenWithSameBlockchainFound) {
router.openUnableHideMainTokenAlert( router.openUnableHideMainTokenAlert(
@ -341,7 +374,7 @@ internal class TokensListViewModel @Inject constructor(
val isRemoveAction = changedTokensList.contains(token) val isRemoveAction = changedTokensList.contains(token)
if (isRemoveAction) { if (isRemoveAction) {
val isAddedOnMainScreen = args.mainScreenTokenList.contains(token) val isAddedOnMainScreen = currentTokensList.contains(token)
if (isAddedOnMainScreen) { if (isAddedOnMainScreen) {
router.openRemoveWalletAlert( router.openRemoveWalletAlert(

View file

@ -1,23 +0,0 @@
package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.tap.domain.model.WalletDataModel
import org.rekotlin.Action
sealed interface TokensAction : Action {
/** Single way to pass data to the screen */
sealed interface SetArgs : TokensAction {
data class ManageAccess(val wallets: List<WalletDataModel>, val derivationStyle: DerivationStyle?) : SetArgs
object ReadAccess : SetArgs
}
// TODO: [REDACTED_TASK_KEY] Remove this action
data class SaveChanges(val tokens: List<TokenWithBlockchain>, val blockchains: List<Blockchain>) : TokensAction
// TODO: Remove this action in 4.7 release
object PrepareAndNavigateToAddCustomToken : TokensAction
}

View file

@ -9,29 +9,25 @@ import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap 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.navigation.NavigationAction
import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.common.util.supportsHdWallet import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.domainStore import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.* import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.rekotlin.Middleware import org.rekotlin.Middleware
@ -43,29 +39,69 @@ object TokensMiddleware {
{ next -> { next ->
{ action -> { action ->
when (action) { when (action) {
is TokensAction.SaveChanges -> handleSaveChanges(action) is TokensAction.LegacySaveChanges -> handleLegacySaveChanges(action)
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken() is TokensAction.NewSaveChanges -> handleNewSaveChanges(action)
} }
next(action) next(action)
} }
} }
} }
private fun handleSaveChanges(action: TokensAction.SaveChanges) { private fun handleNewSaveChanges(action: TokensAction.NewSaveChanges) {
scope.launch { scope.launch {
val scanResponse = store.state.globalState.scanResponse ?: return@launch val scanResponse = action.userWallet.scanResponse
val currentTokens = store.state.tokensState.addedTokens val currentTokens = action.currentTokens
val currentBlockchains = store.state.tokensState.addedBlockchains val currentBlockchains = action.currentCoins
val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains) val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains)
val blockchainsToRemove = val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains)
store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains)
val tokensToAdd = action.tokens.filterNot(currentTokens::contains) val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } } val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } }
removeCurrenciesIfNeeded( removeNewCurrenciesIfNeeded(
userWalletId = action.userWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return@launch
}
val currencyList = blockchainsToAdd + tokensToAdd
if (scanResponse.supportsHdWallet()) {
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
} else {
submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
}
}
private fun handleLegacySaveChanges(action: TokensAction.LegacySaveChanges) {
scope.launch {
val scanResponse = action.scanResponse
val currentTokens = action.currentTokens
val currentBlockchains = action.currentBlockchains
val blockchainsToAdd = action.changedBlockchains.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(action.changedBlockchains::contains)
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove =
currentTokens.filterNot { token -> action.changedTokens.any { it.token == token.token } }
removeLegacyCurrenciesIfNeeded(
currencies = convertToCurrencies( currencies = convertToCurrencies(
blockchains = blockchainsToRemove, blockchains = blockchainsToRemove,
tokens = tokensToRemove, tokens = tokensToRemove,
@ -89,11 +125,11 @@ object TokensMiddleware {
if (scanResponse.supportsHdWallet()) { if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, currencyList) { deriveMissingBlockchains(scanResponse, currencyList) {
submitAdd(it, currencyList) submitLegacyAdd(it, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo()) store.dispatchOnMain(NavigationAction.PopBackTo())
} }
} else { } else {
submitAdd(scanResponse, currencyList) submitLegacyAdd(scanResponse, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo()) store.dispatchOnMain(NavigationAction.PopBackTo())
} }
} }
@ -104,15 +140,14 @@ object TokensMiddleware {
tokens: List<TokenWithBlockchain>, tokens: List<TokenWithBlockchain>,
derivationStyle: DerivationStyle?, derivationStyle: DerivationStyle?,
): List<Currency> { ): List<Currency> {
return blockchains.map { return blockchains.map { Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) } +
Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) tokens.map {
} + tokens.map { Currency.Token(
Currency.Token( token = it.token,
it.token, blockchain = it.blockchain,
it.blockchain, derivationPath = it.blockchain.derivationPath(derivationStyle)?.rawPath,
it.blockchain.derivationPath(derivationStyle)?.rawPath, )
) }
}
} }
private fun deriveMissingBlockchains( private fun deriveMissingBlockchains(
@ -123,7 +158,7 @@ object TokensMiddleware {
val config = CardConfig.createConfig(scanResponse.card) val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull { val derivationDataList = currencyList.mapNotNull {
val curve = config.primaryCurve(it.blockchain) val curve = config.primaryCurve(it.blockchain)
curve?.let { getDerivations(curve, scanResponse, currencyList) } curve?.let { getLegacyDerivations(curve, scanResponse, currencyList) }
} }
val derivations = derivationDataList.associate { it.derivations } val derivations = derivationDataList.associate { it.derivations }
if (derivations.isEmpty()) { if (derivations.isEmpty()) {
@ -163,7 +198,55 @@ object TokensMiddleware {
} }
} }
private fun getDerivations( private fun deriveMissingCoins(
scanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
onSuccess: (ScanResponse) -> Unit,
) {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull {
config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value))
?.let { curve -> getNewDerivations(curve, scanResponse, currencyList) }
}
val derivations = derivationDataList.associate(DerivationData::derivations)
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
scope.launch {
val result = tangemSdkManager.derivePublicKeys(
cardId = null,
derivations = derivations,
)
when (result) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys,
)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
}
private fun getLegacyDerivations(
curve: EllipticCurve, curve: EllipticCurve,
scanResponse: ScanResponse, scanResponse: ScanResponse,
currencyList: List<Currency>, currencyList: List<Currency>,
@ -200,9 +283,48 @@ object TokensMiddleware {
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive) return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
} }
private fun getNewDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
): DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val manageTokensCandidates = currencyList
.map { Blockchain.fromId(it.network.id.value) }
.distinct()
.filter { it.getSupportedCurves().contains(curve) }
.mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) }
val customTokensCandidates = currencyList
.filter { Blockchain.fromId(it.network.id.value).getSupportedCurves().contains(curve) }
.mapNotNull(CryptoCurrency::derivationPath)
.map(::DerivationPath)
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
if (bothCandidates.isEmpty()) return null
currencyList.find { it is CryptoCurrency.Coin && Blockchain.fromId(it.network.id.value) == Blockchain.Cardano }
?.let { currency ->
currency.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 DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>) class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) { private fun submitLegacyAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to add currencies, no user wallet selected") Timber.e("Unable to add currencies, no user wallet selected")
return return
@ -223,7 +345,15 @@ object TokensMiddleware {
} }
} }
private suspend fun removeCurrenciesIfNeeded(currencies: List<Currency>) { private fun submitNewAdd(userWalletId: UserWalletId, currencyList: List<CryptoCurrency>) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
scope.launch {
currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList)
}
}
private suspend fun removeLegacyCurrenciesIfNeeded(currencies: List<Currency>) {
if (currencies.isEmpty()) return if (currencies.isEmpty()) return
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to remove currencies, no user wallet selected") Timber.e("Unable to remove currencies, no user wallet selected")
@ -232,55 +362,10 @@ object TokensMiddleware {
walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies) walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies)
} }
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean { private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
return currency.derivationPath?.let { if (currencies.isEmpty()) return
!scanResponse.hasDerivation(currency.blockchain, it) val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
} ?: false
}
private fun handleAddingCustomToken() = scope.launch { currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
val onAddCustomToken = fun(customCurrency: CustomCurrency) {
val scanResponse = store.state.globalState.scanResponse ?: return
fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List<Currency>) {
submitAdd(scanResponse, currencyList)
// pop from the AddCustomTokenScreen
store.dispatchOnMain(NavigationAction.PopBackTo())
store.dispatchOnMain(NavigationAction.PopBackTo())
}
Analytics.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency))
val currency = Currency.fromCustomCurrency(customCurrency)
val isNeedToDerive = isNeedToDerive(scanResponse, currency)
val currencyList = listOf(currency)
if (isNeedToDerive) {
deriveMissingBlockchains(scanResponse, currencyList) {
submitAndPopBack(it, currencyList)
}
} else {
submitAndPopBack(scanResponse, currencyList)
}
}
val addedCurrencies = store.state.walletState.walletsStores
.map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) }
.flatten()
.map { currency ->
when (currency) {
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(
currency.blockchain,
currency.derivationPath,
)
is Currency.Token -> DomainWrapped.Currency.Token(
currency.token,
currency.blockchain,
currency.derivationPath,
)
}
}
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken))
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
} }
} }

View file

@ -1,11 +1,7 @@
package com.tangem.tap.features.tokens.legacy.redux package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.common.Blockchain import com.tangem.domain.tokens.TokensAction
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.Currency.Token
import org.rekotlin.Action import org.rekotlin.Action
object TokensReducer { object TokensReducer {
@ -16,40 +12,8 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
if (action !is TokensAction) return state.tokensState if (action !is TokensAction) return state.tokensState
return when (action) { return when (action) {
is TokensAction.SetArgs.ManageAccess -> { is TokensAction.SetArgs.ManageAccess -> state.tokensState.copy(isManageAccess = true)
state.tokensState.copy( is TokensAction.SetArgs.ReadAccess -> state.tokensState.copy(isManageAccess = false)
isManageAccess = true,
addedWallets = action.wallets,
addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle),
addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle),
)
}
is TokensAction.SetArgs.ReadAccess -> {
state.tokensState.copy(isManageAccess = false)
}
else -> state.tokensState else -> state.tokensState
} }
} }
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return mapNotNull { walletDataModel ->
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) {
null
} else {
(walletDataModel.currency as? Currency.Blockchain)?.blockchain
}
}.distinct()
}
private fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
derivationStyle: DerivationStyle?,
): List<TokenWithBlockchain> {
return mapNotNull { walletDataModel ->
if (walletDataModel.currency !is Token) return@mapNotNull null
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain)
}.distinct()
}

View file

@ -1,16 +1,5 @@
package com.tangem.tap.features.tokens.legacy.redux package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.domain.model.WalletDataModel
import org.rekotlin.StateType import org.rekotlin.StateType
data class TokensState( data class TokensState(val isManageAccess: Boolean = false) : StateType
val isManageAccess: Boolean = false,
val addedWallets: List<WalletDataModel> = emptyList(),
val addedTokens: List<TokenWithBlockchain> = emptyList(),
val addedBlockchains: List<Blockchain> = emptyList(),
) : StateType
// TODO: [REDACTED_TASK_KEY] Remove this class
data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain)

View file

@ -1,13 +1,15 @@
package com.tangem.tap.features.wallet.converters package com.tangem.tap.features.wallet.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.models.CryptoCurrency import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.TwoWayConverter
internal class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> { internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrency> {
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() } private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
@ -40,4 +42,26 @@ internal class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
) )
} }
} }
override fun convertBack(value: CryptoCurrency): Currency {
val blockchain = Blockchain.fromId(value.network.id.value)
if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
return when (value) {
is CryptoCurrency.Coin -> Currency.Blockchain(
blockchain = blockchain,
derivationPath = value.derivationPath,
)
is CryptoCurrency.Token -> Currency.Token(
token = Token(
name = value.name,
symbol = value.symbol,
contractAddress = value.contractAddress,
decimals = value.decimals,
id = value.id.value,
),
blockchain = blockchain,
derivationPath = value.derivationPath,
)
}
}
} }

View file

@ -18,9 +18,11 @@ import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.tokens.getIconUrl import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
@ -39,7 +41,10 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
@Suppress("LargeClass")
class TradeCryptoMiddleware { class TradeCryptoMiddleware {
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun handle(state: () -> AppState?, action: TradeCryptoAction) { fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return if (DemoHelper.tryHandle(state, action)) return
@ -52,11 +57,10 @@ class TradeCryptoMiddleware {
openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency()) openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency())
} }
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action) is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send())
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action) is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
is TradeCryptoAction.New.Swap -> { is TradeCryptoAction.New.Swap -> openSwap(currency = action.cryptoCurrency.toSwapCurrency())
openSwap(currency = action.cryptoCurrency.toSwapCurrency()) is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action)
} is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action)
} }
} }
@ -301,4 +305,98 @@ class TradeCryptoMiddleware {
) )
} }
} }
private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) {
val cryptoStatus = action.tokenStatus
val currency = cryptoStatus.currency
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWallet = action.userWallet,
blockchain = blockchain,
derivationPath = blockchain.derivationPath(
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
),
)
if (walletManager == null) {
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type is AmountType.Token }
when (currency) {
is CryptoCurrency.Coin -> error("Action.tokenStatus.currency is Coin")
is CryptoCurrency.Token -> {
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = action.coinFiatRate,
tokenAmount = sendableAmounts.first(),
tokenRate = cryptoStatus.value.fiatRate,
),
)
}
}
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
}
}
private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) {
val cryptoStatus = action.coinStatus
val currency = cryptoStatus.currency
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
val walletManager = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWallet = action.userWallet,
blockchain = blockchain,
derivationPath = blockchain.derivationPath(
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
),
)
if (walletManager == null) {
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin }
when (currency) {
is CryptoCurrency.Coin -> {
val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol }
if (amountToSend == null) {
val error = TapError.UnsupportedState(stateError = "Amount to send is null")
FirebaseCrashlytics.getInstance()
.recordException(IllegalStateException(error.stateError))
store.dispatchErrorNotification(error)
return@launch
}
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
coinAmount = amountToSend,
coinRate = cryptoStatus.value.fiatRate,
),
)
}
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
}
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
}
}
} }

View file

@ -37,7 +37,6 @@ import com.tangem.tap.common.utils.SafeStoreSubscriber
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.statePrinter.printScanResponseState import com.tangem.tap.domain.statePrinter.printScanResponseState
import com.tangem.tap.domain.statePrinter.printWalletState import com.tangem.tap.domain.statePrinter.printWalletState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletAction
@ -311,17 +310,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
return when (item.itemId) { return when (item.itemId) {
R.id.details_menu -> { R.id.details_menu -> {
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
store.state.globalState.scanResponse?.let { scanResponse -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
store.dispatch(
DetailsAction.PrepareScreen( true
scanResponse = scanResponse,
wallets = store.state.walletState.walletManagers.map { it.wallet },
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true
}
false
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }

View file

@ -6,7 +6,7 @@ import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.TokensAction
import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.entities.FiatCurrency
@ -14,7 +14,6 @@ import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
import com.tangem.tap.features.wallet.redux.ErrorType import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState
@ -106,14 +105,8 @@ class MultiWalletView : WalletView() {
binding.btnAddToken.setOnClickListener { binding.btnAddToken.setOnClickListener {
Analytics.send(Portfolio.ButtonManageTokens()) Analytics.send(Portfolio.ButtonManageTokens())
store.dispatch( store.dispatch(action = TokensAction.SetArgs.ManageAccess)
TokensAction.SetArgs.ManageAccess( store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.AddTokens))
wallets = state.walletsDataFromStores,
derivationStyle = store.state.globalState.scanResponse
?.derivationStyleProvider?.getDerivationStyle(),
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
} }
handleErrorStates(state = state, binding = binding, fragment = fragment) handleErrorStates(state = state, binding = binding, fragment = fragment)
} }

View file

@ -0,0 +1,21 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.tap.features.wallet.converters.CryptoCurrencyConverter
class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager {
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
override fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForBuy(
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
override fun availableForSell(cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForSell(
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
}

View file

@ -14,6 +14,7 @@ import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.network.exchangeServices.ExchangeService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.rekotlin.Action import org.rekotlin.Action
@ -45,6 +46,7 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl
var tangemSdkManager: TangemSdkManager? = null var tangemSdkManager: TangemSdkManager? = null
var walletStoresManager: WalletStoresManager? = null var walletStoresManager: WalletStoresManager? = null
var appFiatCurrency: FiatCurrency = FiatCurrency.Default var appFiatCurrency: FiatCurrency = FiatCurrency.Default
var exchangeService: ExchangeService? = null
fun getActualCard(): CardDTO? { fun getActualCard(): CardDTO? {
return scanResponse?.card return scanResponse?.card

View file

@ -3,9 +3,11 @@ package com.tangem.tap.proxy.redux
import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
@ -16,6 +18,7 @@ import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.StateType import org.rekotlin.StateType
data class DaggerGraphState( data class DaggerGraphState(
@ -35,6 +38,11 @@ data class DaggerGraphState(
val cardSdkConfigRepository: CardSdkConfigRepository? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null,
val appCurrencyRepository: AppCurrencyRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null,
val walletManagersFacade: WalletManagersFacade? = null, val walletManagersFacade: WalletManagersFacade? = null,
val appStateHolder: AppStateHolder? = null,
val appThemeModeRepository: AppThemeModeRepository? = null,
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
val currenciesRepository: CurrenciesRepository? = null,
) : StateType { ) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T { inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 24% opacity --> <!-- 24% opacity -->
<item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_enabled="true" android:state_selected="true" /> <item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_enabled="true" android:state_selected="true" />
<item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_checked="true" android:state_enabled="true" /> <item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_checked="true" android:state_enabled="true" />
<item android:color="@color/backgroundLightGray" android:state_enabled="true" /> <item android:color="@color/background_secondary" android:state_enabled="true" />
<item android:color="@color/backgroundLightGray" /> <item android:color="@color/background_secondary" />
</selector> </selector>

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorSecondary" android:state_enabled="true" android:state_selected="true" /> <item android:color="@color/accent" android:state_enabled="true" android:state_selected="true" />
<item android:color="?attr/colorSecondary" android:state_checked="true" android:state_enabled="true" /> <item android:color="@color/accent" android:state_checked="true" android:state_enabled="true" />
<!-- 12% of 87% opacity -->
<item android:alpha="0.10" android:color="?attr/colorOnSurface" android:state_enabled="true" /> <item android:color="@color/text_secondary" android:state_enabled="true" />
<item android:alpha="0.12" android:color="?attr/colorOnSurface" /> <item android:color="@color/text_secondary" />
</selector> </selector>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/accent" android:state_enabled="true" android:state_selected="true" />
<item android:color="@color/accent" android:state_checked="true" android:state_enabled="true" />
<item android:color="@color/text_primary_1" android:state_enabled="true" />
<item android:color="@color/text_primary_1" />
</selector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/accent" android:state_focused="true" />
<item android:color="@color/text_secondary" />
</selector>

View file

@ -4,6 +4,6 @@
android:viewportWidth="14" android:viewportWidth="14"
android:viewportHeight="19"> android:viewportHeight="19">
<path <path
android:fillColor="@color/blue" android:fillColor="@color/accent"
android:pathData="M11,14.51V7.5H9V14.51H6L10,18.5L14,14.51H11ZM4,0.5L0,4.49H3V11.5H5V4.49H8L4,0.5Z" /> android:pathData="M11,14.51V7.5H9V14.51H6L10,18.5L14,14.51H11ZM4,0.5L0,4.49H3V11.5H5V4.49H8L4,0.5Z" />
</vector> </vector>

View file

@ -4,6 +4,6 @@
android:viewportWidth="16" android:viewportWidth="16"
android:viewportHeight="19"> android:viewportHeight="19">
<path <path
android:fillColor="#1C1C1E" android:fillColor="@color/text_primary_1"
android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z" /> android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z" />
</vector> </vector>

View file

@ -6,6 +6,6 @@
android:viewportHeight="19"> android:viewportHeight="19">
<path <path
android:fillColor="#99838383" android:fillColor="@color/text_disabled"
android:pathData="M13.8333 1.66667H10.35C10 0.7 9.08333 0 8 0C6.91667 0 6 0.7 5.65 1.66667H2.16667C1.25 1.66667 0.5 2.41667 0.5 3.33333V16.6667C0.5 17.5833 1.25 18.3333 2.16667 18.3333H13.8333C14.75 18.3333 15.5 17.5833 15.5 16.6667V3.33333C15.5 2.41667 14.75 1.66667 13.8333 1.66667ZM8 1.66667C8.45833 1.66667 8.83333 2.04167 8.83333 2.5C8.83333 2.95833 8.45833 3.33333 8 3.33333C7.54167 3.33333 7.16667 2.95833 7.16667 2.5C7.16667 2.04167 7.54167 1.66667 8 1.66667ZM13.8333 16.6667H2.16667V3.33333H3.83333V5.83333H12.1667V3.33333H13.8333V16.6667Z" /> android:pathData="M13.8333 1.66667H10.35C10 0.7 9.08333 0 8 0C6.91667 0 6 0.7 5.65 1.66667H2.16667C1.25 1.66667 0.5 2.41667 0.5 3.33333V16.6667C0.5 17.5833 1.25 18.3333 2.16667 18.3333H13.8333C14.75 18.3333 15.5 17.5833 15.5 16.6667V3.33333C15.5 2.41667 14.75 1.66667 13.8333 1.66667ZM8 1.66667C8.45833 1.66667 8.83333 2.04167 8.83333 2.5C8.83333 2.95833 8.45833 3.33333 8 3.33333C7.54167 3.33333 7.16667 2.95833 7.16667 2.5C7.16667 2.04167 7.54167 1.66667 8 1.66667ZM13.8333 16.6667H2.16667V3.33333H3.83333V5.83333H12.1667V3.33333H13.8333V16.6667Z" />
</vector> </vector>

View file

@ -5,5 +5,5 @@
android:viewportHeight="18"> android:viewportHeight="18">
<path <path
android:pathData="M1.0712,6.8139C1.5571,6.8139 1.8138,6.5481 1.8138,6.0622V3.8986C1.8138,2.9451 2.318,2.4593 3.2348,2.4593H5.4534C5.9392,2.4593 6.2051,2.1934 6.2051,1.7167C6.2051,1.2491 5.9392,0.9833 5.4534,0.9833H3.2164C1.3004,0.9833 0.3378,1.9275 0.3378,3.8161V6.0622C0.3378,6.5481 0.6036,6.8139 1.0712,6.8139ZM16.4271,6.8139C16.913,6.8139 17.1697,6.5481 17.1697,6.0622V3.8161C17.1697,1.9275 16.2071,0.9833 14.2911,0.9833H12.045C11.5682,0.9833 11.3024,1.2491 11.3024,1.7167C11.3024,2.1934 11.5682,2.4593 12.045,2.4593H14.2636C15.1712,2.4593 15.6937,2.9451 15.6937,3.8986V6.0622C15.6937,6.5481 15.9596,6.8139 16.4271,6.8139ZM8.3595,8.6016V5.4021C8.3595,5.1821 8.1854,4.9987 7.9562,4.9987H4.7566C4.5366,4.9987 4.3624,5.1821 4.3624,5.4021V8.6016C4.3624,8.8217 4.5366,8.9958 4.7566,8.9958H7.9562C8.1854,8.9958 8.3595,8.8217 8.3595,8.6016ZM9.9455,5.7963H12.3475V8.1983H9.9455V5.7963ZM11.6507,7.5015V6.5022H10.6423V7.5015H11.6507ZM6.856,7.5015V6.5022H5.8567V7.5015H6.856ZM5.1508,10.591H7.5619V12.993H5.1508V10.591ZM13.0259,10.9027V9.9035H12.0266V10.9027H13.0259ZM10.2664,10.9027V9.9035H9.2671V10.9027H10.2664ZM6.856,12.2871V11.2878H5.8567V12.2871H6.856ZM11.6416,12.2871V11.2878H10.6423V12.2871H11.6416ZM12.045,17.806H14.2911C16.2071,17.806 17.1697,16.8526 17.1697,14.964V12.7271C17.1697,12.2412 16.9039,11.9754 16.4271,11.9754C15.9504,11.9754 15.6937,12.2412 15.6937,12.7271V14.8907C15.6937,15.8441 15.1712,16.33 14.2636,16.33H12.045C11.5682,16.33 11.3024,16.5959 11.3024,17.0726C11.3024,17.5402 11.5682,17.806 12.045,17.806ZM3.2164,17.806H5.4534C5.9392,17.806 6.2051,17.5402 6.2051,17.0726C6.2051,16.5959 5.9392,16.33 5.4534,16.33H3.2348C2.318,16.33 1.8138,15.8441 1.8138,14.8907V12.7271C1.8138,12.2412 1.5479,11.9754 1.0712,11.9754C0.5945,11.9754 0.3378,12.2412 0.3378,12.7271V14.964C0.3378,16.8618 1.3004,17.806 3.2164,17.806ZM10.2664,13.6714V12.6721H9.2671V13.6714H10.2664ZM13.0259,13.6714V12.6721H12.0266V13.6714H13.0259ZM13.1451,8.6016V5.4021C13.1451,5.1821 12.9709,4.9987 12.7417,4.9987H9.5513C9.3222,4.9987 9.148,5.1821 9.148,5.4021V8.6016C9.148,8.8217 9.3222,8.9958 9.5513,8.9958H12.7417C12.9709,8.9958 13.1451,8.8217 13.1451,8.6016ZM5.1508,5.7963H7.5619V8.1983H5.1508V5.7963ZM8.3595,13.3872V10.1877C8.3595,9.9676 8.1854,9.7935 7.9562,9.7935H4.7566C4.5366,9.7935 4.3624,9.9676 4.3624,10.1877V13.3872C4.3624,13.6072 4.5366,13.7906 4.7566,13.7906H7.9562C8.1854,13.7906 8.3595,13.6072 8.3595,13.3872Z" android:pathData="M1.0712,6.8139C1.5571,6.8139 1.8138,6.5481 1.8138,6.0622V3.8986C1.8138,2.9451 2.318,2.4593 3.2348,2.4593H5.4534C5.9392,2.4593 6.2051,2.1934 6.2051,1.7167C6.2051,1.2491 5.9392,0.9833 5.4534,0.9833H3.2164C1.3004,0.9833 0.3378,1.9275 0.3378,3.8161V6.0622C0.3378,6.5481 0.6036,6.8139 1.0712,6.8139ZM16.4271,6.8139C16.913,6.8139 17.1697,6.5481 17.1697,6.0622V3.8161C17.1697,1.9275 16.2071,0.9833 14.2911,0.9833H12.045C11.5682,0.9833 11.3024,1.2491 11.3024,1.7167C11.3024,2.1934 11.5682,2.4593 12.045,2.4593H14.2636C15.1712,2.4593 15.6937,2.9451 15.6937,3.8986V6.0622C15.6937,6.5481 15.9596,6.8139 16.4271,6.8139ZM8.3595,8.6016V5.4021C8.3595,5.1821 8.1854,4.9987 7.9562,4.9987H4.7566C4.5366,4.9987 4.3624,5.1821 4.3624,5.4021V8.6016C4.3624,8.8217 4.5366,8.9958 4.7566,8.9958H7.9562C8.1854,8.9958 8.3595,8.8217 8.3595,8.6016ZM9.9455,5.7963H12.3475V8.1983H9.9455V5.7963ZM11.6507,7.5015V6.5022H10.6423V7.5015H11.6507ZM6.856,7.5015V6.5022H5.8567V7.5015H6.856ZM5.1508,10.591H7.5619V12.993H5.1508V10.591ZM13.0259,10.9027V9.9035H12.0266V10.9027H13.0259ZM10.2664,10.9027V9.9035H9.2671V10.9027H10.2664ZM6.856,12.2871V11.2878H5.8567V12.2871H6.856ZM11.6416,12.2871V11.2878H10.6423V12.2871H11.6416ZM12.045,17.806H14.2911C16.2071,17.806 17.1697,16.8526 17.1697,14.964V12.7271C17.1697,12.2412 16.9039,11.9754 16.4271,11.9754C15.9504,11.9754 15.6937,12.2412 15.6937,12.7271V14.8907C15.6937,15.8441 15.1712,16.33 14.2636,16.33H12.045C11.5682,16.33 11.3024,16.5959 11.3024,17.0726C11.3024,17.5402 11.5682,17.806 12.045,17.806ZM3.2164,17.806H5.4534C5.9392,17.806 6.2051,17.5402 6.2051,17.0726C6.2051,16.5959 5.9392,16.33 5.4534,16.33H3.2348C2.318,16.33 1.8138,15.8441 1.8138,14.8907V12.7271C1.8138,12.2412 1.5479,11.9754 1.0712,11.9754C0.5945,11.9754 0.3378,12.2412 0.3378,12.7271V14.964C0.3378,16.8618 1.3004,17.806 3.2164,17.806ZM10.2664,13.6714V12.6721H9.2671V13.6714H10.2664ZM13.0259,13.6714V12.6721H12.0266V13.6714H13.0259ZM13.1451,8.6016V5.4021C13.1451,5.1821 12.9709,4.9987 12.7417,4.9987H9.5513C9.3222,4.9987 9.148,5.1821 9.148,5.4021V8.6016C9.148,8.8217 9.3222,8.9958 9.5513,8.9958H12.7417C12.9709,8.9958 13.1451,8.8217 13.1451,8.6016ZM5.1508,5.7963H7.5619V8.1983H5.1508V5.7963ZM8.3595,13.3872V10.1877C8.3595,9.9676 8.1854,9.7935 7.9562,9.7935H4.7566C4.5366,9.7935 4.3624,9.9676 4.3624,10.1877V13.3872C4.3624,13.6072 4.5366,13.7906 4.7566,13.7906H7.9562C8.1854,13.7906 8.3595,13.6072 8.3595,13.3872Z"
android:fillColor="#000000"/> android:fillColor="@color/text_primary_1" />
</vector> </vector>

View file

@ -10,7 +10,7 @@
<clip-path android:pathData="M40 20C40 31.0457 31.0457 40 20 40C8.95431 40 0 31.0457 0 20C0 8.95431 8.95431 0 20 0C31.0457 0 40 8.95431 40 20Z" /> <clip-path android:pathData="M40 20C40 31.0457 31.0457 40 20 40C8.95431 40 0 31.0457 0 20C0 8.95431 8.95431 0 20 0C31.0457 0 40 8.95431 40 20Z" />
<path <path
android:fillColor="#F4F5F6" android:fillColor="@color/button_secondary"
android:pathData="M0 0V40H40V0" /> android:pathData="M0 0V40H40V0" />
</group> </group>

View file

@ -85,7 +85,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:elevation="18dp" android:elevation="18dp"
android:indeterminate="true" android:indeterminate="true"
android:indeterminateTint="@color/backgroundLightGray" android:indeterminateTint="@color/background_secondary"
android:visibility="invisible" /> android:visibility="invisible" />
</FrameLayout> </FrameLayout>

View file

@ -18,6 +18,7 @@
android:layout_gravity="center" android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless" android:background="?selectableItemBackgroundBorderless"
android:padding="5dp" android:padding="5dp"
app:srcCompat="@drawable/ic_angle_bracket_up" /> app:srcCompat="@drawable/ic_angle_bracket_up"
app:tint="@color/icon_primary_1" />
</FrameLayout> </FrameLayout>

View file

@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@color/backgroundWhite" android:background="@color/background_primary"
android:minHeight="420dp" android:minHeight="420dp"
tools:layout_gravity="bottom"> tools:layout_gravity="bottom">
@ -25,7 +25,7 @@
android:layout_width="32dp" android:layout_width="32dp"
android:layout_height="32dp" android:layout_height="32dp"
android:background="@drawable/shape_circle" android:background="@drawable/shape_circle"
android:backgroundTint="@color/backgroundWhite" android:backgroundTint="@color/background_primary"
app:layout_constraintBottom_toBottomOf="@id/iv_cross" app:layout_constraintBottom_toBottomOf="@id/iv_cross"
app:layout_constraintEnd_toEndOf="@id/iv_cross" app:layout_constraintEnd_toEndOf="@id/iv_cross"
app:layout_constraintStart_toStartOf="@id/iv_cross" app:layout_constraintStart_toStartOf="@id/iv_cross"

View file

@ -11,7 +11,7 @@
android:layout_height="56dp" android:layout_height="56dp"
android:padding="16dp" android:padding="16dp"
android:text="@string/wallet_choose_trade_action" android:text="@string/wallet_choose_trade_action"
android:textColor="@color/darkGray2" android:textColor="@color/text_secondary"
android:textSize="14sp" /> android:textSize="14sp" />
<TextView <TextView
@ -24,7 +24,7 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:padding="16dp" android:padding="16dp"
android:text="@string/common_buy" android:text="@string/common_buy"
android:textColor="@color/darkGray3" android:textColor="@color/text_primary_1"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_arrow_up_24" /> app:drawableStartCompat="@drawable/ic_arrow_up_24" />
@ -39,7 +39,7 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:padding="16dp" android:padding="16dp"
android:text="@string/common_sell" android:text="@string/common_sell"
android:textColor="@color/darkGray3" android:textColor="@color/text_primary_1"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_arrow_down_24" /> app:drawableStartCompat="@drawable/ic_arrow_down_24" />
@ -54,7 +54,7 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:padding="16dp" android:padding="16dp"
android:text="@string/swapping_swap_action" android:text="@string/swapping_swap_action"
android:textColor="@color/darkGray3" android:textColor="@color/text_primary_1"
android:textSize="14sp" android:textSize="14sp"
android:textStyle="bold" android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_exchange_vertical_24" /> app:drawableStartCompat="@drawable/ic_exchange_vertical_24" />

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