Updated on 2026-08-14
This commit is contained in:
commit
f0d03e3648
286 changed files with 6353 additions and 5898 deletions
56
app/src/main/java/com/tangem/tap/DeviceFlipDetector.kt
Normal file
56
app/src/main/java/com/tangem/tap/DeviceFlipDetector.kt
Normal 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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
typealias ValueCallback<T> = (T) -> Unit
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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 ?: "") }
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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) }),
|
||||
;
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ internal object TokensListInteractorModule {
|
|||
reduxStateHolder = reduxStateHolder,
|
||||
testnetTokensStorage = testnetTokensStorage,
|
||||
),
|
||||
reduxStateHolder = reduxStateHolder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
9
app/src/main/res/color/selector_chip_text.xml
Normal file
9
app/src/main/res/color/selector_chip_text.xml
Normal 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>
|
||||
5
app/src/main/res/color/selector_edit_text_secondary.xml
Normal file
5
app/src/main/res/color/selector_edit_text_secondary.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue