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

View file

@ -1,40 +0,0 @@
package com.tangem.tap.common
import timber.log.Timber
class CompositionCounter(
val id: String,
count: Int = 0,
) {
var count: Int = count
private set
fun increase(id: String): CompositionCounter {
if (this.id != id) return this
count += 1
return CompositionCounter(id, count)
}
}
class CompositionLogger(
private val recomposeViewId: String,
private val tag: String = recomposeViewId,
private var turnOnForIds: List<String> = listOf(recomposeViewId),
) {
val count: Int
get() = compositionCounter.count
private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId)
fun nextComposition() {
compositionCounter = compositionCounter.increase(recomposeViewId)
log("")
}
fun log(message: String) {
if (!turnOnForIds.contains(recomposeViewId)) return
Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message")
}
}

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.analytics.topup.TopUpController
@ -103,4 +104,5 @@ sealed class GlobalAction : Action {
}
data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
}

View file

@ -131,6 +131,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
sellService = makeSellExchangeService(config),
primaryRules = CardExchangeRules(cardProvider),
)
// TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance)
store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
}

View file

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

View file

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

View file

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

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.domain
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -108,8 +110,32 @@ internal object TokensDomainModule {
@Provides
@ViewModelScoped
fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(dispatchers)
return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers)
}
@Provides
@ViewModelScoped
fun provideGetCurrencyStatusByNetworkUseCase(
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetNetworkCoinStatusUseCase {
return GetNetworkCoinStatusUseCase(
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
dispatchers = dispatchers,
)
}
@Provides
@ViewModelScoped
fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
}
}

View file

@ -1,105 +0,0 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
class LoadAvailableCoinsService(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
private val assetReader: AssetReader,
) {
private val currenciesAdapter: JsonAdapter<CurrenciesFromJson> =
MoshiConverter.networkMoshi.adapter(CurrenciesFromJson::class.java)
suspend fun getSupportedTokens(
isTestNet: Boolean,
supportedBlockchains: List<Blockchain>,
page: Int,
searchInput: String?,
): Result<LoadedCoins> {
if (isTestNet) {
return Result.Success(
LoadedCoins(
currencies = getTestnetCoins().filter(searchInput),
moreAvailable = false,
),
)
}
val offset = page * LOAD_PER_PAGE
return when (val result = loadCoins(supportedBlockchains, offset, searchInput)) {
is Result.Success -> {
val data = result.data
Result.Success(
LoadedCoins(
currencies = data.coins.map {
Currency.fromCoinResponse(currency = it, imageHost = data.imageHost)
},
moreAvailable = data.total > offset + LOAD_PER_PAGE,
),
)
}
is Result.Failure -> {
Result.Failure(result.error)
}
}
}
private suspend fun loadCoins(
supportedBlockchains: List<Blockchain>,
offset: Int,
searchInput: String?,
): Result<CoinsResponse> {
return withContext(dispatchers.io) {
runCatching {
tangemTechApi.getCoins(
networkIds = supportedBlockchains.joinToString(
separator = ",",
transform = Blockchain::toNetworkId,
),
active = true,
searchText = searchInput,
offset = offset,
limit = LOAD_PER_PAGE,
)
}.fold(
onSuccess = { Result.Success(it) },
onFailure = { Result.Failure(it) },
)
}
}
private fun getTestnetCoins(): List<Currency> {
val json = assetReader.readJson(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) }
}
private fun List<Currency>.filter(searchInput: String?): List<Currency> {
if (searchInput.isNullOrBlank()) return this
return filter { currency ->
currency.symbol.contains(searchInput, ignoreCase = true) ||
currency.name.contains(searchInput, ignoreCase = true)
}
}
private companion object {
const val LOAD_PER_PAGE = 100
const val FILE_NAME_TESTNET_COINS = "testnet_tokens"
}
}
data class LoadedCoins(
val currencies: List<Currency>,
val moreAvailable: Boolean,
)

View file

@ -7,8 +7,5 @@ package com.tangem.tap.features.customtoken.api.featuretoggles
*/
interface CustomTokenFeatureToggles {
/** Availability of redesigned screen (internal feature) */
val isRedesignedScreenEnabled: Boolean
val isNewCardScanningEnabled: Boolean
}

View file

@ -14,9 +14,6 @@ internal class DefaultCustomTokenFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : CustomTokenFeatureToggles {
override val isRedesignedScreenEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED")
override val isNewCardScanningEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
}

View file

@ -1,79 +0,0 @@
package com.tangem.tap.features.customtoken.legacy
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.appcompat.widget.Toolbar
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.customtoken.legacy.compose.AddCustomTokenScreen
import com.tangem.tap.features.customtoken.legacy.compose.ClosePopupTrigger
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber<AddCustomTokenState> {
private var state: MutableState<AddCustomTokenState> = mutableStateOf(domainStore.state.addCustomTokensState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(ManageTokens.CustomToken.ScreenOpened)
}
override fun subscribeToStore() {
domainStore.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.addCustomTokensState == newState.addCustomTokensState
}.select { it.addCustomTokensState }
}
}
override fun newState(state: AddCustomTokenState) {
if (activity == null || view == null) return
this.state.value = state
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
view.findViewById<Toolbar>(R.id.toolbar)?.setTitle(R.string.add_custom_token_title)
val closePopupTrigger = initClosingPopupTriggerEvent()
view.findViewById<ComposeView>(R.id.view_compose)?.setContent {
TangemTheme {
Box(
modifier = Modifier
.fillMaxSize(),
) {
AddCustomTokenScreen(state, closePopupTrigger)
}
}
}
}
private fun initClosingPopupTriggerEvent(): ClosePopupTrigger = ClosePopupTrigger().apply {
onCloseComplete = ::handleOnBackPressed
addBackPressHandler(
object : FragmentOnBackPressedHandler {
override fun handleOnBackPressed() = close()
},
)
}
}

View file

@ -1,208 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.common.form.DataField
import com.tangem.domain.common.form.FieldId
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.features.addCustomToken.redux.ScreenState
import com.tangem.domain.features.addCustomToken.redux.ViewStates
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.AddCustomTokenWarning
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCase
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList
import com.tangem.wallet.R
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) }
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed),
)
val coroutineScope = rememberCoroutineScope()
val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } }
BottomSheetScaffold(
scaffoldState = bottomSheetScaffoldState,
sheetContent = {
Surface(color = colorResource(id = R.color.lightGray5)) {
selectedTestCase.value.content(toggleBottomSheet)
}
},
sheetPeekHeight = 0.dp,
) {
Column {
TestCasesList(
onItemClick = {
selectedTestCase.value = it
toggleBottomSheet()
},
)
ScreenContent(state, closePopupTrigger)
}
}
ComposeDialogManager()
LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) })
DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } })
}
@OptIn(ExperimentalMaterialApi::class)
private suspend fun BottomSheetScaffoldState.toggle() {
if (bottomSheetState.isCollapsed) {
bottomSheetState.expand()
} else {
bottomSheetState.collapse()
}
}
@Composable
private fun ScreenContent(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
backgroundColor = colorResource(id = R.color.backgroundLightGray),
floatingActionButton = {
HangingOverKeyboardView(keyboardState = keyboardAsState()) {
AddButton(state)
}
},
floatingActionButtonPosition = FabPosition.Center,
) { paddings ->
Box(
modifier = Modifier
.padding(paddings)
.fillMaxSize(),
) {
LazyColumn(
contentPadding = PaddingValues(bottom = 90.dp),
) {
item {
Surface(
modifier = Modifier.padding(16.dp),
shape = MaterialTheme.shapes.small,
elevation = 4.dp,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
FormFields(state, closePopupTrigger)
}
}
}
item { Warnings(state.value.warnings.toList()) }
}
}
}
}
@Composable
private fun FormFields(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
val context = LocalContext.current
val errorConverter = remember { ModuleMessageConverter(context) }
val stateValue = state.value
stateValue.form.fieldList.forEach { field ->
val data = ScreenFieldData.fromState(field, stateValue, errorConverter)
when (field.id) {
ContractAddress -> TokenContractAddressView(data)
Network -> TokenNetworkView(data, stateValue, closePopupTrigger)
Name -> TokenNameView(data)
Symbol -> TokenSymbolView(data)
Decimals -> TokenDecimalsView(data)
DerivationPath -> TokenDerivationPathView(data, stateValue, closePopupTrigger)
}
}
}
@Composable
fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
if (warnings.isEmpty()) return
val context = LocalContext.current
val warningConverter = remember { ModuleMessageConverter(context) }
Column {
warnings.forEachIndexed { index, item ->
val modifier = when (index) {
0 -> Modifier.padding(vertical = 0.dp)
warnings.lastIndex -> Modifier.padding(top = 8.dp, bottom = 16.dp)
else -> Modifier.padding(top = 8.dp, bottom = 0.dp)
}
AddCustomTokenWarning(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
warning = item,
converter = warningConverter,
)
}
}
}
@Composable
private fun AddButton(state: MutableState<AddCustomTokenState>) {
PrimaryButtonIconStart(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
text = stringResource(id = R.string.common_add),
iconResId = R.drawable.ic_plus_24,
enabled = state.value.screenState.addButton.isEnabled,
onClick = { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) },
)
}
data class ScreenFieldData(
val field: DataField<*>,
val error: AddCustomTokenError?,
val errorConverter: ModuleMessageConverter,
val viewState: ViewStates.TokenField,
) {
companion object {
fun fromState(
field: DataField<*>,
state: AddCustomTokenState,
errorConverter: ModuleMessageConverter,
): ScreenFieldData {
return ScreenFieldData(
field = field,
error = state.getError(field.id),
errorConverter = errorConverter,
viewState = selectField(field.id, state.screenState),
)
}
private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField {
return when (id) {
ContractAddress -> screenState.contractAddressField
Network -> screenState.network
Name -> screenState.name
Symbol -> screenState.symbol
Decimals -> screenState.decimals
DerivationPath -> screenState.derivationPath
else -> throw UnsupportedOperationException()
}
}
}
}

View file

@ -1,33 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.form.Field
@Composable
fun BlockchainSpinner(
@StringRes title: Int,
itemList: List<Blockchain>,
selectedItem: Field.Data<Blockchain>,
isEnabled: Boolean = true,
textFieldConverter: (Blockchain) -> String,
dropdownItemView: @Composable ((Blockchain) -> Unit)? = null,
closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(),
onItemSelect: (Blockchain) -> Unit,
) {
OutlinedSpinner(
modifier = Modifier.fillMaxWidth(),
label = stringResource(id = title),
itemList = itemList,
selectedItem = selectedItem,
textFieldConverter = textFieldConverter,
dropdownItemView = dropdownItemView,
isEnabled = isEnabled,
onItemSelected = onItemSelect,
closePopupTrigger = closePopupTrigger,
)
}

View file

@ -1,157 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.AlertDialog
import androidx.compose.material.Button
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.res.TangemTheme
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.DomainDialog
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.domain.redux.global.DomainGlobalState
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@Composable
internal fun ComposeDialogManager() {
val dialogSate = remember { mutableStateOf<DomainDialog?>(null) }
val subscriber = remember {
object : StoreSubscriber<DomainGlobalState> {
override fun newState(state: DomainGlobalState) {
dialogSate.value = state.dialog
}
}
}
ShowTheDialog(dialogSate)
LaunchedEffect(
key1 = Unit,
block = {
domainStore.subscribe(subscriber) { state ->
state.skipRepeats { oldState, newState ->
oldState.globalState == newState.globalState
}.select { it.globalState }
}
},
)
DisposableEffect(
key1 = Unit,
effect = {
onDispose { domainStore.unsubscribe(subscriber) }
},
)
}
@Composable
private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
if (dialogState.value == null) return
val context = LocalContext.current
val errorConverter = remember { ModuleMessageConverter(context) }
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
when (val dialog = dialogState.value) {
is DomainDialog.DialogError -> ErrorDialog(
title = stringResource(id = R.string.common_error),
body = errorConverter.convert(dialog.error).message,
onDismissRequest,
)
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
else -> {}
}
}
/**
* Dialog with single item selection
*/
@Composable
fun SimpleDialog(
title: String,
items: List<CoinsResponse.Coin.Network>,
onSelect: (CoinsResponse.Coin.Network) -> Unit,
onDismissRequest: () -> Unit,
itemContent: @Composable (CoinsResponse.Coin.Network) -> Unit,
) {
Dialog(
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
onDismissRequest = { },
) {
Surface(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) {
Column(modifier = Modifier.padding(TangemTheme.dimens.spacing22)) {
DialogTitle(title = title)
LazyColumn {
items(items = items, key = CoinsResponse.Coin.Network::networkId) { item ->
Row(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clickable {
onSelect(item)
onDismissRequest()
},
verticalAlignment = Alignment.CenterVertically,
) { itemContent(item) }
}
}
}
}
}
}
@Composable
private fun DialogTitle(title: String) {
Text(
text = title,
style = LocalTextStyle.provides(
TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
),
).value,
)
SpacerH16()
}
@Composable
fun ErrorDialog(title: String, body: String, onDismissRequest: () -> Unit) {
AlertDialog(
title = { DialogTitle(title) },
text = { Text(body) },
onDismissRequest = onDismissRequest,
confirmButton = {
Button(onClick = onDismissRequest) {
Text(text = stringResource(id = R.string.common_ok))
}
},
)
}

View file

@ -1,149 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.components.SpacerH8
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
import com.tangem.domain.features.addCustomToken.TokenField
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
import com.tangem.tap.common.compose.TitleSubtitle
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
fieldData = tokenField.data,
labelId = R.string.custom_token_contract_address_input_title,
placeholder = "0x0000000000000000000000000000000000000000",
isEnabled = screenFieldData.viewState.isEnabled,
isLoading = screenFieldData.viewState.isLoading,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) }
) {
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true)))
}
SpacerH8()
}
@Composable
fun TokenNameView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
fieldData = tokenField.data,
labelId = R.string.custom_token_name_input_title,
placeholderId = R.string.custom_token_name_input_placeholder,
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) {
domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true)))
}
SpacerH8()
}
@Composable
fun TokenNetworkView(
screenFieldData: ScreenFieldData,
state: AddCustomTokenState,
closePopupTrigger: ClosePopupTrigger,
) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected)
val networkField = screenFieldData.field as TokenBlockchainField
BlockchainSpinner(
title = R.string.custom_token_network_input_title,
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
closePopupTrigger = closePopupTrigger,
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) }
SpacerH8()
}
@Composable
fun TokenSymbolView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
fieldData = tokenField.data,
labelId = R.string.custom_token_token_symbol_input_title,
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) }
SpacerH8()
}
@Composable
fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
if (!screenFieldData.viewState.isVisible) return
val tokenField = screenFieldData.field as TokenField
OutlinedTextFieldWidget(
fieldData = tokenField.data,
labelId = R.string.custom_token_decimals_input_title,
placeholder = "8",
isEnabled = screenFieldData.viewState.isEnabled,
error = screenFieldData.error,
errorConverter = screenFieldData.errorConverter,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) }
SpacerH8()
}
@Composable
fun TokenDerivationPathView(
screenFieldData: ScreenFieldData,
state: AddCustomTokenState,
closePopupTrigger: ClosePopupTrigger,
) {
if (!screenFieldData.viewState.isVisible) return
val notSelected = stringResource(id = R.string.custom_token_derivation_path_default)
val networkField = screenFieldData.field as TokenDerivationPathField
BlockchainSpinner(
title = R.string.custom_token_derivation_path_input_title,
itemList = networkField.itemList,
selectedItem = networkField.data,
isEnabled = screenFieldData.viewState.isEnabled,
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
dropdownItemView = { blockchain ->
val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected
val blockchainName = state.blockchainToName(blockchain) ?: notSelected
TitleSubtitle(derivationPathName, blockchainName)
},
closePopupTrigger = closePopupTrigger,
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) }
SpacerH8()
}

View file

@ -1,32 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.Keyboard
/**
[REDACTED_AUTHOR]
*/
@Composable
fun HangingOverKeyboardView(
modifier: Modifier = Modifier,
keyboardState: State<Keyboard>,
spaceBetweenKeyboard: Dp = 0.dp,
content: @Composable (BoxScope.() -> Unit),
) {
val padding = remember(keyboardState) {
when (val state = keyboardState.value) {
is Keyboard.Closed -> 0.dp
is Keyboard.Opened -> state.height + spaceBetweenKeyboard
}
}
Box(modifier.padding(bottom = padding)) { content() }
}

View file

@ -1,103 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose
import android.os.Handler
import android.os.Looper
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.os.postDelayed
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.form.Field
import com.tangem.tap.common.compose.TangemTextFieldsDefault
import com.tangem.tap.common.extensions.ValueCallback
/**
[REDACTED_AUTHOR]
*/
@Suppress("MagicNumber")
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun <T> OutlinedSpinner(
label: String,
itemList: List<T>,
selectedItem: Field.Data<T>,
onItemSelected: ValueCallback<T>,
modifier: Modifier = Modifier,
textFieldConverter: (T) -> String = { it.toString() },
dropdownItemView: @Composable ((T) -> Unit)? = null,
isEnabled: Boolean = true,
onClose: VoidCallback = {},
closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(),
) {
val rIsExpanded = remember { mutableStateOf(false) }
val stateSelectedItem = remember { mutableStateOf(selectedItem.value) }
if (!selectedItem.isUserInput) {
stateSelectedItem.value = selectedItem.value
}
val onDropDownItemSelectedInternal: (T) -> Unit = {
stateSelectedItem.value = it
rIsExpanded.value = false
onItemSelected(it)
}
val onDismissRequest = {
rIsExpanded.value = false
onClose()
}
closePopupTrigger.close = {
onDismissRequest()
Handler(Looper.getMainLooper()).postDelayed(100) {
closePopupTrigger.onCloseComplete()
}
}
ExposedDropdownMenuBox(
expanded = rIsExpanded.value,
onExpandedChange = { rIsExpanded.value = !rIsExpanded.value },
) {
OutlinedTextField(
modifier = modifier,
readOnly = true,
enabled = isEnabled,
value = textFieldConverter(stateSelectedItem.value),
onValueChange = {},
label = { Text(label) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
if (isEnabled) {
ExposedDropdownMenu(expanded = rIsExpanded.value, onDismissRequest = onDismissRequest) {
itemList.forEach { item ->
key(item) {
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
if (dropdownItemView == null) Text(textFieldConverter(item)) else dropdownItemView(item)
}
}
}
}
}
}
}
class ClosePopupTrigger {
var close: () -> Unit = {}
var onCloseComplete: () -> Unit = {}
}
@Preview
@Composable
private fun TestSpinnerPreview() {
OutlinedSpinner(
label = "Blockchain name",
itemList = listOf(Blockchain.values()),
selectedItem = Field.Data(Blockchain.Avalanche, false),
onItemSelected = {},
)
}

View file

@ -1,20 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.domain.DomainDialog
import com.tangem.tap.common.compose.TitleSubtitle
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) {
SimpleDialog(
title = stringResource(id = R.string.custom_token_network_input_title),
items = dialog.items,
onSelect = dialog.onSelect,
onDismissRequest = onDismissRequest,
) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") }
}

View file

@ -1,120 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose.test
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Divider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.form.Field
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore
/**
[REDACTED_AUTHOR]
*/
@Composable
fun ContractAddressTests(onItemClick: VoidCallback) {
val casesInfo = listOf(
"USDC on ETH" to "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"BUSD on ETH" to "0x4fabb145d64652a948d72533023f6e7a623c7c53",
"ETH on AVALANCHE" to "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15",
"USDC on ETH (invalid - cut address)" to "0xa0b86991c6218b36c1d1",
"Custom EVM" to "0x1111111111111111112111111111111111111113",
"Supported by several networks" to "0xa1faa113cbe53436df28ff0aee54275c13b40975",
"Invalid" to "!@#_ _-%%^&&*((){P P2iOWsdfFQLA",
)
CasesListContent(casesInfo, onItemClick)
}
@Composable
fun SolanaAddressTests(onItemClick: VoidCallback) {
val casesInfo = listOf(
"USDT (full)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
"USDT (valid - 2/3 of address)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben",
"USDT (invalid - 1/3 of address)" to "Es9vMFrzaCERmJ",
"ETH (full)" to "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
)
CasesListContent(casesInfo, onItemClick)
}
@Composable
private fun CasesListContent(casesList: List<Pair<String, String>>, onItemClick: VoidCallback) {
LazyColumn(
content = {
item {
Row {
ResetContractAddressButton(onItemClick)
Text("", modifier = Modifier.weight(1f))
ResetAllFieldsButton(onItemClick)
}
Divider()
}
items(casesList.size) {
val (info, address) = casesList[it]
ContractAddressButton(info, address, onItemClick)
}
},
)
}
@Composable
fun ResetAllFieldsButton(onItemClick: VoidCallback) {
ActionButton(
name = "Reset",
onClick = {
onItemClick()
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false)))
domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false)))
domainStore.dispatch(
AddCustomTokenAction.OnTokenDerivationPathChanged(
Field.Data(
Blockchain.Unknown,
false,
),
),
)
},
)
}
@Composable
fun ResetContractAddressButton(onItemClick: VoidCallback) {
ActionButton(
name = "Set empty address",
onClick = {
onItemClick()
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false)))
},
)
}
@Composable
private fun ContractAddressButton(name: String, address: String, onItemClick: VoidCallback) {
ActionButton(
modifier = Modifier.fillMaxWidth(),
name = name,
onClick = {
onItemClick()
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false)))
},
)
}
@Composable
fun ActionButton(name: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Button(
modifier = modifier.padding(horizontal = 8.dp),
onClick = onClick,
) { Text(name, fontSize = 12.sp) }
}

View file

@ -1,53 +0,0 @@
package com.tangem.tap.features.customtoken.legacy.compose.test
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
import com.tangem.common.extensions.VoidCallback
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun TestCasesList(onItemClick: (TestCase) -> Unit) {
if (!BuildConfig.TEST_ACTION_ENABLED) return
Surface(color = colorResource(id = R.color.lightGray5)) {
Column(Modifier.padding(horizontal = 16.dp)) {
listOf(TestCase.ContractAddress, TestCase.SolanaTokens)
.map { case -> TestCaseListItem(testCase = case, onItemClick = { onItemClick(case) }) }
}
}
}
@Composable
fun TestCaseListItem(testCase: TestCase, onItemClick: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
text = testCase.description,
)
Button(
onClick = onItemClick,
) { Text("Start") }
}
}
enum class TestCase(val description: String, val content: @Composable (VoidCallback) -> Unit) {
ContractAddress("Test contract address field", { ContractAddressTests(it) }),
Auto("Test contract address field", { ContractAddressTests(it) }),
SolanaTokens("Test Solana contract addresses", { SolanaAddressTests(it) }),
;
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.Action
@ -11,7 +11,6 @@ sealed class DetailsAction : Action {
data class PrepareScreen(
val scanResponse: ScanResponse,
val wallets: List<Wallet>,
) : DetailsAction()
object ReCreateTwinsWallet : DetailsAction()
@ -70,6 +69,10 @@ sealed class DetailsAction : Action {
data class BiometricsStatusChanged(
val needEnrollBiometrics: Boolean,
) : AppSettings()
data class ChangeAppThemeMode(
val appThemeMode: AppThemeMode,
) : AppSettings()
}
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.navigation.AppScreen
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.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
@ -221,6 +222,9 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.EnrollBiometrics -> {
enrollBiometrics()
}
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
changeAppThemeMode(action.appThemeMode)
}
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
@ -252,6 +256,14 @@ class DetailsMiddleware {
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 {
// Nothing to change
if (preferencesStorage.shouldSaveUserWallets == enable) {

View file

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

View file

@ -1,20 +1,18 @@
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.ScanResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.StateType
import java.util.*
import java.util.EnumSet
data class DetailsState(
val scanResponse: ScanResponse? = null,
val wallets: List<Wallet> = emptyList(),
val cardSettingsState: CardSettingsState? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default,
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
@ -57,6 +55,8 @@ data class AppSettingsState(
val isBiometricsAvailable: Boolean = false,
val needEnrollBiometrics: Boolean = false,
val isInProgress: Boolean = false,
val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default,
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
)
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
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.transition.TransitionInflater
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val viewModel = AppSettingsViewModel(store)
private var screenState: MutableState<AppSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
viewModel.checkBiometricsStatus()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
TangemTheme {
@Composable
override fun ScreenContent(modifier: Modifier) {
AppSettingsScreen(
state = screenState.value,
modifier = modifier,
state = viewModel.uiState,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
store.dispatch(NavigationAction.PopBackTo())
},
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.checkBiometricsStatus()
}
}
override fun TransitionInflater.inflateTransitions(): Boolean {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
return true
}
override fun onStart() {
@ -68,6 +69,6 @@ class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
override fun newState(state: DetailsState) {
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
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW32
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard
import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.tap.features.details.ui.appsettings.components.*
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.tap.features.details.ui.common.TangemSwitch
import com.tangem.wallet.R
import kotlinx.collections.immutable.persistentListOf
@Composable
fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit) {
internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
content = { AppSettings(state = state) },
modifier = modifier,
content = {
when (state) {
is AppSettingsScreenState.Content -> AppSettings(state = state)
is AppSettingsScreenState.Loading -> Unit
}
},
titleRes = R.string.app_settings_title,
onBackClick = onBackClick,
)
}
@Composable
private fun AppSettings(state: AppSettingsScreenState) {
var dialogType by remember { mutableStateOf<AppSetting?>(null) }
val onDialogStateChange: (AppSetting?) -> Unit = { dialogType = it }
dialogType?.let {
SettingsAlertDialog(
element = it,
onDialogStateChange = onDialogStateChange,
onSettingToggle = { state.onSettingToggled(it, false) },
)
private fun AppSettings(state: AppSettingsScreenState.Content) {
val dialog by rememberUpdatedState(newValue = state.dialog)
when (val safeDialog = dialog) {
is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog)
is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog)
null -> Unit
}
Column(modifier = Modifier.fillMaxSize()) {
if (state.showEnrollBiometricsCard) {
EnrollBiometricsCard(onClick = state.onEnrollBiometrics)
SpacerH24()
}
AppSettingsElement(
state = state,
setting = AppSetting.SaveWallets,
onDialogStateChange = onDialogStateChange,
LazyColumn {
items(
items = state.items,
key = Item::id,
) { item ->
when (item) {
is Item.Card -> SettingsCardItem(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
item = item,
)
SpacerH32()
AppSettingsElement(
state = state,
setting = AppSetting.SaveAccessCode,
onDialogStateChange = onDialogStateChange,
is Item.Button -> SettingsButtonItem(
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
item = item,
)
is Item.Switch -> SettingsSwitchItem(
modifier = Modifier.padding(
vertical = TangemTheme.dimens.spacing16,
horizontal = TangemTheme.dimens.spacing20,
),
item = item,
)
}
}
@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,
)
},
)
}
}
private fun onCheckedChange(
element: AppSetting,
enabled: Boolean,
onSettingToggled: (AppSetting, Boolean) -> Unit,
onDialogStateChange: (AppSetting?) -> Unit,
) {
// Show warning if user wants to disable the switch
if (!enabled) {
onDialogStateChange(element)
} else {
onSettingToggled(element, true)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreenSample(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
private fun AppSettingsScreenPreview_Light(
@PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState,
) {
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
AppSetting.SaveWallets to true,
AppSetting.SaveAccessCode to false,
),
showEnrollBiometricsCard = false,
isTogglesEnabled = true,
onSettingToggled = { _, _ -> },
onEnrollBiometrics = {},
),
onBackClick = { },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreenPreview_Light() {
TangemTheme {
AppSettingsScreenSample()
AppSettingsScreen(state = state, onBackClick = {})
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreenPreview_Dark() {
private fun AppSettingsScreenPreview_Dark(
@PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState,
) {
TangemTheme(isDark = true) {
AppSettingsScreenSample()
AppSettingsScreen(state = state, onBackClick = {})
}
}
@Composable
private fun AppSettingsScreen_EnrollBiometrics_Sample(modifier: Modifier = Modifier) {
Column(modifier = modifier.background(TangemTheme.colors.background.primary)) {
AppSettingsScreen(
state = AppSettingsScreenState(
settings = mapOf(
AppSetting.SaveWallets to true,
AppSetting.SaveAccessCode to false,
),
showEnrollBiometricsCard = true,
isTogglesEnabled = false,
onSettingToggled = { _, _ -> },
onEnrollBiometrics = {},
),
onBackClick = { },
private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider<AppSettingsScreenState>(
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
val items = persistentListOf(
itemsFactory.createEnrollBiometricsCard {},
itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }),
itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() {
TangemTheme {
AppSettingsScreen_EnrollBiometrics_Sample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() {
TangemTheme(isDark = true) {
AppSettingsScreen_EnrollBiometrics_Sample()
}
}
AppSettingsScreenState.Content(
items = items,
dialog = null,
).let(::add)
},
)
// endregion Preview

View file

@ -1,11 +1,70 @@
package com.tangem.tap.features.details.ui.appsettings
import com.tangem.tap.features.details.redux.AppSetting
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class AppSettingsScreenState(
val settings: Map<AppSetting, Boolean> = emptyMap(),
val showEnrollBiometricsCard: Boolean = false,
val isTogglesEnabled: Boolean = false,
val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ },
val onEnrollBiometrics: () -> Unit = { /* no-op */ },
)
@Immutable
internal sealed class AppSettingsScreenState {
object Loading : AppSettingsScreenState()
data class Content(
val items: ImmutableList<Item>,
val dialog: Dialog?,
) : AppSettingsScreenState()
@Immutable
sealed class Item {
abstract val id: String
data class Card(
override val id: String,
@DrawableRes val iconResId: Int,
val title: TextReference,
val description: TextReference,
val onClick: () -> Unit,
) : Item()
data class Switch(
override val id: String,
val title: TextReference,
val description: TextReference,
val isEnabled: Boolean,
val isChecked: Boolean,
val onCheckedChange: (Boolean) -> Unit,
) : Item()
data class Button(
override val id: String,
val title: TextReference,
val description: TextReference,
val isEnabled: Boolean,
val onClick: () -> Unit,
) : Item()
}
@Immutable
sealed class Dialog {
abstract val onDismiss: () -> Unit
data class Alert(
val title: TextReference,
val description: TextReference,
val confirmText: TextReference,
val onConfirm: () -> Unit,
override val onDismiss: () -> Unit,
) : Dialog()
data class Selector(
val title: TextReference,
val selectedItemIndex: Int,
val items: ImmutableList<TextReference>,
val onSelect: (Int) -> Unit,
override val onDismiss: () -> Unit,
) : Dialog()
}
}

View file

@ -1,36 +1,34 @@
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.redux.AppState
import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.redux.AppSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.wallet.redux.WalletAction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import org.rekotlin.Store
class AppSettingsViewModel(private val store: Store<AppState>) {
internal class AppSettingsViewModel(private val store: Store<AppState>) {
fun updateState(state: DetailsState): AppSettingsScreenState {
return with(state.appSettingsState) {
AppSettingsScreenState(
settings = mapOf(
AppSetting.SaveWallets to saveWallets,
AppSetting.SaveAccessCode to saveAccessCodes,
),
showEnrollBiometricsCard = needEnrollBiometrics,
isTogglesEnabled = !needEnrollBiometrics && !isInProgress,
onSettingToggled = { privacySetting, enabled ->
onSettingsToggled(privacySetting, enabled)
},
onEnrollBiometrics = {
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
},
private val itemsFactory = AppSettingsItemsFactory()
private val dialogsFactory = AppSettingsDialogsFactory()
var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading)
private set
fun updateState(state: DetailsState) {
uiState = AppSettingsScreenState.Content(
items = buildItems(state.appSettingsState),
dialog = (uiState as? AppSettingsScreenState.Content)?.dialog,
)
}
}
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
}
fun checkBiometricsStatus() {
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = false))
@ -39,4 +37,113 @@ class AppSettingsViewModel(private val store: Store<AppState>) {
fun refreshBiometricsStatus() {
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
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.AlertDialog
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.TextButton
import com.tangem.core.ui.components.WarningTextButton
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.redux.AppSetting
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
@Composable
internal fun SettingsAlertDialog(
element: AppSetting,
onDialogStateChange: (AppSetting?) -> Unit,
onSettingToggle: () -> Unit,
) {
val text = when (element) {
AppSetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
AppSetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
}
AlertDialog(
onDismissRequest = { onDialogStateChange(null) },
confirmButton = {
TextButton(
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
text = stringResource(id = R.string.common_cancel),
onClick = {
onDialogStateChange(null)
},
)
},
dismissButton = {
WarningTextButton(
text = stringResource(id = R.string.common_delete),
onClick = {
onDialogStateChange(null)
onSettingToggle()
},
)
},
title = {
Text(
text = stringResource(id = R.string.common_attention),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
)
},
text = {
Text(
text = stringResource(id = text),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
)
},
shape = TangemTheme.shapes.roundedCornersLarge,
internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
BasicDialog(
title = dialog.title.resolveReference(),
message = dialog.description.resolveReference(),
isDismissable = false,
confirmButton = DialogButton(
title = dialog.confirmText.resolveReference(),
warning = true,
onClick = dialog.onConfirm,
),
dismissButton = DialogButton(
title = stringResource(id = R.string.common_cancel),
onClick = dialog.onDismiss,
),
onDismissDialog = dialog.onDismiss,
)
}
// region Preview
@Composable
private fun SettingsAlertDialogSample(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary),
) {
SettingsAlertDialog(
element = AppSetting.SaveAccessCode,
onDialogStateChange = {},
onSettingToggle = { },
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SettingsAlertDialogPreview_Light() {
private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
TangemTheme {
SettingsAlertDialogSample()
SettingsAlertDialog(dialog = dialog)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SettingsAlertDialogPreview_Dark() {
private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
TangemTheme(isDark = true) {
SettingsAlertDialogSample()
SettingsAlertDialog(dialog = dialog)
}
}
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog.Alert>(
collection = buildList {
val dialogsFactory = AppSettingsDialogsFactory()
dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add)
dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add)
},
)
// endregion Preview

View file

@ -0,0 +1,80 @@
package com.tangem.tap.features.details.ui.appsettings.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun SettingsButtonItem(item: Item.Button, modifier: Modifier = Modifier) {
Surface(
modifier = modifier.fillMaxWidth(),
color = TangemTheme.colors.background.secondary,
onClick = item.onClick,
) {
Column(
modifier = Modifier.padding(
horizontal = TangemTheme.dimens.spacing20,
vertical = TangemTheme.dimens.spacing8,
),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
horizontalAlignment = Alignment.Start,
) {
Text(
modifier = Modifier.fillMaxWidth(),
text = item.title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier.fillMaxWidth(),
text = item.description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ButtonItemPreview_Light(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) {
TangemTheme {
SettingsButtonItem(item = item)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun ButtonItemPreview_Dark(@PreviewParameter(ButtonItemProvider::class) item: Item.Button) {
TangemTheme(isDark = true) {
SettingsButtonItem(item = item)
}
}
private class ButtonItemProvider : CollectionPreviewParameterProvider<Item.Button>(
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
itemsFactory.createSelectAppCurrencyButton(
currentAppCurrencyName = "US Dollar",
onClick = { /* no-op */ },
).let(::add)
},
)
// endregion Preview

View file

@ -1,11 +1,6 @@
package com.tangem.tap.features.details.ui.appsettings.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Surface
@ -14,24 +9,25 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW16
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.wallet.R
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun EnrollBiometricsCard(onClick: () -> Unit) {
internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) {
Surface(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing8)
.fillMaxWidth(),
color = TangemTheme.colors.background.primary,
modifier = modifier.fillMaxWidth(),
color = TangemTheme.colors.button.disabled,
shape = TangemTheme.shapes.roundedCornersLarge,
onClick = onClick,
onClick = item.onClick,
) {
Row(
modifier = Modifier.padding(all = 16.dp),
@ -39,20 +35,20 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) {
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Icon(
painter = painterResource(id = R.drawable.ic_alert_circle_24),
painter = painterResource(id = item.iconResId),
tint = TangemTheme.colors.icon.attention,
contentDescription = null,
)
SpacerW16()
Column {
Text(
text = stringResource(id = R.string.app_settings_enable_biometrics_title),
text = item.title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
SpacerH4()
Text(
text = stringResource(id = R.string.app_settings_enable_biometrics_description),
text = item.description.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
@ -62,28 +58,29 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) {
}
// region Preview
@Composable
private fun EnrollBiometricsCardSample(modifier: Modifier = Modifier) {
Column(
modifier = modifier.background(TangemTheme.colors.background.secondary),
) {
EnrollBiometricsCard(onClick = {})
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun EnrollBiometricsCardPreview_Light() {
private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
TangemTheme {
EnrollBiometricsCardSample()
SettingsCardItem(item = item)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun EnrollBiometricsCardPreview_Dark() {
private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
TangemTheme(isDark = true) {
EnrollBiometricsCardSample()
SettingsCardItem(item = item)
}
}
private class CardItemProvider : CollectionPreviewParameterProvider<Item.Card>(
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
itemsFactory.createEnrollBiometricsCard(
onClick = { /* no-op */ },
).let(::add)
},
)
// endregion Preview

View file

@ -0,0 +1,58 @@
package com.tangem.tap.features.details.ui.appsettings.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.components.SelectorDialog
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
import com.tangem.wallet.R
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun SettingsSelectorDialog(dialog: Dialog.Selector) {
SelectorDialog(
title = dialog.title.resolveReference(),
selectedItemIndex = dialog.selectedItemIndex,
items = dialog.items.map { it.resolveReference() }.toImmutableList(),
confirmButton = DialogButton(
title = stringResource(R.string.common_cancel),
onClick = dialog.onDismiss,
),
onSelect = dialog.onSelect,
onDismissDialog = dialog.onDismiss,
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SettingsSelectorDialogPreview_Light(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) {
TangemTheme(isDark = false) {
SettingsSelectorDialog(param)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SettingsSelectorDialogPreview_Dark(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) {
TangemTheme(isDark = true) {
SettingsSelectorDialog(param)
}
}
private class DialogProvider : CollectionPreviewParameterProvider<Dialog.Selector>(
collection = listOf(
AppSettingsDialogsFactory().createThemeModeSelectorDialog(
selectedModeIndex = 0,
onSelect = {},
onDismiss = {},
),
),
)
// endregion Preview

View file

@ -0,0 +1,113 @@
package com.tangem.tap.features.details.ui.appsettings.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW32
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.tap.features.details.ui.common.TangemSwitch
@Composable
internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {
val titleTextColor by rememberUpdatedState(
newValue = if (item.isEnabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.secondary
},
)
val descriptionTextColor by rememberUpdatedState(
newValue = if (item.isEnabled) {
TangemTheme.colors.text.secondary
} else {
TangemTheme.colors.text.tertiary
},
)
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(
modifier = Modifier.weight(weight = .9f),
verticalArrangement = Arrangement.Center,
) {
Text(
text = item.title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = titleTextColor,
)
SpacerH4()
Text(
text = item.description.resolveReference(),
style = TangemTheme.typography.body2,
color = descriptionTextColor,
)
}
SpacerW32()
TangemSwitch(
checked = item.isChecked,
enabled = item.isEnabled,
onCheckedChange = item.onCheckedChange,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SwitchItemPreview_Light(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) {
TangemTheme {
SettingsSwitchItem(item = item)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun SwitchItemPreview_Dark(@PreviewParameter(SwitchItemProvider::class) item: Item.Switch) {
TangemTheme(isDark = true) {
SettingsSwitchItem(item = item)
}
}
private class SwitchItemProvider : CollectionPreviewParameterProvider<Item.Switch>(
collection = buildList {
val itemsFactory = AppSettingsItemsFactory()
itemsFactory.createSaveAccessCodeSwitch(
isChecked = true,
isEnabled = true,
onCheckedChange = { /* no-op */ },
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = false,
isEnabled = true,
onCheckedChange = { /* no-op */ },
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = true,
isEnabled = false,
onCheckedChange = { /* no-op */ },
).let(::add)
itemsFactory.createSaveAccessCodeSwitch(
isChecked = false,
isEnabled = false,
onCheckedChange = { /* no-op */ },
).let(::add)
},
)
// endregion Preview

View file

@ -1,42 +1,36 @@
package com.tangem.tap.features.details.ui.cardsettings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.compose.ui.Modifier
import androidx.transition.TransitionInflater
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
class CardSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
@AndroidEntryPoint
internal class CardSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel = CardSettingsViewModel(store)
private var screenState: MutableState<CardSettingsScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
TangemTheme {
@Composable
override fun ScreenContent(modifier: Modifier) {
CardSettingsScreen(
modifier = modifier,
state = screenState.value,
onBackClick = {
store.dispatch(DetailsAction.ResetCardSettingsData)
@ -44,8 +38,12 @@ class CardSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
},
)
}
}
}
override fun TransitionInflater.inflateTransitions(): Boolean {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
return true
}
override fun onStart() {

View file

@ -2,13 +2,7 @@ package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
@ -18,7 +12,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@ -29,10 +22,15 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) {
internal fun CardSettingsScreen(
state: CardSettingsScreenState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val needReadCard = state.cardDetails == null
SettingsScreensScaffold(
modifier = modifier,
content = {
if (needReadCard) {
CardSettingsReadCard(state.onScanCardClick)
@ -41,14 +39,13 @@ fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit)
}
},
titleRes = R.string.card_settings_title,
backgroundColor = TangemTheme.colors.background.secondary,
onBackClick = onBackClick,
)
}
@Suppress("MagicNumber")
@Composable
fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
) {
@ -84,13 +81,13 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
) {
Text(
text = stringResource(id = R.string.scan_card_settings_title),
color = colorResource(id = R.color.text_primary_1),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h3,
)
Spacer(modifier = Modifier.size(20.dp))
Text(
text = stringResource(id = R.string.scan_card_settings_message),
color = colorResource(id = R.color.text_secondary),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body1,
modifier = Modifier
.verticalScroll(rememberScrollState())
@ -107,7 +104,7 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
@Suppress("ComplexMethod")
@Composable
fun CardSettings(state: CardSettingsScreenState) {
private fun CardSettings(state: CardSettingsScreenState) {
if (state.cardDetails == null) return
LazyColumn(
@ -166,8 +163,25 @@ fun CardSettings(state: CardSettingsScreenState) {
}
}
// region Preview
@Composable
@Preview
private fun CardSettingsPreview() {
private fun CardSettingsScreenStateSample() {
CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {})
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun CardSettingsScreenStatePreview_Light() {
TangemTheme {
CardSettingsScreenStateSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun CardSettingsScreenStatePreview_Dark() {
TangemTheme(isDark = true) {
CardSettingsScreenStateSample()
}
}
// endregion Preview

View file

@ -11,14 +11,14 @@ import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import com.tangem.wallet.R
import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
data class CardSettingsScreenState(
internal data class CardSettingsScreenState(
val cardDetails: List<CardInfo>? = null,
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
)
sealed class CardInfo(
internal sealed class CardInfo(
val titleRes: TextReference,
val subtitle: TextReference,
val clickable: Boolean = false,
@ -68,7 +68,7 @@ sealed class CardInfo(
}
// TODO("Remove and use the same from coreUI")
sealed interface TextReference {
internal sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
}
@ -78,7 +78,7 @@ sealed interface TextReference {
@Composable
@ReadOnlyComposable
fun TextReference.resolveReference(): String {
internal fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
is TextReference.Str -> this.value

View file

@ -10,7 +10,7 @@ import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store
class CardSettingsViewModel(private val store: Store<AppState>) {
internal class CardSettingsViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): CardSettingsScreenState {
return if (state?.manageSecurityState == null) {

View file

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

View file

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

View file

@ -1,18 +1,7 @@
package com.tangem.tap.features.details.ui.details
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
@ -27,12 +16,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SystemBarsEffect
import 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.TangemTheme
import com.tangem.tap.features.details.ui.common.ScreenTitle
@ -41,58 +30,69 @@ import com.tangem.wallet.R
import kotlinx.coroutines.launch
@Composable
fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) {
SystemBarsEffect {
setSystemBarsColor(color = TangemColorPalette.Light1)
}
internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
modifier = modifier,
content = { Content(state = state) },
onBackClick = onBackClick,
)
}
@Composable
fun Content(state: DetailsScreenState) {
Box {
private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) {
Box(modifier = modifier) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp))
state.elements.map { element ->
if (element == SettingsElement.WalletConnect) {
WalletConnectDetailsItem(onItemsClick = state.onItemsClick)
} else {
DetailsItem(
item = element,
appCurrency = state.appCurrency,
onItemsClick = { state.onItemsClick(element) },
ScreenTitle(titleRes = R.string.details_title)
SpacerH(height = TangemTheme.dimens.spacing36)
SettingsItems(
items = state.elements,
onItemsClick = state.onItemsClick,
)
}
}
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)
}
}
@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(
modifier = Modifier
.defaultMinSize(minHeight = 84.dp)
.fillMaxWidth()
.clickable { onItemsClick(SettingsElement.WalletConnect) },
.clickable(onClick = onItemClick),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
@ -100,7 +100,7 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
painter = painterResource(id = R.drawable.ic_walletconnect),
contentDescription = stringResource(id = R.string.wallet_connect_title),
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
tint = colorResource(id = R.color.all_colors_azure),
tint = TangemColorPalette.Azure,
)
Column(
modifier = Modifier.defaultMinSize(minHeight = 56.dp),
@ -111,25 +111,25 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
text = stringResource(id = R.string.wallet_connect_title),
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTheme.typography.h3,
color = colorResource(id = R.color.text_primary_1),
color = TangemTheme.colors.text.primary1,
)
Text(
text = stringResource(id = R.string.wallet_connect_subtitle),
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTheme.typography.body1,
color = colorResource(id = R.color.text_secondary),
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Composable
fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) {
private fun DetailsItem(item: SettingsElement, onItemClick: () -> Unit) {
Row(
modifier = Modifier
.height(56.dp)
.fillMaxWidth()
.clickable(onClick = onItemsClick),
.clickable(onClick = onItemClick),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
@ -137,28 +137,21 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () ->
painter = painterResource(id = item.iconRes),
contentDescription = stringResource(id = item.titleRes),
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
tint = colorResource(id = R.color.icon_secondary),
tint = TangemTheme.colors.icon.secondary,
)
Column(modifier = Modifier.padding(end = 20.dp)) {
Text(
text = stringResource(id = item.titleRes),
modifier = Modifier,
style = TangemTheme.typography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
color = TangemTheme.colors.text.primary1,
)
if (item == SettingsElement.AppCurrency) {
Text(
text = appCurrency,
style = TangemTheme.typography.body2,
color = colorResource(id = R.color.text_secondary),
)
}
}
}
}
@Composable
fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
private fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
LazyRow(
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically,
@ -170,14 +163,14 @@ fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (
modifier = Modifier
.padding(8.dp)
.clickable { onSocialNetworkClick(it) },
tint = colorResource(id = R.color.icon_informative),
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Composable
fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
private fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
SnackbarHost(
@ -207,17 +200,43 @@ fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
}
@Composable
@Preview
private fun Preview() {
private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
text = "${stringResource(id = appNameRes)} $version",
style = TangemTheme.typography.caption,
color = TangemTheme.colors.text.tertiary,
)
}
// region Preview
@Composable
private fun DetailsScreenContentSample() {
DetailsScreen(
state = DetailsScreenState(
elements = SettingsElement.values().toList(),
tangemLinks = TangemSocialAccounts.accountsEn,
tangemVersion = "Tangem 2.14.12 (343)",
appCurrency = "Dollar",
onItemsClick = {},
onSocialNetworkClick = {},
),
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
@Immutable
data class DetailsScreenState(
internal data class DetailsScreenState(
val elements: List<SettingsElement>,
val tangemLinks: List<SocialNetworkLink>,
val tangemVersion: String,
val appCurrency: String,
val onItemsClick: (SettingsElement) -> Unit,
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
val showErrorSnackbar: MutableState<EventError> = mutableStateOf(EventError.Empty),
@ -19,30 +18,29 @@ data class DetailsScreenState(
}
@Immutable
enum class SettingsElement(
internal enum class SettingsElement(
val iconRes: Int,
val titleRes: Int,
) {
WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title),
Chat(R.drawable.ic_chat, R.string.details_chat),
SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback),
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title),
CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title),
AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency),
AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App
Chat(R.drawable.ic_chat, R.string.details_chat),
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),
TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu),
}
@Immutable
data class SocialNetworkLink(
internal data class SocialNetworkLink(
val network: SocialNetwork,
val url: String,
)
sealed class EventError {
internal sealed class EventError {
object Empty : EventError()
data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError()
}
@ -58,7 +56,7 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) {
object Discord : SocialNetwork("Discord", R.drawable.ic_discord)
}
object TangemSocialAccounts {
internal object TangemSocialAccounts {
val accountsEn: List<SocialNetworkLink> = listOf(
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
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.domain.common.util.cardTypesResolver
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.userWalletsListManager
import com.tangem.wallet.BuildConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.rekotlin.Store
class DetailsViewModel(private val store: Store<AppState>) {
internal class DetailsViewModel(private val store: Store<AppState>) {
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
private set
@Suppress("ComplexMethod")
fun updateState(state: DetailsState): DetailsScreenState {
val cardTypesResolver = state.scanResponse?.cardTypesResolver
val settings = SettingsElement.values().mapNotNull {
when (it) {
SettingsElement.WalletConnect -> {
if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null
init {
bootstrapScreenState()
}
fun updateState(state: DetailsState): DetailsScreenState {
return DetailsScreenState(
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) {
SettingsElement.WalletConnect -> if (cardTypesResolver.isMultiwalletAllowed()) it else null
SettingsElement.SendFeedback -> it
SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null
SettingsElement.PrivacyPolicy -> {
if (state.privacyPolicyUrl != null) it else null
}
SettingsElement.PrivacyPolicy -> if (state.privacyPolicyUrl != null) it else null
SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null
SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null
SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null
SettingsElement.ReferralProgram -> if (cardTypesResolver.isTangemWallet()) it else null
SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null
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) {
@ -78,9 +89,6 @@ class DetailsViewModel(private val store: Store<AppState>) {
Analytics.send(Settings.ButtonCardSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
}
SettingsElement.AppCurrency -> {
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
}
SettingsElement.AppSettings -> {
Analytics.send(Settings.ButtonAppSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
@ -118,4 +126,14 @@ class DetailsViewModel(private val store: Store<AppState>) {
val versionName: String = BuildConfig.VERSION_NAME
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
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.compose.ui.Modifier
import androidx.transition.TransitionInflater
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
class ResetCardFragment : Fragment(), StoreSubscriber<DetailsState> {
@AndroidEntryPoint
internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel = ResetCardViewModel(store)
private var screenState: MutableState<ResetCardScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
TangemTheme {
@Composable
override fun ScreenContent(modifier: Modifier) {
ResetCardScreen(
modifier = modifier,
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
override fun TransitionInflater.inflateTransitions(): Boolean {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
return true
}
override fun onStart() {

View file

@ -1,26 +1,12 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.IconToggleButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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
@Composable
fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit) {
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
modifier = modifier,
content = { ResetCardView(state = state) },
onBackClick = onBackClick,
backgroundColor = Color.Transparent,
)
}
@Suppress("LongMethod", "MagicNumber")
@Composable
fun ResetCardView(state: ResetCardScreenState) {
private fun ResetCardView(state: ResetCardScreenState) {
Column(
modifier = Modifier
.fillMaxSize()

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.details.ui.resetcard
import com.tangem.tap.features.details.ui.cardsettings.TextReference
data class ResetCardScreenState(
internal data class ResetCardScreenState(
val accepted: Boolean = false,
val descriptionText: TextReference,
val onAcceptWarningToggleClick: (Boolean) -> Unit,

View file

@ -7,7 +7,7 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import org.rekotlin.Store
class ResetCardViewModel(private val store: Store<AppState>) {
internal class ResetCardViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): ResetCardScreenState {
val descriptionText = state?.cardInfo

View file

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

View file

@ -1,10 +1,6 @@
package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
@ -20,8 +16,13 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) {
internal fun SecurityModeScreen(
state: SecurityModeScreenState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
modifier = modifier,
content = { SecurityModeOptions(state = state) },
// titleRes = R.string.card_settings_security_mode,
onBackClick = onBackClick,
@ -29,7 +30,7 @@ fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit)
}
@Composable
fun SecurityModeOptions(state: SecurityModeScreenState) {
private fun SecurityModeOptions(state: SecurityModeScreenState) {
Column(
modifier = Modifier
.fillMaxSize()
@ -55,7 +56,7 @@ fun SecurityModeOptions(state: SecurityModeScreenState) {
}
@Composable
fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
val selected = option == state.selectedSecurityMode
val title = option.toTitleRes()

View file

@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.securitymode
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.wallet.R
data class SecurityModeScreenState(
internal data class SecurityModeScreenState(
val availableOptions: List<SecurityOption>,
val selectedSecurityMode: SecurityOption,
val isSaveChangesEnabled: Boolean,
@ -11,7 +11,7 @@ data class SecurityModeScreenState(
val onSaveChangesClicked: () -> Unit,
)
fun SecurityOption.toTitleRes(): Int {
internal fun SecurityOption.toTitleRes(): Int {
return when (this) {
SecurityOption.LongTap -> R.string.details_manage_security_long_tap
SecurityOption.PassCode -> R.string.details_manage_security_passcode

View file

@ -6,7 +6,7 @@ import com.tangem.tap.features.details.redux.ManageSecurityState
import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Store
class SecurityModeViewModel(val store: Store<AppState>) {
internal class SecurityModeViewModel(val store: Store<AppState>) {
fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
if (state == null) {

View file

@ -1,24 +1,30 @@
package com.tangem.tap.features.details.ui.walletconnect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.compose.ui.Modifier
import androidx.transition.TransitionInflater
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<WalletConnectState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
private val viewModel = WalletConnectViewModel(store)
private var screenState: MutableState<WalletConnectScreenState> =
mutableStateOf(viewModel.updateState(store.state.walletConnectState))
@ -26,17 +32,12 @@ class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(WalletConnect.ScreenOpened())
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
TangemTheme {
@Composable
override fun ScreenContent(modifier: Modifier) {
WalletConnectScreen(
modifier = modifier,
state = screenState.value,
onBackClick = {
if (screenState.value.isLoading) {
@ -50,8 +51,12 @@ class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
},
)
}
}
}
override fun TransitionInflater.inflateTransitions(): Boolean {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
return true
}
override fun onStart() {

View file

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

View file

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

View file

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

View file

@ -56,7 +56,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
override fun onStart() {
super.onStart()
setStatusBarColor(R.color.backgroundLightGray)
setStatusBarColor(R.color.background_secondary)
webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) }
store.subscribe(subscriber = this) { state ->

View file

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

View file

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

View file

@ -260,7 +260,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray
val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary
tvAmountCurrency.setTextColor(fg.getColor(textColor))
}

View file

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

View file

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

View file

@ -1,252 +1,17 @@
package com.tangem.tap.features.tokens.impl.domain
import androidx.paging.PagingData
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.tokens.impl.domain.models.Token
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.AppStateHolder
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import timber.log.Timber
/**
* Default implementation of tokens list interactor
* FIXME("Necessary to avoid using redux actions")
*
* @property repository repository of tokens list feature
* @property reduxStateHolder redux state holder
*/
internal class DefaultTokensListInteractor(
private val repository: TokensListRepository,
private val reduxStateHolder: AppStateHolder,
) : TokensListInteractor {
internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor {
override fun getTokensList(searchText: String): Flow<PagingData<Token>> {
return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null }))
}
override suspend fun saveChanges(tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain>) {
val scanResponse = requireNotNull(reduxStateHolder.scanResponse)
val derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle()
val currentTokens = store.state.tokensState.addedWallets
.toNonCustomTokensWithBlockchains(derivationStyle = derivationStyle)
val currentBlockchains = store.state.tokensState.addedWallets
.toNonCustomBlockchains(derivationStyle = derivationStyle)
val blockchainsToAdd = blockchains.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(blockchains::contains)
val tokensToAdd = tokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> tokens.any { it.token == token.token } }
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
return
}
remove(
tokens = tokensToRemove,
blockchains = blockchainsToRemove,
derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(),
)
add(tokens = tokensToAdd, blockchains = blockchainsToAdd, scanResponse = scanResponse)
}
private fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
derivationStyle: DerivationStyle?,
): List<TokenWithBlockchain> {
return this.map(WalletDataModel::currency)
.mapNotNull { currency ->
if (currency !is Currency.Token || currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
TokenWithBlockchain(token = currency.token, blockchain = currency.blockchain)
}
.distinct()
}
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
return this.map(WalletDataModel::currency)
.mapNotNull { currency ->
if (currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
(currency as? Currency.Blockchain)?.blockchain
}
.distinct()
}
private suspend fun remove(
tokens: List<TokenWithBlockchain>,
blockchains: List<Blockchain>,
derivationStyle: DerivationStyle?,
) {
val currencies = convertToCurrencies(tokens, blockchains, derivationStyle)
if (currencies.isEmpty()) return
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to remove currencies, no user wallet selected")
return
}
walletCurrenciesManager.removeCurrencies(userWallet = selectedUserWallet, currenciesToRemove = currencies)
}
private suspend fun add(
tokens: List<TokenWithBlockchain>,
blockchains: List<Blockchain>,
scanResponse: ScanResponse,
) {
val currenciesToAdd = convertToCurrencies(
tokens = tokens,
blockchains = blockchains,
derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(),
)
// TODO("[REDACTED_TASK_KEY] use DerivationManager")
if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, currenciesToAdd)
} else {
submitAdd(scanResponse, currenciesToAdd)
return
}
}
private suspend fun deriveMissingBlockchains(scanResponse: ScanResponse, currencies: List<Currency>) {
val config = CardConfig.createConfig(scanResponse.card)
val 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
import androidx.paging.PagingData
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.tokens.impl.domain.models.Token
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import kotlinx.coroutines.flow.Flow
/**
@ -15,12 +13,4 @@ internal interface TokensListInteractor {
/** Get tokens list using filter by text [searchText] */
fun getTokensList(searchText: String): Flow<PagingData<Token>>
/**
* Save added tokens
*
* @param tokens tokens list that need to save
* @param blockchains blockchains list that need to save
*/
suspend fun saveChanges(tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain>)
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.tokens.impl.presentation.models
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
import com.tangem.tap.store
/**
* Required data for tokens list screen
* FIXME("Necessary to avoid using redux state")
*
[REDACTED_AUTHOR]
*/
class TokensListArgs {
/** Tokens list screen mode */
val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess
/** Tokens list that accessible from the main screen */
val mainScreenTokenList: List<TokenWithBlockchain> get() = store.state.tokensState.addedTokens
/** Blockchains list that accessible from the main screen */
val mainScreenBlockchainList: List<Blockchain> get() = store.state.tokensState.addedBlockchains
}

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

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.toMapKey
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.domainStore
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -43,29 +39,69 @@ object TokensMiddleware {
{ next ->
{ action ->
when (action) {
is TokensAction.SaveChanges -> handleSaveChanges(action)
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken()
is TokensAction.LegacySaveChanges -> handleLegacySaveChanges(action)
is TokensAction.NewSaveChanges -> handleNewSaveChanges(action)
}
next(action)
}
}
}
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
private fun handleNewSaveChanges(action: TokensAction.NewSaveChanges) {
scope.launch {
val scanResponse = store.state.globalState.scanResponse ?: return@launch
val scanResponse = action.userWallet.scanResponse
val currentTokens = store.state.tokensState.addedTokens
val currentBlockchains = store.state.tokensState.addedBlockchains
val currentTokens = action.currentTokens
val currentBlockchains = action.currentCoins
val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains)
val blockchainsToRemove =
store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains)
val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains)
val tokensToAdd = action.tokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } }
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } }
removeCurrenciesIfNeeded(
removeNewCurrenciesIfNeeded(
userWalletId = action.userWallet.walletId,
currencies = blockchainsToRemove + tokensToRemove,
)
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
store.dispatchDebugErrorNotification(message = "Nothing to save")
store.dispatchOnMain(NavigationAction.PopBackTo())
return@launch
}
val currencyList = blockchainsToAdd + tokensToAdd
if (scanResponse.supportsHdWallet()) {
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
} else {
submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
}
}
private fun handleLegacySaveChanges(action: TokensAction.LegacySaveChanges) {
scope.launch {
val scanResponse = action.scanResponse
val currentTokens = action.currentTokens
val currentBlockchains = action.currentBlockchains
val blockchainsToAdd = action.changedBlockchains.filterNot(currentBlockchains::contains)
val blockchainsToRemove = currentBlockchains.filterNot(action.changedBlockchains::contains)
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
val tokensToRemove =
currentTokens.filterNot { token -> action.changedTokens.any { it.token == token.token } }
removeLegacyCurrenciesIfNeeded(
currencies = convertToCurrencies(
blockchains = blockchainsToRemove,
tokens = tokensToRemove,
@ -89,11 +125,11 @@ object TokensMiddleware {
if (scanResponse.supportsHdWallet()) {
deriveMissingBlockchains(scanResponse, currencyList) {
submitAdd(it, currencyList)
submitLegacyAdd(it, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
} else {
submitAdd(scanResponse, currencyList)
submitLegacyAdd(scanResponse, currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
}
@ -104,13 +140,12 @@ object TokensMiddleware {
tokens: List<TokenWithBlockchain>,
derivationStyle: DerivationStyle?,
): List<Currency> {
return blockchains.map {
Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath)
} + tokens.map {
return blockchains.map { Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) } +
tokens.map {
Currency.Token(
it.token,
it.blockchain,
it.blockchain.derivationPath(derivationStyle)?.rawPath,
token = it.token,
blockchain = it.blockchain,
derivationPath = it.blockchain.derivationPath(derivationStyle)?.rawPath,
)
}
}
@ -123,7 +158,7 @@ object TokensMiddleware {
val config = CardConfig.createConfig(scanResponse.card)
val derivationDataList = currencyList.mapNotNull {
val curve = config.primaryCurve(it.blockchain)
curve?.let { getDerivations(curve, scanResponse, currencyList) }
curve?.let { getLegacyDerivations(curve, scanResponse, currencyList) }
}
val derivations = derivationDataList.associate { it.derivations }
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,
scanResponse: ScanResponse,
currencyList: List<Currency>,
@ -200,9 +283,48 @@ object TokensMiddleware {
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>>)
private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
private fun submitLegacyAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to add currencies, no user wallet selected")
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
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("Unable to remove currencies, no user wallet selected")
@ -232,55 +362,10 @@ object TokensMiddleware {
walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies)
}
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
return currency.derivationPath?.let {
!scanResponse.hasDerivation(currency.blockchain, it)
} ?: false
}
private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
private fun handleAddingCustomToken() = scope.launch {
val onAddCustomToken = fun(customCurrency: CustomCurrency) {
val scanResponse = store.state.globalState.scanResponse ?: return
fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List<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))
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
}
}

View file

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

View file

@ -1,13 +1,15 @@
package com.tangem.tap.features.wallet.converters
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.store
import com.tangem.utils.converter.Converter
import com.tangem.utils.converter.TwoWayConverter
internal class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrency> {
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.Token
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
@ -39,7 +41,10 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
@Suppress("LargeClass")
class TradeCryptoMiddleware {
@Suppress("LongMethod", "CyclomaticComplexMethod")
fun handle(state: () -> AppState?, action: TradeCryptoAction) {
if (DemoHelper.tryHandle(state, action)) return
@ -52,11 +57,10 @@ class TradeCryptoMiddleware {
openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency())
}
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send())
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
is TradeCryptoAction.New.Swap -> {
openSwap(currency = action.cryptoCurrency.toSwapCurrency())
}
is TradeCryptoAction.New.Swap -> openSwap(currency = action.cryptoCurrency.toSwapCurrency())
is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action)
is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action)
}
}
@ -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.statePrinter.printScanResponseState
import com.tangem.tap.domain.statePrinter.printWalletState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
@ -311,18 +310,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
return when (item.itemId) {
R.id.details_menu -> {
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
store.state.globalState.scanResponse?.let { scanResponse ->
store.dispatch(
DetailsAction.PrepareScreen(
scanResponse = scanResponse,
wallets = store.state.walletState.walletManagers.map { it.wallet },
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true
}
false
}
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.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.tokens.TokensAction
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.entities.FiatCurrency
@ -14,7 +14,6 @@ import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
@ -106,14 +105,8 @@ class MultiWalletView : WalletView() {
binding.btnAddToken.setOnClickListener {
Analytics.send(Portfolio.ButtonManageTokens())
store.dispatch(
TokensAction.SetArgs.ManageAccess(
wallets = state.walletsDataFromStores,
derivationStyle = store.state.globalState.scanResponse
?.derivationStyleProvider?.getDerivationStyle(),
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(action = TokensAction.SetArgs.ManageAccess)
store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.AddTokens))
}
handleErrorStates(state = state, binding = binding, fragment = fragment)
}

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

View file

@ -3,9 +3,11 @@ package com.tangem.tap.proxy.redux
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.features.tester.api.TesterRouter
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
@ -16,6 +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.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import org.rekotlin.StateType
data class DaggerGraphState(
@ -35,6 +38,11 @@ data class DaggerGraphState(
val cardSdkConfigRepository: CardSdkConfigRepository? = null,
val appCurrencyRepository: AppCurrencyRepository? = 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 {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {

View file

@ -4,7 +4,7 @@
<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:color="@color/backgroundLightGray" android:state_enabled="true" />
<item android:color="@color/backgroundLightGray" />
<item android:color="@color/background_secondary" android:state_enabled="true" />
<item android:color="@color/background_secondary" />
</selector>

View file

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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="?attr/colorSecondary" 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:alpha="0.12" android:color="?attr/colorOnSurface" />
<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_secondary" android:state_enabled="true" />
<item android:color="@color/text_secondary" />
</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:viewportHeight="19">
<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" />
</vector>

View file

@ -4,6 +4,6 @@
android:viewportWidth="16"
android:viewportHeight="19">
<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" />
</vector>

View file

@ -6,6 +6,6 @@
android:viewportHeight="19">
<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" />
</vector>

View file

@ -5,5 +5,5 @@
android:viewportHeight="18">
<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:fillColor="#000000"/>
android:fillColor="@color/text_primary_1" />
</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" />
<path
android:fillColor="#F4F5F6"
android:fillColor="@color/button_secondary"
android:pathData="M0 0V40H40V0" />
</group>

View file

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

View file

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

View file

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

View file

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

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