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.FeaturesLocalLoader
|
||||||
import com.tangem.datasource.config.models.Config
|
import com.tangem.datasource.config.models.Config
|
||||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||||
import com.tangem.domain.DomainLayer
|
|
||||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||||
|
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||||
import com.tangem.domain.card.ScanCardProcessor
|
import com.tangem.domain.card.ScanCardProcessor
|
||||||
import com.tangem.domain.common.LogConfig
|
import com.tangem.domain.common.LogConfig
|
||||||
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.domain.wallets.legacy.WalletManagersRepository
|
import com.tangem.domain.wallets.legacy.WalletManagersRepository
|
||||||
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
|
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
|
||||||
|
|
@ -169,6 +170,12 @@ class TapApplication : Application(), ImageLoaderFactory {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var walletManagersFacade: WalletManagersFacade
|
lateinit var walletManagersFacade: WalletManagersFacade
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var currenciesRepository: CurrenciesRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appThemeModeRepository: AppThemeModeRepository
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
|
@ -189,6 +196,9 @@ class TapApplication : Application(), ImageLoaderFactory {
|
||||||
scanCardProcessor = scanCardProcessor,
|
scanCardProcessor = scanCardProcessor,
|
||||||
appCurrencyRepository = appCurrencyRepository,
|
appCurrencyRepository = appCurrencyRepository,
|
||||||
walletManagersFacade = walletManagersFacade,
|
walletManagersFacade = walletManagersFacade,
|
||||||
|
appStateHolder = appStateHolder,
|
||||||
|
currenciesRepository = currenciesRepository,
|
||||||
|
appThemeModeRepository = appThemeModeRepository,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -208,7 +218,6 @@ class TapApplication : Application(), ImageLoaderFactory {
|
||||||
activityResultCaller = foregroundActivityObserver
|
activityResultCaller = foregroundActivityObserver
|
||||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||||
|
|
||||||
DomainLayer.init()
|
|
||||||
preferencesStorage = preferencesDataSource
|
preferencesStorage = preferencesDataSource
|
||||||
walletConnectRepository = WalletConnectRepository(this)
|
walletConnectRepository = WalletConnectRepository(this)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.core.navigation.FragmentShareTransition
|
||||||
import com.tangem.feature.referral.ReferralFragment
|
import com.tangem.feature.referral.ReferralFragment
|
||||||
import com.tangem.feature.swap.presentation.SwapFragment
|
import com.tangem.feature.swap.presentation.SwapFragment
|
||||||
import com.tangem.tap.features.customtoken.legacy.AddCustomTokenFragment
|
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment
|
||||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
|
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
|
||||||
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
|
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
|
||||||
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
|
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
|
||||||
|
|
@ -33,7 +33,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment as RedesignedAddCustomTokenFragment
|
|
||||||
|
|
||||||
fun FragmentActivity.openFragment(
|
fun FragmentActivity.openFragment(
|
||||||
screen: AppScreen,
|
screen: AppScreen,
|
||||||
|
|
@ -155,18 +154,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
||||||
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
|
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
|
||||||
AppScreen.Disclaimer -> DisclaimerFragment()
|
AppScreen.Disclaimer -> DisclaimerFragment()
|
||||||
AppScreen.AddTokens -> TokensListFragment()
|
AppScreen.AddTokens -> TokensListFragment()
|
||||||
|
AppScreen.AddCustomToken -> AddCustomTokenFragment()
|
||||||
AppScreen.AddCustomToken -> {
|
|
||||||
val featureToggles = store.state.daggerGraphState.get(
|
|
||||||
getDependency = DaggerGraphState::customTokenFeatureToggles,
|
|
||||||
)
|
|
||||||
if (featureToggles.isRedesignedScreenEnabled) {
|
|
||||||
RedesignedAddCustomTokenFragment()
|
|
||||||
} else {
|
|
||||||
AddCustomTokenFragment()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppScreen.WalletDetails -> {
|
AppScreen.WalletDetails -> {
|
||||||
val featureToggles = store.state.daggerGraphState.get(
|
val featureToggles = store.state.daggerGraphState.get(
|
||||||
getDependency = DaggerGraphState::tokenDetailsFeatureToggles,
|
getDependency = DaggerGraphState::tokenDetailsFeatureToggles,
|
||||||
|
|
|
||||||
|
|
@ -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.common.core.TangemError
|
||||||
import com.tangem.datasource.config.ConfigManager
|
import com.tangem.datasource.config.ConfigManager
|
||||||
import com.tangem.datasource.config.models.ChatConfig
|
import com.tangem.datasource.config.models.ChatConfig
|
||||||
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||||
import com.tangem.tap.common.analytics.topup.TopUpController
|
import com.tangem.tap.common.analytics.topup.TopUpController
|
||||||
|
|
@ -103,4 +104,5 @@ sealed class GlobalAction : Action {
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
|
data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
|
||||||
|
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +131,8 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
|
||||||
sellService = makeSellExchangeService(config),
|
sellService = makeSellExchangeService(config),
|
||||||
primaryRules = CardExchangeRules(cardProvider),
|
primaryRules = CardExchangeRules(cardProvider),
|
||||||
)
|
)
|
||||||
|
// TODO: for refactoring (after remove old design refactor CurrencyExchangeManager and use 1 instance)
|
||||||
|
store.state.daggerGraphState.get(DaggerGraphState::appStateHolder).exchangeService = exchangeManager
|
||||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
|
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
|
||||||
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
|
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,9 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
|
||||||
userWalletsListManager = action.manager,
|
userWalletsListManager = action.manager,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is GlobalAction.ChangeAppThemeMode -> globalState.copy(
|
||||||
|
appThemeMode = action.appThemeMode,
|
||||||
|
)
|
||||||
else -> globalState
|
else -> globalState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.tap.common.redux.global
|
package com.tangem.tap.common.redux.global
|
||||||
|
|
||||||
import com.tangem.datasource.config.ConfigManager
|
import com.tangem.datasource.config.ConfigManager
|
||||||
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||||
import com.tangem.tap.common.analytics.topup.TopUpController
|
import com.tangem.tap.common.analytics.topup.TopUpController
|
||||||
|
|
@ -29,6 +30,7 @@ data class GlobalState(
|
||||||
val userCountryCode: String? = null,
|
val userCountryCode: String? = null,
|
||||||
val userWalletsListManager: UserWalletsListManager? = null,
|
val userWalletsListManager: UserWalletsListManager? = null,
|
||||||
val topUpController: TopUpController? = null,
|
val topUpController: TopUpController? = null,
|
||||||
|
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
|
||||||
) : StateType
|
) : StateType
|
||||||
|
|
||||||
typealias CryptoCurrencyName = String
|
typealias CryptoCurrencyName = String
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,11 @@ package com.tangem.tap.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.tangem.domain.card.ScanCardUseCase
|
import com.tangem.domain.card.ScanCardUseCase
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
|
import com.tangem.domain.exchange.RampStateManager
|
||||||
import com.tangem.tap.domain.TangemSdkManager
|
import com.tangem.tap.domain.TangemSdkManager
|
||||||
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
|
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
|
||||||
|
import com.tangem.tap.network.exchangeServices.DefaultRampManager
|
||||||
|
import com.tangem.tap.proxy.AppStateHolder
|
||||||
import com.tangem.tap.userTokensRepository
|
import com.tangem.tap.userTokensRepository
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
|
@ -40,4 +43,10 @@ internal object ActivityModule {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideDefaultRampManager(appStateHolder: AppStateHolder): RampStateManager {
|
||||||
|
return DefaultRampManager(appStateHolder.exchangeService)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
|
import com.tangem.domain.exchange.RampStateManager
|
||||||
import com.tangem.domain.tokens.*
|
import com.tangem.domain.tokens.*
|
||||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
|
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
|
@ -108,8 +110,32 @@ internal object TokensDomainModule {
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
fun provideGetCryptoCurrencyActionsUseCase(
|
fun provideGetCryptoCurrencyActionsUseCase(
|
||||||
|
rampStateManager: RampStateManager,
|
||||||
|
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
|
||||||
dispatchers: CoroutineDispatcherProvider,
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
): GetCryptoCurrencyActionsUseCase {
|
): GetCryptoCurrencyActionsUseCase {
|
||||||
return GetCryptoCurrencyActionsUseCase(dispatchers)
|
return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ViewModelScoped
|
||||||
|
fun provideGetCurrencyStatusByNetworkUseCase(
|
||||||
|
currenciesRepository: CurrenciesRepository,
|
||||||
|
quotesRepository: QuotesRepository,
|
||||||
|
networksRepository: NetworksRepository,
|
||||||
|
dispatchers: CoroutineDispatcherProvider,
|
||||||
|
): GetNetworkCoinStatusUseCase {
|
||||||
|
return GetNetworkCoinStatusUseCase(
|
||||||
|
currenciesRepository = currenciesRepository,
|
||||||
|
quotesRepository = quotesRepository,
|
||||||
|
networksRepository = networksRepository,
|
||||||
|
dispatchers = dispatchers,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@ViewModelScoped
|
||||||
|
fun provideGetCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase {
|
||||||
|
return GetCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 {
|
interface CustomTokenFeatureToggles {
|
||||||
|
|
||||||
/** Availability of redesigned screen (internal feature) */
|
|
||||||
val isRedesignedScreenEnabled: Boolean
|
|
||||||
|
|
||||||
val isNewCardScanningEnabled: Boolean
|
val isNewCardScanningEnabled: Boolean
|
||||||
}
|
}
|
||||||
|
|
@ -14,9 +14,6 @@ internal class DefaultCustomTokenFeatureToggles(
|
||||||
private val featureTogglesManager: FeatureTogglesManager,
|
private val featureTogglesManager: FeatureTogglesManager,
|
||||||
) : CustomTokenFeatureToggles {
|
) : CustomTokenFeatureToggles {
|
||||||
|
|
||||||
override val isRedesignedScreenEnabled: Boolean
|
|
||||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED")
|
|
||||||
|
|
||||||
override val isNewCardScanningEnabled: Boolean
|
override val isNewCardScanningEnabled: Boolean
|
||||||
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
|
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
package com.tangem.tap.features.details.redux
|
||||||
|
|
||||||
import com.tangem.blockchain.common.Wallet
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
|
||||||
import com.tangem.domain.common.CardTypesResolver
|
import com.tangem.domain.common.CardTypesResolver
|
||||||
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.tap.common.entities.FiatCurrency
|
import com.tangem.tap.common.entities.FiatCurrency
|
||||||
import org.rekotlin.Action
|
import org.rekotlin.Action
|
||||||
|
|
@ -11,7 +11,6 @@ sealed class DetailsAction : Action {
|
||||||
|
|
||||||
data class PrepareScreen(
|
data class PrepareScreen(
|
||||||
val scanResponse: ScanResponse,
|
val scanResponse: ScanResponse,
|
||||||
val wallets: List<Wallet>,
|
|
||||||
) : DetailsAction()
|
) : DetailsAction()
|
||||||
|
|
||||||
object ReCreateTwinsWallet : DetailsAction()
|
object ReCreateTwinsWallet : DetailsAction()
|
||||||
|
|
@ -70,6 +69,10 @@ sealed class DetailsAction : Action {
|
||||||
data class BiometricsStatusChanged(
|
data class BiometricsStatusChanged(
|
||||||
val needEnrollBiometrics: Boolean,
|
val needEnrollBiometrics: Boolean,
|
||||||
) : AppSettings()
|
) : AppSettings()
|
||||||
|
|
||||||
|
data class ChangeAppThemeMode(
|
||||||
|
val appThemeMode: AppThemeMode,
|
||||||
|
) : AppSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()
|
data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction()
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.tangem.common.flatMap
|
||||||
import com.tangem.core.analytics.Analytics
|
import com.tangem.core.analytics.Analytics
|
||||||
import com.tangem.core.navigation.AppScreen
|
import com.tangem.core.navigation.AppScreen
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||||
import com.tangem.domain.common.util.cardTypesResolver
|
import com.tangem.domain.common.util.cardTypesResolver
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
|
|
@ -221,6 +222,9 @@ class DetailsMiddleware {
|
||||||
is DetailsAction.AppSettings.EnrollBiometrics -> {
|
is DetailsAction.AppSettings.EnrollBiometrics -> {
|
||||||
enrollBiometrics()
|
enrollBiometrics()
|
||||||
}
|
}
|
||||||
|
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
|
||||||
|
changeAppThemeMode(action.appThemeMode)
|
||||||
|
}
|
||||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
|
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
|
||||||
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
|
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
|
||||||
is DetailsAction.AppSettings.BiometricsStatusChanged,
|
is DetailsAction.AppSettings.BiometricsStatusChanged,
|
||||||
|
|
@ -252,6 +256,14 @@ class DetailsMiddleware {
|
||||||
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
|
store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
|
||||||
|
val repository = store.state.daggerGraphState.get(DaggerGraphState::appThemeModeRepository)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
repository.changeAppThemeMode(appThemeMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
|
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
|
||||||
// Nothing to change
|
// Nothing to change
|
||||||
if (preferencesStorage.shouldSaveUserWallets == enable) {
|
if (preferencesStorage.shouldSaveUserWallets == enable) {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import com.tangem.tap.preferencesStorage
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.tap.tangemSdkManager
|
import com.tangem.tap.tangemSdkManager
|
||||||
import org.rekotlin.Action
|
import org.rekotlin.Action
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
object DetailsReducer {
|
object DetailsReducer {
|
||||||
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
|
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
|
||||||
|
|
@ -39,8 +39,11 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
||||||
is DetailsAction.AppSettings -> {
|
is DetailsAction.AppSettings -> {
|
||||||
handlePrivacyAction(action, detailsState)
|
handlePrivacyAction(action, detailsState)
|
||||||
}
|
}
|
||||||
is DetailsAction.ChangeAppCurrency ->
|
is DetailsAction.ChangeAppCurrency -> detailsState.copy(
|
||||||
detailsState.copy(appCurrency = action.fiatCurrency)
|
appSettingsState = detailsState.appSettingsState.copy(
|
||||||
|
selectedFiatCurrency = action.fiatCurrency,
|
||||||
|
),
|
||||||
|
)
|
||||||
is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
|
is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
|
||||||
else -> detailsState
|
else -> detailsState
|
||||||
}
|
}
|
||||||
|
|
@ -49,13 +52,13 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
||||||
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState {
|
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState {
|
||||||
return DetailsState(
|
return DetailsState(
|
||||||
scanResponse = action.scanResponse,
|
scanResponse = action.scanResponse,
|
||||||
wallets = action.wallets,
|
|
||||||
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||||
appCurrency = store.state.globalState.appCurrency,
|
|
||||||
appSettingsState = AppSettingsState(
|
appSettingsState = AppSettingsState(
|
||||||
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
|
isBiometricsAvailable = tangemSdkManager.canUseBiometry,
|
||||||
saveWallets = preferencesStorage.shouldSaveUserWallets,
|
saveWallets = preferencesStorage.shouldSaveUserWallets,
|
||||||
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
|
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
|
||||||
|
selectedFiatCurrency = store.state.globalState.appCurrency,
|
||||||
|
selectedThemeMode = store.state.globalState.appThemeMode,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +197,11 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
|
||||||
needEnrollBiometrics = action.needEnrollBiometrics,
|
needEnrollBiometrics = action.needEnrollBiometrics,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy(
|
||||||
|
appSettingsState = state.appSettingsState.copy(
|
||||||
|
selectedThemeMode = action.appThemeMode,
|
||||||
|
),
|
||||||
|
)
|
||||||
is DetailsAction.AppSettings.EnrollBiometrics,
|
is DetailsAction.AppSettings.EnrollBiometrics,
|
||||||
is DetailsAction.AppSettings.CheckBiometricsStatus,
|
is DetailsAction.AppSettings.CheckBiometricsStatus,
|
||||||
-> state
|
-> state
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,18 @@
|
||||||
package com.tangem.tap.features.details.redux
|
package com.tangem.tap.features.details.redux
|
||||||
|
|
||||||
import com.tangem.blockchain.common.Wallet
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.domain.models.scan.CardDTO
|
import com.tangem.domain.models.scan.CardDTO
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.tap.common.entities.Button
|
import com.tangem.tap.common.entities.Button
|
||||||
import com.tangem.tap.common.entities.FiatCurrency
|
import com.tangem.tap.common.entities.FiatCurrency
|
||||||
import org.rekotlin.StateType
|
import org.rekotlin.StateType
|
||||||
import java.util.*
|
import java.util.EnumSet
|
||||||
|
|
||||||
data class DetailsState(
|
data class DetailsState(
|
||||||
val scanResponse: ScanResponse? = null,
|
val scanResponse: ScanResponse? = null,
|
||||||
val wallets: List<Wallet> = emptyList(),
|
|
||||||
val cardSettingsState: CardSettingsState? = null,
|
val cardSettingsState: CardSettingsState? = null,
|
||||||
val privacyPolicyUrl: String? = null,
|
val privacyPolicyUrl: String? = null,
|
||||||
val createBackupAllowed: Boolean = false,
|
val createBackupAllowed: Boolean = false,
|
||||||
val appCurrency: FiatCurrency = FiatCurrency.Default,
|
|
||||||
val appSettingsState: AppSettingsState = AppSettingsState(),
|
val appSettingsState: AppSettingsState = AppSettingsState(),
|
||||||
) : StateType
|
) : StateType
|
||||||
|
|
||||||
|
|
@ -57,6 +55,8 @@ data class AppSettingsState(
|
||||||
val isBiometricsAvailable: Boolean = false,
|
val isBiometricsAvailable: Boolean = false,
|
||||||
val needEnrollBiometrics: Boolean = false,
|
val needEnrollBiometrics: Boolean = false,
|
||||||
val isInProgress: Boolean = false,
|
val isInProgress: Boolean = false,
|
||||||
|
val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default,
|
||||||
|
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class SecurityOption { LongTap, PassCode, AccessCode }
|
enum class SecurityOption { LongTap, PassCode, AccessCode }
|
||||||
|
|
|
||||||
|
|
@ -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
|
package com.tangem.tap.features.details.ui.appsettings
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.View
|
import androidx.compose.ui.Modifier
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.transition.TransitionInflater
|
import androidx.transition.TransitionInflater
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.screen.ComposeFragment
|
||||||
|
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||||
import com.tangem.tap.features.details.redux.DetailsAction
|
import com.tangem.tap.features.details.redux.DetailsAction
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.rekotlin.StoreSubscriber
|
import org.rekotlin.StoreSubscriber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class AppSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||||
|
|
||||||
class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
|
|
||||||
private val viewModel = AppSettingsViewModel(store)
|
private val viewModel = AppSettingsViewModel(store)
|
||||||
private var screenState: MutableState<AppSettingsScreenState> =
|
|
||||||
mutableStateOf(viewModel.updateState(store.state.detailsState))
|
@Composable
|
||||||
|
override fun ScreenContent(modifier: Modifier) {
|
||||||
|
AppSettingsScreen(
|
||||||
|
modifier = modifier,
|
||||||
|
state = viewModel.uiState,
|
||||||
|
onBackClick = {
|
||||||
|
store.dispatch(DetailsAction.ResetCardSettingsData)
|
||||||
|
store.dispatch(NavigationAction.PopBackTo())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val inflater = TransitionInflater.from(requireContext())
|
|
||||||
enterTransition = inflater.inflateTransition(R.transition.fade)
|
|
||||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
|
||||||
viewModel.checkBiometricsStatus()
|
viewModel.checkBiometricsStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||||
return ComposeView(requireContext()).apply {
|
enterTransition = inflateTransition(R.transition.fade)
|
||||||
setContent {
|
exitTransition = inflateTransition(R.transition.fade)
|
||||||
isTransitionGroup = true
|
|
||||||
TangemTheme {
|
return true
|
||||||
AppSettingsScreen(
|
|
||||||
state = screenState.value,
|
|
||||||
onBackClick = {
|
|
||||||
store.dispatch(DetailsAction.ResetCardSettingsData)
|
|
||||||
store.dispatch(NavigationAction.PopBackTo())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
@ -68,6 +69,6 @@ class AppSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
|
||||||
|
|
||||||
override fun newState(state: DetailsState) {
|
override fun newState(state: DetailsState) {
|
||||||
if (activity == null || view == null) return
|
if (activity == null || view == null) return
|
||||||
screenState.value = viewModel.updateState(state)
|
viewModel.updateState(state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
package com.tangem.tap.features.details.ui.appsettings
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.tangem.core.ui.components.SpacerH24
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import com.tangem.core.ui.components.SpacerH32
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
import com.tangem.core.ui.components.SpacerH4
|
|
||||||
import com.tangem.core.ui.components.SpacerW32
|
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.tap.features.details.redux.AppSetting
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard
|
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||||
import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog
|
import com.tangem.tap.features.details.ui.appsettings.components.*
|
||||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||||
import com.tangem.tap.features.details.ui.common.TangemSwitch
|
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit) {
|
internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
SettingsScreensScaffold(
|
SettingsScreensScaffold(
|
||||||
content = { AppSettings(state = state) },
|
modifier = modifier,
|
||||||
|
content = {
|
||||||
|
when (state) {
|
||||||
|
is AppSettingsScreenState.Content -> AppSettings(state = state)
|
||||||
|
is AppSettingsScreenState.Loading -> Unit
|
||||||
|
}
|
||||||
|
},
|
||||||
titleRes = R.string.app_settings_title,
|
titleRes = R.string.app_settings_title,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppSettings(state: AppSettingsScreenState) {
|
private fun AppSettings(state: AppSettingsScreenState.Content) {
|
||||||
var dialogType by remember { mutableStateOf<AppSetting?>(null) }
|
val dialog by rememberUpdatedState(newValue = state.dialog)
|
||||||
val onDialogStateChange: (AppSetting?) -> Unit = { dialogType = it }
|
when (val safeDialog = dialog) {
|
||||||
|
is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog)
|
||||||
dialogType?.let {
|
is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog)
|
||||||
SettingsAlertDialog(
|
null -> Unit
|
||||||
element = it,
|
|
||||||
onDialogStateChange = onDialogStateChange,
|
|
||||||
onSettingToggle = { state.onSettingToggled(it, false) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
LazyColumn {
|
||||||
if (state.showEnrollBiometricsCard) {
|
items(
|
||||||
EnrollBiometricsCard(onClick = state.onEnrollBiometrics)
|
items = state.items,
|
||||||
SpacerH24()
|
key = Item::id,
|
||||||
}
|
) { item ->
|
||||||
|
when (item) {
|
||||||
AppSettingsElement(
|
is Item.Card -> SettingsCardItem(
|
||||||
state = state,
|
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||||
setting = AppSetting.SaveWallets,
|
item = item,
|
||||||
onDialogStateChange = onDialogStateChange,
|
|
||||||
)
|
|
||||||
SpacerH32()
|
|
||||||
AppSettingsElement(
|
|
||||||
state = state,
|
|
||||||
setting = AppSetting.SaveAccessCode,
|
|
||||||
onDialogStateChange = onDialogStateChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
|
||||||
@Composable
|
|
||||||
private fun AppSettingsElement(
|
|
||||||
state: AppSettingsScreenState,
|
|
||||||
setting: AppSetting,
|
|
||||||
onDialogStateChange: (AppSetting?) -> Unit,
|
|
||||||
) {
|
|
||||||
val titleRes = when (setting) {
|
|
||||||
AppSetting.SaveWallets -> R.string.app_settings_saved_wallet
|
|
||||||
AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes
|
|
||||||
}
|
|
||||||
val subtitleRes = when (setting) {
|
|
||||||
AppSetting.SaveWallets -> R.string.app_settings_saved_wallet_footer
|
|
||||||
AppSetting.SaveAccessCode -> R.string.app_settings_saved_access_codes_footer
|
|
||||||
}
|
|
||||||
val checked = state.settings[setting] ?: false
|
|
||||||
|
|
||||||
val titleTextColor by rememberUpdatedState(
|
|
||||||
newValue = if (state.isTogglesEnabled) {
|
|
||||||
TangemTheme.colors.text.primary1
|
|
||||||
} else {
|
|
||||||
TangemTheme.colors.text.secondary
|
|
||||||
},
|
|
||||||
)
|
|
||||||
val descriptionTextColor by rememberUpdatedState(
|
|
||||||
newValue = if (state.isTogglesEnabled) {
|
|
||||||
TangemTheme.colors.text.secondary
|
|
||||||
} else {
|
|
||||||
TangemTheme.colors.text.tertiary
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = TangemTheme.dimens.spacing20),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.weight(weight = .9f),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = titleRes),
|
|
||||||
style = TangemTheme.typography.subtitle1,
|
|
||||||
color = titleTextColor,
|
|
||||||
)
|
|
||||||
SpacerH4()
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = subtitleRes),
|
|
||||||
style = TangemTheme.typography.body2,
|
|
||||||
color = descriptionTextColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SpacerW32()
|
|
||||||
TangemSwitch(
|
|
||||||
checked = checked,
|
|
||||||
enabled = state.isTogglesEnabled,
|
|
||||||
onCheckedChange = { isChecked ->
|
|
||||||
onCheckedChange(
|
|
||||||
element = setting,
|
|
||||||
enabled = isChecked,
|
|
||||||
onSettingToggled = state.onSettingToggled,
|
|
||||||
onDialogStateChange = onDialogStateChange,
|
|
||||||
)
|
)
|
||||||
},
|
is Item.Button -> SettingsButtonItem(
|
||||||
)
|
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8),
|
||||||
}
|
item = item,
|
||||||
}
|
)
|
||||||
|
is Item.Switch -> SettingsSwitchItem(
|
||||||
private fun onCheckedChange(
|
modifier = Modifier.padding(
|
||||||
element: AppSetting,
|
vertical = TangemTheme.dimens.spacing16,
|
||||||
enabled: Boolean,
|
horizontal = TangemTheme.dimens.spacing20,
|
||||||
onSettingToggled: (AppSetting, Boolean) -> Unit,
|
),
|
||||||
onDialogStateChange: (AppSetting?) -> Unit,
|
item = item,
|
||||||
) {
|
)
|
||||||
// Show warning if user wants to disable the switch
|
}
|
||||||
if (!enabled) {
|
}
|
||||||
onDialogStateChange(element)
|
|
||||||
} else {
|
|
||||||
onSettingToggled(element, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Preview
|
// region Preview
|
||||||
@Composable
|
|
||||||
private fun AppSettingsScreenSample(modifier: Modifier = Modifier) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.background(TangemTheme.colors.background.primary),
|
|
||||||
) {
|
|
||||||
AppSettingsScreen(
|
|
||||||
state = AppSettingsScreenState(
|
|
||||||
settings = mapOf(
|
|
||||||
AppSetting.SaveWallets to true,
|
|
||||||
AppSetting.SaveAccessCode to false,
|
|
||||||
),
|
|
||||||
showEnrollBiometricsCard = false,
|
|
||||||
isTogglesEnabled = true,
|
|
||||||
onSettingToggled = { _, _ -> },
|
|
||||||
onEnrollBiometrics = {},
|
|
||||||
),
|
|
||||||
onBackClick = { },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppSettingsScreenPreview_Light() {
|
private fun AppSettingsScreenPreview_Light(
|
||||||
|
@PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState,
|
||||||
|
) {
|
||||||
TangemTheme {
|
TangemTheme {
|
||||||
AppSettingsScreenSample()
|
AppSettingsScreen(state = state, onBackClick = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppSettingsScreenPreview_Dark() {
|
private fun AppSettingsScreenPreview_Dark(
|
||||||
|
@PreviewParameter(AppSettingsScreenStateProvider::class) state: AppSettingsScreenState,
|
||||||
|
) {
|
||||||
TangemTheme(isDark = true) {
|
TangemTheme(isDark = true) {
|
||||||
AppSettingsScreenSample()
|
AppSettingsScreen(state = state, onBackClick = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvider<AppSettingsScreenState>(
|
||||||
private fun AppSettingsScreen_EnrollBiometrics_Sample(modifier: Modifier = Modifier) {
|
collection = buildList {
|
||||||
Column(modifier = modifier.background(TangemTheme.colors.background.primary)) {
|
val itemsFactory = AppSettingsItemsFactory()
|
||||||
AppSettingsScreen(
|
val items = persistentListOf(
|
||||||
state = AppSettingsScreenState(
|
itemsFactory.createEnrollBiometricsCard {},
|
||||||
settings = mapOf(
|
itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
|
||||||
AppSetting.SaveWallets to true,
|
itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }),
|
||||||
AppSetting.SaveAccessCode to false,
|
itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
|
||||||
),
|
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
|
||||||
showEnrollBiometricsCard = true,
|
|
||||||
isTogglesEnabled = false,
|
|
||||||
onSettingToggled = { _, _ -> },
|
|
||||||
onEnrollBiometrics = {},
|
|
||||||
),
|
|
||||||
onBackClick = { },
|
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
AppSettingsScreenState.Content(
|
||||||
@Composable
|
items = items,
|
||||||
private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() {
|
dialog = null,
|
||||||
TangemTheme {
|
).let(::add)
|
||||||
AppSettingsScreen_EnrollBiometrics_Sample()
|
},
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
|
||||||
@Composable
|
|
||||||
private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() {
|
|
||||||
TangemTheme(isDark = true) {
|
|
||||||
AppSettingsScreen_EnrollBiometrics_Sample()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion Preview
|
// endregion Preview
|
||||||
|
|
@ -1,11 +1,70 @@
|
||||||
package com.tangem.tap.features.details.ui.appsettings
|
package com.tangem.tap.features.details.ui.appsettings
|
||||||
|
|
||||||
import com.tangem.tap.features.details.redux.AppSetting
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class AppSettingsScreenState(
|
@Immutable
|
||||||
val settings: Map<AppSetting, Boolean> = emptyMap(),
|
internal sealed class AppSettingsScreenState {
|
||||||
val showEnrollBiometricsCard: Boolean = false,
|
|
||||||
val isTogglesEnabled: Boolean = false,
|
object Loading : AppSettingsScreenState()
|
||||||
val onSettingToggled: (AppSetting, Boolean) -> Unit = { _, _ -> /* no-op */ },
|
|
||||||
val onEnrollBiometrics: () -> Unit = { /* no-op */ },
|
data class Content(
|
||||||
)
|
val items: ImmutableList<Item>,
|
||||||
|
val dialog: Dialog?,
|
||||||
|
) : AppSettingsScreenState()
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class Item {
|
||||||
|
|
||||||
|
abstract val id: String
|
||||||
|
|
||||||
|
data class Card(
|
||||||
|
override val id: String,
|
||||||
|
@DrawableRes val iconResId: Int,
|
||||||
|
val title: TextReference,
|
||||||
|
val description: TextReference,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
) : Item()
|
||||||
|
|
||||||
|
data class Switch(
|
||||||
|
override val id: String,
|
||||||
|
val title: TextReference,
|
||||||
|
val description: TextReference,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
val isChecked: Boolean,
|
||||||
|
val onCheckedChange: (Boolean) -> Unit,
|
||||||
|
) : Item()
|
||||||
|
|
||||||
|
data class Button(
|
||||||
|
override val id: String,
|
||||||
|
val title: TextReference,
|
||||||
|
val description: TextReference,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
) : Item()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class Dialog {
|
||||||
|
|
||||||
|
abstract val onDismiss: () -> Unit
|
||||||
|
|
||||||
|
data class Alert(
|
||||||
|
val title: TextReference,
|
||||||
|
val description: TextReference,
|
||||||
|
val confirmText: TextReference,
|
||||||
|
val onConfirm: () -> Unit,
|
||||||
|
override val onDismiss: () -> Unit,
|
||||||
|
) : Dialog()
|
||||||
|
|
||||||
|
data class Selector(
|
||||||
|
val title: TextReference,
|
||||||
|
val selectedItemIndex: Int,
|
||||||
|
val items: ImmutableList<TextReference>,
|
||||||
|
val onSelect: (Int) -> Unit,
|
||||||
|
override val onDismiss: () -> Unit,
|
||||||
|
) : Dialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,35 +1,33 @@
|
||||||
package com.tangem.tap.features.details.ui.appsettings
|
package com.tangem.tap.features.details.ui.appsettings
|
||||||
|
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
import com.tangem.tap.features.details.redux.AppSetting
|
import com.tangem.tap.features.details.redux.AppSetting
|
||||||
|
import com.tangem.tap.features.details.redux.AppSettingsState
|
||||||
import com.tangem.tap.features.details.redux.DetailsAction
|
import com.tangem.tap.features.details.redux.DetailsAction
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
|
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
|
|
||||||
class AppSettingsViewModel(private val store: Store<AppState>) {
|
internal class AppSettingsViewModel(private val store: Store<AppState>) {
|
||||||
|
|
||||||
fun updateState(state: DetailsState): AppSettingsScreenState {
|
private val itemsFactory = AppSettingsItemsFactory()
|
||||||
return with(state.appSettingsState) {
|
private val dialogsFactory = AppSettingsDialogsFactory()
|
||||||
AppSettingsScreenState(
|
|
||||||
settings = mapOf(
|
|
||||||
AppSetting.SaveWallets to saveWallets,
|
|
||||||
AppSetting.SaveAccessCode to saveAccessCodes,
|
|
||||||
),
|
|
||||||
showEnrollBiometricsCard = needEnrollBiometrics,
|
|
||||||
isTogglesEnabled = !needEnrollBiometrics && !isInProgress,
|
|
||||||
onSettingToggled = { privacySetting, enabled ->
|
|
||||||
onSettingsToggled(privacySetting, enabled)
|
|
||||||
},
|
|
||||||
onEnrollBiometrics = {
|
|
||||||
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
|
var uiState: AppSettingsScreenState by mutableStateOf(AppSettingsScreenState.Loading)
|
||||||
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
|
private set
|
||||||
|
|
||||||
|
fun updateState(state: DetailsState) {
|
||||||
|
uiState = AppSettingsScreenState.Content(
|
||||||
|
items = buildItems(state.appSettingsState),
|
||||||
|
dialog = (uiState as? AppSettingsScreenState.Content)?.dialog,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkBiometricsStatus() {
|
fun checkBiometricsStatus() {
|
||||||
|
|
@ -39,4 +37,113 @@ class AppSettingsViewModel(private val store: Store<AppState>) {
|
||||||
fun refreshBiometricsStatus() {
|
fun refreshBiometricsStatus() {
|
||||||
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = true))
|
store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = true))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildItems(state: AppSettingsState): ImmutableList<AppSettingsScreenState.Item> {
|
||||||
|
val items = buildList {
|
||||||
|
if (state.needEnrollBiometrics) {
|
||||||
|
itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics).let(::add)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsFactory.createSelectAppCurrencyButton(
|
||||||
|
currentAppCurrencyName = state.selectedFiatCurrency.name,
|
||||||
|
onClick = ::showAppCurrencySelector,
|
||||||
|
).let(::add)
|
||||||
|
|
||||||
|
if (state.isBiometricsAvailable) {
|
||||||
|
val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress
|
||||||
|
|
||||||
|
itemsFactory.createSaveWalletsSwitch(
|
||||||
|
isChecked = state.saveWallets,
|
||||||
|
isEnabled = canUseBiometrics,
|
||||||
|
onCheckedChange = ::onSaveWalletsToggled,
|
||||||
|
).let(::add)
|
||||||
|
|
||||||
|
itemsFactory.createSaveAccessCodeSwitch(
|
||||||
|
isChecked = state.saveAccessCodes,
|
||||||
|
isEnabled = canUseBiometrics,
|
||||||
|
onCheckedChange = ::onSaveAccessCodesToggled,
|
||||||
|
).let(::add)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
|
||||||
|
showThemeModeSelector(state.selectedThemeMode)
|
||||||
|
}.let(::add)
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.toImmutableList()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun enrollBiometrics() {
|
||||||
|
store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAppCurrencySelector() {
|
||||||
|
store.dispatchOnMain(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showThemeModeSelector(selectedMode: AppThemeMode) {
|
||||||
|
updateContentState {
|
||||||
|
copy(
|
||||||
|
dialog = dialogsFactory.createThemeModeSelectorDialog(
|
||||||
|
selectedModeIndex = selectedMode.ordinal,
|
||||||
|
onSelect = { mode ->
|
||||||
|
store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode))
|
||||||
|
dismissDialog()
|
||||||
|
},
|
||||||
|
onDismiss = ::dismissDialog,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSaveWalletsToggled(isChecked: Boolean) {
|
||||||
|
if (isChecked) {
|
||||||
|
onSettingsToggled(AppSetting.SaveWallets, enable = true)
|
||||||
|
} else {
|
||||||
|
updateContentState {
|
||||||
|
copy(
|
||||||
|
dialog = dialogsFactory.createDeleteSavedWalletsAlert(
|
||||||
|
onDelete = {
|
||||||
|
onSettingsToggled(AppSetting.SaveWallets, enable = false)
|
||||||
|
dismissDialog()
|
||||||
|
},
|
||||||
|
onDismiss = ::dismissDialog,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSaveAccessCodesToggled(isChecked: Boolean) {
|
||||||
|
if (isChecked) {
|
||||||
|
onSettingsToggled(AppSetting.SaveAccessCode, enable = true)
|
||||||
|
} else {
|
||||||
|
updateContentState {
|
||||||
|
copy(
|
||||||
|
dialog = dialogsFactory.createDeleteSavedAccessCodesAlert(
|
||||||
|
onDelete = {
|
||||||
|
onSettingsToggled(AppSetting.SaveAccessCode, enable = false)
|
||||||
|
dismissDialog()
|
||||||
|
},
|
||||||
|
onDismiss = ::dismissDialog,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onSettingsToggled(setting: AppSetting, enable: Boolean) {
|
||||||
|
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun dismissDialog() {
|
||||||
|
updateContentState { copy(dialog = null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) {
|
||||||
|
uiState = when (val state = uiState) {
|
||||||
|
is AppSettingsScreenState.Content -> block(state)
|
||||||
|
is AppSettingsScreenState.Loading -> state
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,97 +1,60 @@
|
||||||
package com.tangem.tap.features.details.ui.appsettings.components
|
package com.tangem.tap.features.details.ui.appsettings.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.AlertDialog
|
|
||||||
import androidx.compose.material.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import com.tangem.core.ui.components.TextButton
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import com.tangem.core.ui.components.WarningTextButton
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
|
import com.tangem.core.ui.components.BasicDialog
|
||||||
|
import com.tangem.core.ui.components.DialogButton
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.tap.features.details.redux.AppSetting
|
import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory
|
||||||
|
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun SettingsAlertDialog(
|
internal fun SettingsAlertDialog(dialog: Dialog.Alert) {
|
||||||
element: AppSetting,
|
BasicDialog(
|
||||||
onDialogStateChange: (AppSetting?) -> Unit,
|
title = dialog.title.resolveReference(),
|
||||||
onSettingToggle: () -> Unit,
|
message = dialog.description.resolveReference(),
|
||||||
) {
|
isDismissable = false,
|
||||||
val text = when (element) {
|
confirmButton = DialogButton(
|
||||||
AppSetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message
|
title = dialog.confirmText.resolveReference(),
|
||||||
AppSetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message
|
warning = true,
|
||||||
}
|
onClick = dialog.onConfirm,
|
||||||
|
),
|
||||||
AlertDialog(
|
dismissButton = DialogButton(
|
||||||
onDismissRequest = { onDialogStateChange(null) },
|
title = stringResource(id = R.string.common_cancel),
|
||||||
confirmButton = {
|
onClick = dialog.onDismiss,
|
||||||
TextButton(
|
),
|
||||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12),
|
onDismissDialog = dialog.onDismiss,
|
||||||
text = stringResource(id = R.string.common_cancel),
|
|
||||||
onClick = {
|
|
||||||
onDialogStateChange(null)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
WarningTextButton(
|
|
||||||
text = stringResource(id = R.string.common_delete),
|
|
||||||
onClick = {
|
|
||||||
onDialogStateChange(null)
|
|
||||||
onSettingToggle()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.common_attention),
|
|
||||||
color = TangemTheme.colors.text.primary1,
|
|
||||||
style = TangemTheme.typography.h2,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = text),
|
|
||||||
color = TangemTheme.colors.text.secondary,
|
|
||||||
style = TangemTheme.typography.body2,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Preview
|
// region Preview
|
||||||
@Composable
|
|
||||||
private fun SettingsAlertDialogSample(modifier: Modifier = Modifier) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier
|
|
||||||
.background(TangemTheme.colors.background.primary),
|
|
||||||
) {
|
|
||||||
SettingsAlertDialog(
|
|
||||||
element = AppSetting.SaveAccessCode,
|
|
||||||
onDialogStateChange = {},
|
|
||||||
onSettingToggle = { },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsAlertDialogPreview_Light() {
|
private fun AlertDialogPreview_Light(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
|
||||||
TangemTheme {
|
TangemTheme {
|
||||||
SettingsAlertDialogSample()
|
SettingsAlertDialog(dialog = dialog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsAlertDialogPreview_Dark() {
|
private fun AlertDialogPreview_Dark(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) {
|
||||||
TangemTheme(isDark = true) {
|
TangemTheme(isDark = true) {
|
||||||
SettingsAlertDialogSample()
|
SettingsAlertDialog(dialog = dialog)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class AlertDialogProvider : CollectionPreviewParameterProvider<Dialog.Alert>(
|
||||||
|
collection = buildList {
|
||||||
|
val dialogsFactory = AppSettingsDialogsFactory()
|
||||||
|
|
||||||
|
dialogsFactory.createDeleteSavedAccessCodesAlert({}, {}).let(::add)
|
||||||
|
dialogsFactory.createDeleteSavedWalletsAlert({}, {}).let(::add)
|
||||||
|
},
|
||||||
|
)
|
||||||
// endregion Preview
|
// endregion Preview
|
||||||
|
|
@ -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
|
package com.tangem.tap.features.details.ui.appsettings.components
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
|
|
@ -14,24 +9,25 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.components.SpacerH4
|
import com.tangem.core.ui.components.SpacerH4
|
||||||
import com.tangem.core.ui.components.SpacerW16
|
import com.tangem.core.ui.components.SpacerW16
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.wallet.R
|
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
|
||||||
|
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun EnrollBiometricsCard(onClick: () -> Unit) {
|
internal fun SettingsCardItem(item: Item.Card, modifier: Modifier = Modifier) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = modifier.fillMaxWidth(),
|
||||||
.padding(horizontal = TangemTheme.dimens.spacing8)
|
color = TangemTheme.colors.button.disabled,
|
||||||
.fillMaxWidth(),
|
|
||||||
color = TangemTheme.colors.background.primary,
|
|
||||||
shape = TangemTheme.shapes.roundedCornersLarge,
|
shape = TangemTheme.shapes.roundedCornersLarge,
|
||||||
onClick = onClick,
|
onClick = item.onClick,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(all = 16.dp),
|
modifier = Modifier.padding(all = 16.dp),
|
||||||
|
|
@ -39,20 +35,20 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) {
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ic_alert_circle_24),
|
painter = painterResource(id = item.iconResId),
|
||||||
tint = TangemTheme.colors.icon.attention,
|
tint = TangemTheme.colors.icon.attention,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
SpacerW16()
|
SpacerW16()
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.app_settings_enable_biometrics_title),
|
text = item.title.resolveReference(),
|
||||||
style = TangemTheme.typography.subtitle1,
|
style = TangemTheme.typography.subtitle1,
|
||||||
color = TangemTheme.colors.text.primary1,
|
color = TangemTheme.colors.text.primary1,
|
||||||
)
|
)
|
||||||
SpacerH4()
|
SpacerH4()
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.app_settings_enable_biometrics_description),
|
text = item.description.resolveReference(),
|
||||||
style = TangemTheme.typography.body2,
|
style = TangemTheme.typography.body2,
|
||||||
color = TangemTheme.colors.text.secondary,
|
color = TangemTheme.colors.text.secondary,
|
||||||
)
|
)
|
||||||
|
|
@ -62,28 +58,29 @@ internal fun EnrollBiometricsCard(onClick: () -> Unit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// region Preview
|
// region Preview
|
||||||
@Composable
|
|
||||||
private fun EnrollBiometricsCardSample(modifier: Modifier = Modifier) {
|
|
||||||
Column(
|
|
||||||
modifier = modifier.background(TangemTheme.colors.background.secondary),
|
|
||||||
) {
|
|
||||||
EnrollBiometricsCard(onClick = {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
@Composable
|
@Composable
|
||||||
private fun EnrollBiometricsCardPreview_Light() {
|
private fun CardItemPreview_Light(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
|
||||||
TangemTheme {
|
TangemTheme {
|
||||||
EnrollBiometricsCardSample()
|
SettingsCardItem(item = item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true, widthDp = 360)
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
@Composable
|
@Composable
|
||||||
private fun EnrollBiometricsCardPreview_Dark() {
|
private fun CardItemPreview_Dark(@PreviewParameter(CardItemProvider::class) item: Item.Card) {
|
||||||
TangemTheme(isDark = true) {
|
TangemTheme(isDark = true) {
|
||||||
EnrollBiometricsCardSample()
|
SettingsCardItem(item = item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CardItemProvider : CollectionPreviewParameterProvider<Item.Card>(
|
||||||
|
collection = buildList {
|
||||||
|
val itemsFactory = AppSettingsItemsFactory()
|
||||||
|
|
||||||
|
itemsFactory.createEnrollBiometricsCard(
|
||||||
|
onClick = { /* no-op */ },
|
||||||
|
).let(::add)
|
||||||
|
},
|
||||||
|
)
|
||||||
// endregion Preview
|
// endregion Preview
|
||||||
|
|
@ -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,51 +1,49 @@
|
||||||
package com.tangem.tap.features.details.ui.cardsettings
|
package com.tangem.tap.features.details.ui.cardsettings
|
||||||
|
|
||||||
import android.os.Bundle
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.transition.TransitionInflater
|
import androidx.transition.TransitionInflater
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.screen.ComposeFragment
|
||||||
|
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||||
import com.tangem.tap.features.details.redux.DetailsAction
|
import com.tangem.tap.features.details.redux.DetailsAction
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.rekotlin.StoreSubscriber
|
import org.rekotlin.StoreSubscriber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CardSettingsFragment : Fragment(), StoreSubscriber<DetailsState> {
|
@AndroidEntryPoint
|
||||||
|
internal class CardSettingsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||||
|
|
||||||
private val viewModel = CardSettingsViewModel(store)
|
private val viewModel = CardSettingsViewModel(store)
|
||||||
|
|
||||||
private var screenState: MutableState<CardSettingsScreenState> =
|
private var screenState: MutableState<CardSettingsScreenState> =
|
||||||
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
|
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
@Composable
|
||||||
super.onCreate(savedInstanceState)
|
override fun ScreenContent(modifier: Modifier) {
|
||||||
|
CardSettingsScreen(
|
||||||
val inflater = TransitionInflater.from(requireContext())
|
modifier = modifier,
|
||||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
state = screenState.value,
|
||||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
onBackClick = {
|
||||||
|
store.dispatch(DetailsAction.ResetCardSettingsData)
|
||||||
|
store.dispatch(NavigationAction.PopBackTo())
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||||
return ComposeView(requireContext()).apply {
|
enterTransition = inflateTransition(R.transition.fade)
|
||||||
setContent {
|
exitTransition = inflateTransition(R.transition.fade)
|
||||||
isTransitionGroup = true
|
|
||||||
TangemTheme {
|
return true
|
||||||
CardSettingsScreen(
|
|
||||||
state = screenState.value,
|
|
||||||
onBackClick = {
|
|
||||||
store.dispatch(DetailsAction.ResetCardSettingsData)
|
|
||||||
store.dispatch(NavigationAction.PopBackTo())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,7 @@ package com.tangem.tap.features.details.ui.cardsettings
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
|
@ -18,7 +12,6 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.colorResource
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -29,10 +22,15 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit) {
|
internal fun CardSettingsScreen(
|
||||||
|
state: CardSettingsScreenState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
val needReadCard = state.cardDetails == null
|
val needReadCard = state.cardDetails == null
|
||||||
|
|
||||||
SettingsScreensScaffold(
|
SettingsScreensScaffold(
|
||||||
|
modifier = modifier,
|
||||||
content = {
|
content = {
|
||||||
if (needReadCard) {
|
if (needReadCard) {
|
||||||
CardSettingsReadCard(state.onScanCardClick)
|
CardSettingsReadCard(state.onScanCardClick)
|
||||||
|
|
@ -41,14 +39,13 @@ fun CardSettingsScreen(state: CardSettingsScreenState, onBackClick: () -> Unit)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
titleRes = R.string.card_settings_title,
|
titleRes = R.string.card_settings_title,
|
||||||
backgroundColor = TangemTheme.colors.background.secondary,
|
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
@Composable
|
@Composable
|
||||||
fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
|
|
@ -84,13 +81,13 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.scan_card_settings_title),
|
text = stringResource(id = R.string.scan_card_settings_title),
|
||||||
color = colorResource(id = R.color.text_primary_1),
|
color = TangemTheme.colors.text.primary1,
|
||||||
style = TangemTheme.typography.h3,
|
style = TangemTheme.typography.h3,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.size(20.dp))
|
Spacer(modifier = Modifier.size(20.dp))
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.scan_card_settings_message),
|
text = stringResource(id = R.string.scan_card_settings_message),
|
||||||
color = colorResource(id = R.color.text_secondary),
|
color = TangemTheme.colors.text.secondary,
|
||||||
style = TangemTheme.typography.body1,
|
style = TangemTheme.typography.body1,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
|
|
@ -107,7 +104,7 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
|
||||||
|
|
||||||
@Suppress("ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
@Composable
|
@Composable
|
||||||
fun CardSettings(state: CardSettingsScreenState) {
|
private fun CardSettings(state: CardSettingsScreenState) {
|
||||||
if (state.cardDetails == null) return
|
if (state.cardDetails == null) return
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
@ -166,8 +163,25 @@ fun CardSettings(state: CardSettingsScreenState) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// region Preview
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
private fun CardSettingsScreenStateSample() {
|
||||||
private fun CardSettingsPreview() {
|
|
||||||
CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {})
|
CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}) {}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Composable
|
||||||
|
private fun CardSettingsScreenStatePreview_Light() {
|
||||||
|
TangemTheme {
|
||||||
|
CardSettingsScreenStateSample()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Composable
|
||||||
|
private fun CardSettingsScreenStatePreview_Dark() {
|
||||||
|
TangemTheme(isDark = true) {
|
||||||
|
CardSettingsScreenStateSample()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion Preview
|
||||||
|
|
@ -11,14 +11,14 @@ import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
|
import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
|
||||||
|
|
||||||
data class CardSettingsScreenState(
|
internal data class CardSettingsScreenState(
|
||||||
val cardDetails: List<CardInfo>? = null,
|
val cardDetails: List<CardInfo>? = null,
|
||||||
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
|
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
|
||||||
val onScanCardClick: () -> Unit,
|
val onScanCardClick: () -> Unit,
|
||||||
val onElementClick: (CardInfo) -> Unit,
|
val onElementClick: (CardInfo) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class CardInfo(
|
internal sealed class CardInfo(
|
||||||
val titleRes: TextReference,
|
val titleRes: TextReference,
|
||||||
val subtitle: TextReference,
|
val subtitle: TextReference,
|
||||||
val clickable: Boolean = false,
|
val clickable: Boolean = false,
|
||||||
|
|
@ -68,7 +68,7 @@ sealed class CardInfo(
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO("Remove and use the same from coreUI")
|
// TODO("Remove and use the same from coreUI")
|
||||||
sealed interface TextReference {
|
internal sealed interface TextReference {
|
||||||
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
|
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
|
||||||
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
|
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +78,7 @@ sealed interface TextReference {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ReadOnlyComposable
|
@ReadOnlyComposable
|
||||||
fun TextReference.resolveReference(): String {
|
internal fun TextReference.resolveReference(): String {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
|
is TextReference.Res -> stringResource(this.id, *this.formatArgs.toTypedArray())
|
||||||
is TextReference.Str -> this.value
|
is TextReference.Str -> this.value
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import com.tangem.tap.features.details.redux.CardSettingsState
|
||||||
import com.tangem.tap.features.details.redux.DetailsAction
|
import com.tangem.tap.features.details.redux.DetailsAction
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
|
|
||||||
class CardSettingsViewModel(private val store: Store<AppState>) {
|
internal class CardSettingsViewModel(private val store: Store<AppState>) {
|
||||||
|
|
||||||
fun updateState(state: CardSettingsState?): CardSettingsScreenState {
|
fun updateState(state: CardSettingsState?): CardSettingsScreenState {
|
||||||
return if (state?.manageSecurityState == null) {
|
return if (state?.manageSecurityState == null) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package com.tangem.tap.features.details.ui.common
|
package com.tangem.tap.features.details.ui.common
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
|
|
@ -12,20 +13,24 @@ import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||||
|
import com.tangem.core.ui.components.SystemBarsEffect
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreensScaffold(
|
internal fun SettingsScreensScaffold(
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
content: @Composable () -> Unit,
|
|
||||||
background: @Composable (() -> Unit)? = null,
|
|
||||||
fab: @Composable (() -> Unit)? = null,
|
|
||||||
backgroundColor: Color = TangemTheme.colors.background.secondary,
|
|
||||||
titleRes: Int? = null,
|
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
fab: @Composable (() -> Unit)? = null,
|
||||||
|
@StringRes titleRes: Int? = null,
|
||||||
) {
|
) {
|
||||||
BackHandler(true, onBackClick)
|
val backgroundColor = TangemTheme.colors.background.secondary
|
||||||
|
|
||||||
|
BackHandler(onBack = onBackClick)
|
||||||
|
SystemBarsEffect {
|
||||||
|
setSystemBarsColor(backgroundColor)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|
@ -37,33 +42,30 @@ fun SettingsScreensScaffold(
|
||||||
modifier = modifier.systemBarsPadding(),
|
modifier = modifier.systemBarsPadding(),
|
||||||
backgroundColor = backgroundColor,
|
backgroundColor = backgroundColor,
|
||||||
floatingActionButton = { fab?.invoke() },
|
floatingActionButton = { fab?.invoke() },
|
||||||
) {
|
) { paddings ->
|
||||||
if (titleRes != null) {
|
Column(
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
modifier = Modifier
|
||||||
background?.invoke()
|
.padding(paddings)
|
||||||
|
.fillMaxSize(),
|
||||||
Column(modifier = modifier.fillMaxWidth()) {
|
) {
|
||||||
Text(
|
if (titleRes != null) {
|
||||||
text = stringResource(id = titleRes),
|
Text(
|
||||||
modifier = modifier.padding(
|
text = stringResource(id = titleRes),
|
||||||
start = TangemTheme.dimens.spacing20,
|
modifier = Modifier
|
||||||
end = TangemTheme.dimens.spacing20,
|
.padding(horizontal = TangemTheme.dimens.spacing20)
|
||||||
bottom = TangemTheme.dimens.spacing54,
|
.padding(bottom = TangemTheme.dimens.spacing36),
|
||||||
),
|
style = TangemTheme.typography.h1,
|
||||||
style = TangemTheme.typography.h1,
|
color = TangemTheme.colors.text.primary1,
|
||||||
color = TangemTheme.colors.text.primary1,
|
)
|
||||||
)
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
|
internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = titleRes),
|
text = stringResource(id = titleRes),
|
||||||
modifier = modifier.padding(start = 20.dp, end = 20.dp),
|
modifier = modifier.padding(start = 20.dp, end = 20.dp),
|
||||||
|
|
@ -73,7 +75,7 @@ fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyTopBarWithNavigation(
|
internal fun EmptyTopBarWithNavigation(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
backgroundColor: Color = TangemTheme.colors.background.primary,
|
backgroundColor: Color = TangemTheme.colors.background.primary,
|
||||||
) {
|
) {
|
||||||
|
|
@ -95,7 +97,12 @@ fun EmptyTopBarWithNavigation(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) {
|
internal fun DetailsMainButton(
|
||||||
|
title: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
) {
|
||||||
PrimaryButtonIconEnd(
|
PrimaryButtonIconEnd(
|
||||||
text = title,
|
text = title,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
|
|
@ -107,7 +114,7 @@ fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = M
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
|
internal fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,48 @@
|
||||||
package com.tangem.tap.features.details.ui.details
|
package com.tangem.tap.features.details.ui.details
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.View
|
import androidx.compose.ui.Modifier
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.transition.TransitionInflater
|
import androidx.transition.TransitionInflater
|
||||||
import com.tangem.core.analytics.Analytics
|
import com.tangem.core.analytics.Analytics
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.screen.ComposeFragment
|
||||||
|
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||||
import com.tangem.tap.common.analytics.events.Settings
|
import com.tangem.tap.common.analytics.events.Settings
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.rekotlin.StoreSubscriber
|
import org.rekotlin.StoreSubscriber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
|
@AndroidEntryPoint
|
||||||
|
internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||||
|
|
||||||
private val detailsViewModel = DetailsViewModel(store)
|
private val detailsViewModel = DetailsViewModel(store)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Analytics.send(Settings.ScreenOpened())
|
Analytics.send(Settings.ScreenOpened())
|
||||||
val inflater = TransitionInflater.from(requireContext())
|
|
||||||
enterTransition = inflater.inflateTransition(R.transition.fade)
|
|
||||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
@Composable
|
||||||
return ComposeView(requireContext()).apply {
|
override fun ScreenContent(modifier: Modifier) {
|
||||||
setContent {
|
DetailsScreen(
|
||||||
isTransitionGroup = true
|
modifier = modifier,
|
||||||
TangemTheme {
|
state = detailsViewModel.detailsScreenState.value,
|
||||||
DetailsScreen(
|
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||||
state = detailsViewModel.detailsScreenState.value,
|
)
|
||||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
}
|
||||||
)
|
|
||||||
}
|
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||||
}
|
enterTransition = inflateTransition(R.transition.fade)
|
||||||
}
|
exitTransition = inflateTransition(R.transition.fade)
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,7 @@
|
||||||
package com.tangem.tap.features.details.ui.details
|
package com.tangem.tap.features.details.ui.details
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
|
@ -27,12 +16,12 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.colorResource
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.tangem.core.ui.components.SystemBarsEffect
|
import com.tangem.core.ui.components.SpacerH
|
||||||
|
import com.tangem.core.ui.components.SpacerHMax
|
||||||
import com.tangem.core.ui.res.TangemColorPalette
|
import com.tangem.core.ui.res.TangemColorPalette
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
import com.tangem.tap.features.details.ui.common.ScreenTitle
|
import com.tangem.tap.features.details.ui.common.ScreenTitle
|
||||||
|
|
@ -41,58 +30,69 @@ import com.tangem.wallet.R
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) {
|
internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
SystemBarsEffect {
|
|
||||||
setSystemBarsColor(color = TangemColorPalette.Light1)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsScreensScaffold(
|
SettingsScreensScaffold(
|
||||||
|
modifier = modifier,
|
||||||
content = { Content(state = state) },
|
content = { Content(state = state) },
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Content(state: DetailsScreenState) {
|
private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) {
|
||||||
Box {
|
Box(modifier = modifier) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState()),
|
.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp))
|
ScreenTitle(titleRes = R.string.details_title)
|
||||||
state.elements.map { element ->
|
SpacerH(height = TangemTheme.dimens.spacing36)
|
||||||
if (element == SettingsElement.WalletConnect) {
|
SettingsItems(
|
||||||
WalletConnectDetailsItem(onItemsClick = state.onItemsClick)
|
items = state.elements,
|
||||||
} else {
|
onItemsClick = state.onItemsClick,
|
||||||
DetailsItem(
|
|
||||||
item = element,
|
|
||||||
appCurrency = state.appCurrency,
|
|
||||||
onItemsClick = { state.onItemsClick(element) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick)
|
|
||||||
Spacer(modifier = Modifier.size(12.dp))
|
|
||||||
Text(
|
|
||||||
text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}",
|
|
||||||
style = TangemTheme.typography.caption,
|
|
||||||
color = colorResource(id = R.color.text_tertiary),
|
|
||||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp),
|
|
||||||
)
|
)
|
||||||
|
SpacerHMax()
|
||||||
|
TangemSocialAccounts(
|
||||||
|
links = state.tangemLinks,
|
||||||
|
onSocialNetworkClick = state.onSocialNetworkClick,
|
||||||
|
)
|
||||||
|
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||||
|
TangemAppVersion(
|
||||||
|
appNameRes = state.appNameRes,
|
||||||
|
version = state.tangemVersion,
|
||||||
|
)
|
||||||
|
SpacerH(height = TangemTheme.dimens.spacing24)
|
||||||
}
|
}
|
||||||
ShowSnackbarIfNeeded(state.showErrorSnackbar.value)
|
ShowSnackbarIfNeeded(state.showErrorSnackbar.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
|
private fun SettingsItems(items: List<SettingsElement>, onItemsClick: (SettingsElement) -> Unit) {
|
||||||
|
items.forEach { item ->
|
||||||
|
val onItemClick = remember(item) {
|
||||||
|
{ onItemsClick(item) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item == SettingsElement.WalletConnect) {
|
||||||
|
WalletConnectDetailsItem(onItemClick)
|
||||||
|
} else {
|
||||||
|
DetailsItem(
|
||||||
|
item = item,
|
||||||
|
onItemClick = onItemClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WalletConnectDetailsItem(onItemClick: () -> Unit) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.defaultMinSize(minHeight = 84.dp)
|
.defaultMinSize(minHeight = 84.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onItemsClick(SettingsElement.WalletConnect) },
|
.clickable(onClick = onItemClick),
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|
@ -100,7 +100,7 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
|
||||||
painter = painterResource(id = R.drawable.ic_walletconnect),
|
painter = painterResource(id = R.drawable.ic_walletconnect),
|
||||||
contentDescription = stringResource(id = R.string.wallet_connect_title),
|
contentDescription = stringResource(id = R.string.wallet_connect_title),
|
||||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
||||||
tint = colorResource(id = R.color.all_colors_azure),
|
tint = TangemColorPalette.Azure,
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.defaultMinSize(minHeight = 56.dp),
|
modifier = Modifier.defaultMinSize(minHeight = 56.dp),
|
||||||
|
|
@ -111,25 +111,25 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
|
||||||
text = stringResource(id = R.string.wallet_connect_title),
|
text = stringResource(id = R.string.wallet_connect_title),
|
||||||
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
|
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
|
||||||
style = TangemTheme.typography.h3,
|
style = TangemTheme.typography.h3,
|
||||||
color = colorResource(id = R.color.text_primary_1),
|
color = TangemTheme.colors.text.primary1,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.wallet_connect_subtitle),
|
text = stringResource(id = R.string.wallet_connect_subtitle),
|
||||||
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
|
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
|
||||||
style = TangemTheme.typography.body1,
|
style = TangemTheme.typography.body1,
|
||||||
color = colorResource(id = R.color.text_secondary),
|
color = TangemTheme.colors.text.secondary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () -> Unit) {
|
private fun DetailsItem(item: SettingsElement, onItemClick: () -> Unit) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onItemsClick),
|
.clickable(onClick = onItemClick),
|
||||||
horizontalArrangement = Arrangement.Start,
|
horizontalArrangement = Arrangement.Start,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
|
|
@ -137,28 +137,21 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () ->
|
||||||
painter = painterResource(id = item.iconRes),
|
painter = painterResource(id = item.iconRes),
|
||||||
contentDescription = stringResource(id = item.titleRes),
|
contentDescription = stringResource(id = item.titleRes),
|
||||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
modifier = Modifier.padding(start = 20.dp, end = 20.dp),
|
||||||
tint = colorResource(id = R.color.icon_secondary),
|
tint = TangemTheme.colors.icon.secondary,
|
||||||
)
|
)
|
||||||
Column(modifier = Modifier.padding(end = 20.dp)) {
|
Column(modifier = Modifier.padding(end = 20.dp)) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = item.titleRes),
|
text = stringResource(id = item.titleRes),
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
style = TangemTheme.typography.subtitle1,
|
style = TangemTheme.typography.subtitle1,
|
||||||
color = colorResource(id = R.color.text_primary_1),
|
color = TangemTheme.colors.text.primary1,
|
||||||
)
|
)
|
||||||
if (item == SettingsElement.AppCurrency) {
|
|
||||||
Text(
|
|
||||||
text = appCurrency,
|
|
||||||
style = TangemTheme.typography.body2,
|
|
||||||
color = colorResource(id = R.color.text_secondary),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
|
private fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
|
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -170,14 +163,14 @@ fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
.clickable { onSocialNetworkClick(it) },
|
.clickable { onSocialNetworkClick(it) },
|
||||||
tint = colorResource(id = R.color.icon_informative),
|
tint = TangemTheme.colors.icon.informative,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
|
private fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
SnackbarHost(
|
SnackbarHost(
|
||||||
|
|
@ -207,17 +200,43 @@ fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) {
|
||||||
private fun Preview() {
|
Text(
|
||||||
|
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||||
|
text = "${stringResource(id = appNameRes)} $version",
|
||||||
|
style = TangemTheme.typography.caption,
|
||||||
|
color = TangemTheme.colors.text.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Preview
|
||||||
|
@Composable
|
||||||
|
private fun DetailsScreenContentSample() {
|
||||||
DetailsScreen(
|
DetailsScreen(
|
||||||
state = DetailsScreenState(
|
state = DetailsScreenState(
|
||||||
elements = SettingsElement.values().toList(),
|
elements = SettingsElement.values().toList(),
|
||||||
tangemLinks = TangemSocialAccounts.accountsEn,
|
tangemLinks = TangemSocialAccounts.accountsEn,
|
||||||
tangemVersion = "Tangem 2.14.12 (343)",
|
tangemVersion = "Tangem 2.14.12 (343)",
|
||||||
appCurrency = "Dollar",
|
|
||||||
onItemsClick = {},
|
onItemsClick = {},
|
||||||
onSocialNetworkClick = {},
|
onSocialNetworkClick = {},
|
||||||
),
|
),
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Composable
|
||||||
|
private fun DetailsScreenContentPreview_Light() {
|
||||||
|
TangemTheme(isDark = false) {
|
||||||
|
DetailsScreenContentSample()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true, widthDp = 360)
|
||||||
|
@Composable
|
||||||
|
private fun DetailsScreenContentPreview_Dark() {
|
||||||
|
TangemTheme(isDark = true) {
|
||||||
|
DetailsScreenContentSample()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// endregion Preview
|
||||||
|
|
@ -6,11 +6,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class DetailsScreenState(
|
internal data class DetailsScreenState(
|
||||||
val elements: List<SettingsElement>,
|
val elements: List<SettingsElement>,
|
||||||
val tangemLinks: List<SocialNetworkLink>,
|
val tangemLinks: List<SocialNetworkLink>,
|
||||||
val tangemVersion: String,
|
val tangemVersion: String,
|
||||||
val appCurrency: String,
|
|
||||||
val onItemsClick: (SettingsElement) -> Unit,
|
val onItemsClick: (SettingsElement) -> Unit,
|
||||||
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
|
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
|
||||||
val showErrorSnackbar: MutableState<EventError> = mutableStateOf(EventError.Empty),
|
val showErrorSnackbar: MutableState<EventError> = mutableStateOf(EventError.Empty),
|
||||||
|
|
@ -19,30 +18,29 @@ data class DetailsScreenState(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
enum class SettingsElement(
|
internal enum class SettingsElement(
|
||||||
val iconRes: Int,
|
val iconRes: Int,
|
||||||
val titleRes: Int,
|
val titleRes: Int,
|
||||||
) {
|
) {
|
||||||
WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title),
|
WalletConnect(R.drawable.ic_walletconnect, R.string.wallet_connect_title),
|
||||||
Chat(R.drawable.ic_chat, R.string.details_chat),
|
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
|
||||||
SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback),
|
|
||||||
ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title),
|
ReferralProgram(R.drawable.ic_add_friends, R.string.details_referral_title),
|
||||||
CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title),
|
CardSettings(R.drawable.ic_card_settings, R.string.card_settings_title),
|
||||||
AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency),
|
|
||||||
AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
|
AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
|
||||||
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
|
Chat(R.drawable.ic_chat, R.string.details_chat),
|
||||||
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App
|
SendFeedback(R.drawable.ic_comment, R.string.details_row_title_send_feedback),
|
||||||
|
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App,
|
||||||
PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy),
|
PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy),
|
||||||
TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu),
|
TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu),
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class SocialNetworkLink(
|
internal data class SocialNetworkLink(
|
||||||
val network: SocialNetwork,
|
val network: SocialNetwork,
|
||||||
val url: String,
|
val url: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
sealed class EventError {
|
internal sealed class EventError {
|
||||||
object Empty : EventError()
|
object Empty : EventError()
|
||||||
data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError()
|
data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError()
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +56,7 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) {
|
||||||
object Discord : SocialNetwork("Discord", R.drawable.ic_discord)
|
object Discord : SocialNetwork("Discord", R.drawable.ic_discord)
|
||||||
}
|
}
|
||||||
|
|
||||||
object TangemSocialAccounts {
|
internal object TangemSocialAccounts {
|
||||||
val accountsEn: List<SocialNetworkLink> = listOf(
|
val accountsEn: List<SocialNetworkLink> = listOf(
|
||||||
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
|
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
|
||||||
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
|
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
|
||||||
|
|
|
||||||
|
|
@ -7,52 +7,63 @@ import com.tangem.core.navigation.AppScreen
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.domain.common.util.cardTypesResolver
|
import com.tangem.domain.common.util.cardTypesResolver
|
||||||
import com.tangem.tap.common.analytics.events.Settings
|
import com.tangem.tap.common.analytics.events.Settings
|
||||||
|
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||||
import com.tangem.tap.common.feedback.SupportInfo
|
import com.tangem.tap.common.feedback.SupportInfo
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
import com.tangem.tap.common.redux.global.GlobalAction
|
import com.tangem.tap.common.redux.global.GlobalAction
|
||||||
|
import com.tangem.tap.features.details.redux.DetailsAction
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||||
import com.tangem.tap.features.home.LocaleRegionProvider
|
import com.tangem.tap.features.home.LocaleRegionProvider
|
||||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||||
|
import com.tangem.tap.scope
|
||||||
|
import com.tangem.tap.userWalletsListManager
|
||||||
import com.tangem.wallet.BuildConfig
|
import com.tangem.wallet.BuildConfig
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
|
|
||||||
class DetailsViewModel(private val store: Store<AppState>) {
|
internal class DetailsViewModel(private val store: Store<AppState>) {
|
||||||
|
|
||||||
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
|
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@Suppress("ComplexMethod")
|
init {
|
||||||
|
bootstrapScreenState()
|
||||||
|
}
|
||||||
|
|
||||||
fun updateState(state: DetailsState): DetailsScreenState {
|
fun updateState(state: DetailsState): DetailsScreenState {
|
||||||
val cardTypesResolver = state.scanResponse?.cardTypesResolver
|
return DetailsScreenState(
|
||||||
val settings = SettingsElement.values().mapNotNull {
|
elements = createSettingsItems(state),
|
||||||
|
tangemLinks = getSocialLinks(),
|
||||||
|
tangemVersion = getTangemAppVersion(),
|
||||||
|
onItemsClick = { handleClickingSettingsItem(it) },
|
||||||
|
onSocialNetworkClick = { handleSocialNetworkClick(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
private fun createSettingsItems(state: DetailsState): List<SettingsElement> {
|
||||||
|
val scanResponse = state.scanResponse ?: return emptyList()
|
||||||
|
val cardTypesResolver = scanResponse.cardTypesResolver
|
||||||
|
|
||||||
|
return SettingsElement.values().mapNotNull {
|
||||||
when (it) {
|
when (it) {
|
||||||
SettingsElement.WalletConnect -> {
|
SettingsElement.WalletConnect -> if (cardTypesResolver.isMultiwalletAllowed()) it else null
|
||||||
if (cardTypesResolver?.isMultiwalletAllowed() == true) it else null
|
|
||||||
}
|
|
||||||
SettingsElement.SendFeedback -> it
|
SettingsElement.SendFeedback -> it
|
||||||
SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null
|
SettingsElement.LinkMoreCards -> if (state.createBackupAllowed) it else null
|
||||||
SettingsElement.PrivacyPolicy -> {
|
SettingsElement.PrivacyPolicy -> if (state.privacyPolicyUrl != null) it else null
|
||||||
if (state.privacyPolicyUrl != null) it else null
|
|
||||||
}
|
|
||||||
SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null
|
SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null
|
||||||
SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null
|
SettingsElement.ReferralProgram -> if (cardTypesResolver.isTangemWallet()) it else null
|
||||||
SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null
|
|
||||||
SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null
|
SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null
|
||||||
else -> it
|
else -> it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return DetailsScreenState(
|
|
||||||
elements = settings,
|
|
||||||
tangemLinks = getSocialLinks(),
|
|
||||||
tangemVersion = getTangemAppVersion(),
|
|
||||||
appCurrency = state.appCurrency.name,
|
|
||||||
onItemsClick = { handleClickingSettingsItem(it) },
|
|
||||||
onSocialNetworkClick = { handleSocialNetworkClick(it) },
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
|
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
|
||||||
|
|
@ -78,9 +89,6 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
||||||
Analytics.send(Settings.ButtonCardSettings())
|
Analytics.send(Settings.ButtonCardSettings())
|
||||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
|
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
|
||||||
}
|
}
|
||||||
SettingsElement.AppCurrency -> {
|
|
||||||
store.dispatch(WalletAction.AppCurrencyAction.ChooseAppCurrency)
|
|
||||||
}
|
|
||||||
SettingsElement.AppSettings -> {
|
SettingsElement.AppSettings -> {
|
||||||
Analytics.send(Settings.ButtonAppSettings())
|
Analytics.send(Settings.ButtonAppSettings())
|
||||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
|
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
|
||||||
|
|
@ -118,4 +126,14 @@ class DetailsViewModel(private val store: Store<AppState>) {
|
||||||
val versionName: String = BuildConfig.VERSION_NAME
|
val versionName: String = BuildConfig.VERSION_NAME
|
||||||
return "$versionName ($versionCode)"
|
return "$versionName ($versionCode)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun bootstrapScreenState() {
|
||||||
|
userWalletsListManager.selectedUserWallet
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.onEach { selectedUserWallet ->
|
||||||
|
store.dispatchWithMain(DetailsAction.PrepareScreen(selectedUserWallet.scanResponse))
|
||||||
|
}
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,47 +1,45 @@
|
||||||
package com.tangem.tap.features.details.ui.resetcard
|
package com.tangem.tap.features.details.ui.resetcard
|
||||||
|
|
||||||
import android.os.Bundle
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.transition.TransitionInflater
|
import androidx.transition.TransitionInflater
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.screen.ComposeFragment
|
||||||
|
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.rekotlin.StoreSubscriber
|
import org.rekotlin.StoreSubscriber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ResetCardFragment : Fragment(), StoreSubscriber<DetailsState> {
|
@AndroidEntryPoint
|
||||||
|
internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||||
|
|
||||||
private val viewModel = ResetCardViewModel(store)
|
private val viewModel = ResetCardViewModel(store)
|
||||||
|
|
||||||
private var screenState: MutableState<ResetCardScreenState> =
|
private var screenState: MutableState<ResetCardScreenState> =
|
||||||
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
|
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
@Composable
|
||||||
super.onCreate(savedInstanceState)
|
override fun ScreenContent(modifier: Modifier) {
|
||||||
|
ResetCardScreen(
|
||||||
val inflater = TransitionInflater.from(requireContext())
|
modifier = modifier,
|
||||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
state = screenState.value,
|
||||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||||
return ComposeView(requireContext()).apply {
|
enterTransition = inflateTransition(R.transition.fade)
|
||||||
setContent {
|
exitTransition = inflateTransition(R.transition.fade)
|
||||||
isTransitionGroup = true
|
|
||||||
TangemTheme {
|
return true
|
||||||
ResetCardScreen(
|
|
||||||
state = screenState.value,
|
|
||||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,12 @@
|
||||||
package com.tangem.tap.features.details.ui.resetcard
|
package com.tangem.tap.features.details.ui.resetcard
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconToggleButton
|
import androidx.compose.material.IconToggleButton
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
|
@ -34,17 +20,17 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit) {
|
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
SettingsScreensScaffold(
|
SettingsScreensScaffold(
|
||||||
|
modifier = modifier,
|
||||||
content = { ResetCardView(state = state) },
|
content = { ResetCardView(state = state) },
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
backgroundColor = Color.Transparent,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod", "MagicNumber")
|
@Suppress("LongMethod", "MagicNumber")
|
||||||
@Composable
|
@Composable
|
||||||
fun ResetCardView(state: ResetCardScreenState) {
|
private fun ResetCardView(state: ResetCardScreenState) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package com.tangem.tap.features.details.ui.resetcard
|
||||||
|
|
||||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||||
|
|
||||||
data class ResetCardScreenState(
|
internal data class ResetCardScreenState(
|
||||||
val accepted: Boolean = false,
|
val accepted: Boolean = false,
|
||||||
val descriptionText: TextReference,
|
val descriptionText: TextReference,
|
||||||
val onAcceptWarningToggleClick: (Boolean) -> Unit,
|
val onAcceptWarningToggleClick: (Boolean) -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||||
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
|
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
|
|
||||||
class ResetCardViewModel(private val store: Store<AppState>) {
|
internal class ResetCardViewModel(private val store: Store<AppState>) {
|
||||||
|
|
||||||
fun updateState(state: CardSettingsState?): ResetCardScreenState {
|
fun updateState(state: CardSettingsState?): ResetCardScreenState {
|
||||||
val descriptionText = state?.cardInfo
|
val descriptionText = state?.cardInfo
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,45 @@
|
||||||
package com.tangem.tap.features.details.ui.securitymode
|
package com.tangem.tap.features.details.ui.securitymode
|
||||||
|
|
||||||
import android.os.Bundle
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.transition.TransitionInflater
|
import androidx.transition.TransitionInflater
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.screen.ComposeFragment
|
||||||
|
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||||
import com.tangem.tap.features.details.redux.DetailsState
|
import com.tangem.tap.features.details.redux.DetailsState
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.rekotlin.StoreSubscriber
|
import org.rekotlin.StoreSubscriber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SecurityModeFragment : Fragment(), StoreSubscriber<DetailsState> {
|
@AndroidEntryPoint
|
||||||
|
internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||||
|
|
||||||
private val viewModel = SecurityModeViewModel(store)
|
private val viewModel = SecurityModeViewModel(store)
|
||||||
|
|
||||||
private var screenState: MutableState<SecurityModeScreenState> =
|
private var screenState: MutableState<SecurityModeScreenState> =
|
||||||
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
|
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
@Composable
|
||||||
super.onCreate(savedInstanceState)
|
override fun ScreenContent(modifier: Modifier) {
|
||||||
|
SecurityModeScreen(
|
||||||
val inflater = TransitionInflater.from(requireContext())
|
modifier = modifier,
|
||||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
state = screenState.value,
|
||||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||||
return ComposeView(requireContext()).apply {
|
enterTransition = inflateTransition(R.transition.fade)
|
||||||
setContent {
|
exitTransition = inflateTransition(R.transition.fade)
|
||||||
isTransitionGroup = true
|
|
||||||
TangemTheme {
|
return true
|
||||||
SecurityModeScreen(
|
|
||||||
state = screenState.value,
|
|
||||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
package com.tangem.tap.features.details.ui.securitymode
|
package com.tangem.tap.features.details.ui.securitymode
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -20,8 +16,13 @@ import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit) {
|
internal fun SecurityModeScreen(
|
||||||
|
state: SecurityModeScreenState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
SettingsScreensScaffold(
|
SettingsScreensScaffold(
|
||||||
|
modifier = modifier,
|
||||||
content = { SecurityModeOptions(state = state) },
|
content = { SecurityModeOptions(state = state) },
|
||||||
// titleRes = R.string.card_settings_security_mode,
|
// titleRes = R.string.card_settings_security_mode,
|
||||||
onBackClick = onBackClick,
|
onBackClick = onBackClick,
|
||||||
|
|
@ -29,7 +30,7 @@ fun SecurityModeScreen(state: SecurityModeScreenState, onBackClick: () -> Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SecurityModeOptions(state: SecurityModeScreenState) {
|
private fun SecurityModeOptions(state: SecurityModeScreenState) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
@ -55,7 +56,7 @@ fun SecurityModeOptions(state: SecurityModeScreenState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
|
private fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
|
||||||
val selected = option == state.selectedSecurityMode
|
val selected = option == state.selectedSecurityMode
|
||||||
|
|
||||||
val title = option.toTitleRes()
|
val title = option.toTitleRes()
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.securitymode
|
||||||
import com.tangem.tap.features.details.redux.SecurityOption
|
import com.tangem.tap.features.details.redux.SecurityOption
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
||||||
data class SecurityModeScreenState(
|
internal data class SecurityModeScreenState(
|
||||||
val availableOptions: List<SecurityOption>,
|
val availableOptions: List<SecurityOption>,
|
||||||
val selectedSecurityMode: SecurityOption,
|
val selectedSecurityMode: SecurityOption,
|
||||||
val isSaveChangesEnabled: Boolean,
|
val isSaveChangesEnabled: Boolean,
|
||||||
|
|
@ -11,7 +11,7 @@ data class SecurityModeScreenState(
|
||||||
val onSaveChangesClicked: () -> Unit,
|
val onSaveChangesClicked: () -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun SecurityOption.toTitleRes(): Int {
|
internal fun SecurityOption.toTitleRes(): Int {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
SecurityOption.LongTap -> R.string.details_manage_security_long_tap
|
SecurityOption.LongTap -> R.string.details_manage_security_long_tap
|
||||||
SecurityOption.PassCode -> R.string.details_manage_security_passcode
|
SecurityOption.PassCode -> R.string.details_manage_security_passcode
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.tangem.tap.features.details.redux.ManageSecurityState
|
||||||
import com.tangem.tap.features.details.redux.SecurityOption
|
import com.tangem.tap.features.details.redux.SecurityOption
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
|
|
||||||
class SecurityModeViewModel(val store: Store<AppState>) {
|
internal class SecurityModeViewModel(val store: Store<AppState>) {
|
||||||
|
|
||||||
fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
|
fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,30 @@
|
||||||
package com.tangem.tap.features.details.ui.walletconnect
|
package com.tangem.tap.features.details.ui.walletconnect
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.transition.TransitionInflater
|
import androidx.transition.TransitionInflater
|
||||||
import com.tangem.core.analytics.Analytics
|
import com.tangem.core.analytics.Analytics
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.core.ui.res.TangemTheme
|
import com.tangem.core.ui.screen.ComposeFragment
|
||||||
|
import com.tangem.core.ui.theme.AppThemeModeHolder
|
||||||
import com.tangem.tap.common.analytics.events.WalletConnect
|
import com.tangem.tap.common.analytics.events.WalletConnect
|
||||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
|
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
|
import com.tangem.wallet.R
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.rekotlin.StoreSubscriber
|
import org.rekotlin.StoreSubscriber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
internal class WalletConnectFragment : ComposeFragment(), StoreSubscriber<WalletConnectState> {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
override lateinit var appThemeModeHolder: AppThemeModeHolder
|
||||||
|
|
||||||
class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
|
|
||||||
private val viewModel = WalletConnectViewModel(store)
|
private val viewModel = WalletConnectViewModel(store)
|
||||||
private var screenState: MutableState<WalletConnectScreenState> =
|
private var screenState: MutableState<WalletConnectScreenState> =
|
||||||
mutableStateOf(viewModel.updateState(store.state.walletConnectState))
|
mutableStateOf(viewModel.updateState(store.state.walletConnectState))
|
||||||
|
|
@ -26,32 +32,31 @@ class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Analytics.send(WalletConnect.ScreenOpened())
|
Analytics.send(WalletConnect.ScreenOpened())
|
||||||
val inflater = TransitionInflater.from(requireContext())
|
|
||||||
enterTransition = inflater.inflateTransition(android.R.transition.fade)
|
|
||||||
exitTransition = inflater.inflateTransition(android.R.transition.fade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
@Composable
|
||||||
return ComposeView(requireContext()).apply {
|
override fun ScreenContent(modifier: Modifier) {
|
||||||
setContent {
|
WalletConnectScreen(
|
||||||
isTransitionGroup = true
|
modifier = modifier,
|
||||||
TangemTheme {
|
state = screenState.value,
|
||||||
WalletConnectScreen(
|
onBackClick = {
|
||||||
state = screenState.value,
|
if (screenState.value.isLoading) {
|
||||||
onBackClick = {
|
store.dispatch(
|
||||||
if (screenState.value.isLoading) {
|
WalletConnectAction.FailureEstablishingSession(
|
||||||
store.dispatch(
|
store.state.walletConnectState.newSessionData?.session?.session,
|
||||||
WalletConnectAction.FailureEstablishingSession(
|
),
|
||||||
store.state.walletConnectState.newSessionData?.session?.session,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
store.dispatch(NavigationAction.PopBackTo())
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
store.dispatch(NavigationAction.PopBackTo())
|
||||||
}
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun TransitionInflater.inflateTransitions(): Boolean {
|
||||||
|
enterTransition = inflateTransition(R.transition.fade)
|
||||||
|
exitTransition = inflateTransition(R.transition.fade)
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,15 @@ import com.tangem.wallet.R
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WalletConnectScreen(state: WalletConnectScreenState, onBackClick: () -> Unit) {
|
internal fun WalletConnectScreen(
|
||||||
|
state: WalletConnectScreenState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
SettingsScreensScaffold(
|
SettingsScreensScaffold(
|
||||||
|
modifier = modifier,
|
||||||
content = {
|
content = {
|
||||||
if (state.sessions.isEmpty()) {
|
if (state.sessions.isEmpty()) {
|
||||||
EmptyScreen(state)
|
EmptyScreen(state)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package com.tangem.tap.features.details.ui.walletconnect
|
||||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class WalletConnectScreenState(
|
internal data class WalletConnectScreenState(
|
||||||
val sessions: ImmutableList<WcSessionForScreen>,
|
val sessions: ImmutableList<WcSessionForScreen>,
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
val onRemoveSession: (String) -> Unit = {},
|
val onRemoveSession: (String) -> Unit = {},
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import kotlinx.collections.immutable.toImmutableList
|
||||||
import org.rekotlin.Store
|
import org.rekotlin.Store
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class WalletConnectViewModel(private val store: Store<AppState>) {
|
internal class WalletConnectViewModel(private val store: Store<AppState>) {
|
||||||
fun updateState(state: WalletConnectState): WalletConnectScreenState {
|
fun updateState(state: WalletConnectState): WalletConnectScreenState {
|
||||||
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
|
Timber.d("WC2 Sessions: ${state.wc2Sessions}")
|
||||||
val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions
|
val sessions = state.sessions.map { wcSession -> WcSessionForScreen.fromSession(wcSession) } + state.wc2Sessions
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
setStatusBarColor(R.color.backgroundLightGray)
|
setStatusBarColor(R.color.background_secondary)
|
||||||
|
|
||||||
webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) }
|
webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) }
|
||||||
store.subscribe(subscriber = this) { state ->
|
store.subscribe(subscriber = this) { state ->
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
setStatusBarColor(R.color.backgroundWhite)
|
setStatusBarColor(R.color.background_primary)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =
|
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ object ResetBackupCardDialog {
|
||||||
setPositiveButton(R.string.common_cancel) { _, _ ->
|
setPositiveButton(R.string.common_cancel) { _, _ ->
|
||||||
Analytics.send(Onboarding.Backup.ResetCancelEvent)
|
Analytics.send(Onboarding.Backup.ResetCancelEvent)
|
||||||
}
|
}
|
||||||
setNegativeButton(R.string.common_reset) { _, _ ->
|
setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ ->
|
||||||
Analytics.send(Onboarding.Backup.ResetPerformEvent)
|
Analytics.send(Onboarding.Backup.ResetPerformEvent)
|
||||||
store.dispatch(BackupAction.ResetBackupCard(cardId))
|
store.dispatch(BackupAction.ResetBackupCard(cardId))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
|
||||||
|
|
||||||
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
|
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
|
||||||
tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
|
tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
|
||||||
val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray
|
val textColor = if (state.inputIsEnabled) R.color.accent else R.color.text_secondary
|
||||||
tvAmountCurrency.setTextColor(fg.getColor(textColor))
|
tvAmountCurrency.setTextColor(fg.getColor(textColor))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ internal object TokensListInteractorModule {
|
||||||
reduxStateHolder = reduxStateHolder,
|
reduxStateHolder = reduxStateHolder,
|
||||||
testnetTokensStorage = testnetTokensStorage,
|
testnetTokensStorage = testnetTokensStorage,
|
||||||
),
|
),
|
||||||
reduxStateHolder = reduxStateHolder,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package com.tangem.tap.features.tokens.impl.di
|
package com.tangem.tap.features.tokens.impl.di
|
||||||
|
|
||||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter
|
import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
|
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
|
@ -18,7 +17,5 @@ internal object TokensListRouterModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ViewModelScoped
|
@ViewModelScoped
|
||||||
fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter {
|
fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter()
|
||||||
return DefaultTokensListRouter(customTokenFeatureToggles)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,252 +1,17 @@
|
||||||
package com.tangem.tap.features.tokens.impl.domain
|
package com.tangem.tap.features.tokens.impl.domain
|
||||||
|
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
|
||||||
import com.tangem.blockchain.common.Blockchain
|
|
||||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
|
||||||
import com.tangem.common.CompletionResult
|
|
||||||
import com.tangem.common.card.EllipticCurve
|
|
||||||
import com.tangem.common.extensions.guard
|
|
||||||
import com.tangem.common.extensions.toMapKey
|
|
||||||
import com.tangem.common.flatMap
|
|
||||||
import com.tangem.crypto.hdWallet.DerivationPath
|
|
||||||
import com.tangem.domain.common.configs.CardConfig
|
|
||||||
import com.tangem.domain.common.util.derivationStyleProvider
|
|
||||||
import com.tangem.domain.common.util.supportsHdWallet
|
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
|
||||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
|
||||||
import com.tangem.tap.*
|
|
||||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
|
||||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
|
||||||
import com.tangem.tap.common.redux.global.GlobalAction
|
|
||||||
import com.tangem.tap.domain.TapError
|
|
||||||
import com.tangem.tap.domain.model.WalletDataModel
|
|
||||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
|
|
||||||
import com.tangem.tap.features.wallet.models.Currency
|
|
||||||
import com.tangem.tap.proxy.AppStateHolder
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default implementation of tokens list interactor
|
* Default implementation of tokens list interactor
|
||||||
* FIXME("Necessary to avoid using redux actions")
|
|
||||||
*
|
*
|
||||||
* @property repository repository of tokens list feature
|
* @property repository repository of tokens list feature
|
||||||
* @property reduxStateHolder redux state holder
|
|
||||||
*/
|
*/
|
||||||
internal class DefaultTokensListInteractor(
|
internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor {
|
||||||
private val repository: TokensListRepository,
|
|
||||||
private val reduxStateHolder: AppStateHolder,
|
|
||||||
) : TokensListInteractor {
|
|
||||||
|
|
||||||
override fun getTokensList(searchText: String): Flow<PagingData<Token>> {
|
override fun getTokensList(searchText: String): Flow<PagingData<Token>> {
|
||||||
return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null }))
|
return repository.getAvailableTokens(searchText = searchText.ifBlank(defaultValue = { null }))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveChanges(tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain>) {
|
|
||||||
val scanResponse = requireNotNull(reduxStateHolder.scanResponse)
|
|
||||||
|
|
||||||
val derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle()
|
|
||||||
val currentTokens = store.state.tokensState.addedWallets
|
|
||||||
.toNonCustomTokensWithBlockchains(derivationStyle = derivationStyle)
|
|
||||||
|
|
||||||
val currentBlockchains = store.state.tokensState.addedWallets
|
|
||||||
.toNonCustomBlockchains(derivationStyle = derivationStyle)
|
|
||||||
|
|
||||||
val blockchainsToAdd = blockchains.filterNot(currentBlockchains::contains)
|
|
||||||
val blockchainsToRemove = currentBlockchains.filterNot(blockchains::contains)
|
|
||||||
|
|
||||||
val tokensToAdd = tokens.filterNot(currentTokens::contains)
|
|
||||||
val tokensToRemove = currentTokens.filterNot { token -> tokens.any { it.token == token.token } }
|
|
||||||
|
|
||||||
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
|
|
||||||
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
|
|
||||||
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
|
|
||||||
store.dispatchDebugErrorNotification(message = "Nothing to save")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(
|
|
||||||
tokens = tokensToRemove,
|
|
||||||
blockchains = blockchainsToRemove,
|
|
||||||
derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(),
|
|
||||||
)
|
|
||||||
|
|
||||||
add(tokens = tokensToAdd, blockchains = blockchainsToAdd, scanResponse = scanResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
|
|
||||||
derivationStyle: DerivationStyle?,
|
|
||||||
): List<TokenWithBlockchain> {
|
|
||||||
return this.map(WalletDataModel::currency)
|
|
||||||
.mapNotNull { currency ->
|
|
||||||
if (currency !is Currency.Token || currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
|
|
||||||
TokenWithBlockchain(token = currency.token, blockchain = currency.blockchain)
|
|
||||||
}
|
|
||||||
.distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
|
|
||||||
return this.map(WalletDataModel::currency)
|
|
||||||
.mapNotNull { currency ->
|
|
||||||
if (currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
|
|
||||||
(currency as? Currency.Blockchain)?.blockchain
|
|
||||||
}
|
|
||||||
.distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun remove(
|
|
||||||
tokens: List<TokenWithBlockchain>,
|
|
||||||
blockchains: List<Blockchain>,
|
|
||||||
derivationStyle: DerivationStyle?,
|
|
||||||
) {
|
|
||||||
val currencies = convertToCurrencies(tokens, blockchains, derivationStyle)
|
|
||||||
if (currencies.isEmpty()) return
|
|
||||||
|
|
||||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
|
||||||
Timber.e("Unable to remove currencies, no user wallet selected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
walletCurrenciesManager.removeCurrencies(userWallet = selectedUserWallet, currenciesToRemove = currencies)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun add(
|
|
||||||
tokens: List<TokenWithBlockchain>,
|
|
||||||
blockchains: List<Blockchain>,
|
|
||||||
scanResponse: ScanResponse,
|
|
||||||
) {
|
|
||||||
val currenciesToAdd = convertToCurrencies(
|
|
||||||
tokens = tokens,
|
|
||||||
blockchains = blockchains,
|
|
||||||
derivationStyle = scanResponse.derivationStyleProvider.getDerivationStyle(),
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO("[REDACTED_TASK_KEY] use DerivationManager")
|
|
||||||
if (scanResponse.supportsHdWallet()) {
|
|
||||||
deriveMissingBlockchains(scanResponse, currenciesToAdd)
|
|
||||||
} else {
|
|
||||||
submitAdd(scanResponse, currenciesToAdd)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun deriveMissingBlockchains(scanResponse: ScanResponse, currencies: List<Currency>) {
|
|
||||||
val config = CardConfig.createConfig(scanResponse.card)
|
|
||||||
val derivations = currencies.mapNotNull {
|
|
||||||
val curve = config.primaryCurve(it.blockchain)
|
|
||||||
curve?.let { getDerivations(curve, scanResponse, currencies) }
|
|
||||||
}.associate(transform = TokensMiddleware.DerivationData::derivations)
|
|
||||||
|
|
||||||
if (derivations.isEmpty()) {
|
|
||||||
submitAdd(scanResponse, currencies)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) {
|
|
||||||
is CompletionResult.Success -> {
|
|
||||||
val newDerivedKeys = result.data.entries
|
|
||||||
val oldDerivedKeys = scanResponse.derivedKeys
|
|
||||||
|
|
||||||
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
|
|
||||||
|
|
||||||
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
|
|
||||||
val oldDerivations = ExtendedPublicKeysMap(map = oldDerivedKeys[walletKey] ?: emptyMap())
|
|
||||||
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(map = emptyMap())
|
|
||||||
ExtendedPublicKeysMap(map = oldDerivations + newDerivations)
|
|
||||||
}
|
|
||||||
|
|
||||||
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
|
|
||||||
|
|
||||||
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
|
|
||||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
|
||||||
|
|
||||||
submitAdd(scanResponse, currencies)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
is CompletionResult.Failure -> {
|
|
||||||
store.dispatchDebugErrorNotification(TapError.CustomError(customMessage = "Error adding tokens"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDerivations(
|
|
||||||
curve: EllipticCurve,
|
|
||||||
scanResponse: ScanResponse,
|
|
||||||
currencyList: List<Currency>,
|
|
||||||
): TokensMiddleware.DerivationData? {
|
|
||||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
|
||||||
|
|
||||||
val manageTokensCandidates = currencyList
|
|
||||||
.map(Currency::blockchain)
|
|
||||||
.distinct()
|
|
||||||
.filter { it.getSupportedCurves().contains(curve) }
|
|
||||||
.mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) }
|
|
||||||
|
|
||||||
val customTokensCandidates = currencyList
|
|
||||||
.filter { it.blockchain.getSupportedCurves().contains(curve) }
|
|
||||||
.mapNotNull(Currency::derivationPath)
|
|
||||||
.map(::DerivationPath)
|
|
||||||
|
|
||||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
|
|
||||||
if (bothCandidates.isEmpty()) return null
|
|
||||||
|
|
||||||
currencyList.find { it is Currency.Blockchain && it.blockchain == Blockchain.Cardano }?.let { currency ->
|
|
||||||
currency.derivationPath?.let {
|
|
||||||
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
|
||||||
val alreadyDerivedKeys = scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
|
||||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
|
||||||
|
|
||||||
val toDerive = bothCandidates.filterNot(alreadyDerivedPaths::contains)
|
|
||||||
if (toDerive.isEmpty()) return null
|
|
||||||
|
|
||||||
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun submitAdd(scanResponse: ScanResponse, currencies: List<Currency>) {
|
|
||||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
|
||||||
Timber.e("Unable to add currencies, no user wallet selected")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userWalletsListManager
|
|
||||||
.update(
|
|
||||||
userWalletId = selectedUserWallet.walletId,
|
|
||||||
update = { userWallet -> userWallet.copy(scanResponse = scanResponse) },
|
|
||||||
)
|
|
||||||
.flatMap { updatedUserWallet ->
|
|
||||||
walletCurrenciesManager.addCurrencies(
|
|
||||||
userWallet = updatedUserWallet,
|
|
||||||
currenciesToAdd = currencies,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun convertToCurrencies(
|
|
||||||
tokens: List<TokenWithBlockchain>,
|
|
||||||
blockchains: List<Blockchain>,
|
|
||||||
derivationStyle: DerivationStyle?,
|
|
||||||
): List<Currency> {
|
|
||||||
return tokens.map { tokenWithBlockchain ->
|
|
||||||
Currency.Token(
|
|
||||||
token = tokenWithBlockchain.token,
|
|
||||||
blockchain = tokenWithBlockchain.blockchain,
|
|
||||||
derivationPath = tokenWithBlockchain.blockchain.derivationPath(derivationStyle)?.rawPath,
|
|
||||||
)
|
|
||||||
}.plus(
|
|
||||||
blockchains.map { blockchain ->
|
|
||||||
Currency.Blockchain(
|
|
||||||
blockchain = blockchain,
|
|
||||||
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
package com.tangem.tap.features.tokens.impl.domain
|
package com.tangem.tap.features.tokens.impl.domain
|
||||||
|
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import com.tangem.blockchain.common.Blockchain
|
|
||||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -15,12 +13,4 @@ internal interface TokensListInteractor {
|
||||||
|
|
||||||
/** Get tokens list using filter by text [searchText] */
|
/** Get tokens list using filter by text [searchText] */
|
||||||
fun getTokensList(searchText: String): Flow<PagingData<Token>>
|
fun getTokensList(searchText: String): Flow<PagingData<Token>>
|
||||||
|
|
||||||
/**
|
|
||||||
* Save added tokens
|
|
||||||
*
|
|
||||||
* @param tokens tokens list that need to save
|
|
||||||
* @param blockchains blockchains list that need to save
|
|
||||||
*/
|
|
||||||
suspend fun saveChanges(tokens: List<TokenWithBlockchain>, blockchains: List<Blockchain>)
|
|
||||||
}
|
}
|
||||||
|
|
@ -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.dispatchDialogShow
|
||||||
import com.tangem.tap.common.extensions.dispatchNotification
|
import com.tangem.tap.common.extensions.dispatchNotification
|
||||||
import com.tangem.tap.common.redux.AppDialog
|
import com.tangem.tap.common.redux.AppDialog
|
||||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
|
|
||||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.wallet.R
|
import com.tangem.wallet.R
|
||||||
|
|
@ -18,20 +16,14 @@ import com.tangem.wallet.R
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
internal class DefaultTokensListRouter(
|
internal class DefaultTokensListRouter : TokensListRouter {
|
||||||
private val customTokenFeatureToggles: CustomTokenFeatureToggles,
|
|
||||||
) : TokensListRouter {
|
|
||||||
|
|
||||||
override fun popBackStack() {
|
override fun popBackStack() {
|
||||||
store.dispatch(NavigationAction.PopBackTo())
|
store.dispatch(NavigationAction.PopBackTo())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openAddCustomTokenScreen() {
|
override fun openAddCustomTokenScreen() {
|
||||||
if (customTokenFeatureToggles.isRedesignedScreenEnabled) {
|
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
|
||||||
} else {
|
|
||||||
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showAddressCopiedNotification() {
|
override fun showAddressCopiedNotification() {
|
||||||
|
|
|
||||||
|
|
@ -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.fromNetworkId
|
||||||
import com.tangem.domain.common.extensions.supportedTokens
|
import com.tangem.domain.common.extensions.supportedTokens
|
||||||
import com.tangem.domain.common.util.cardTypesResolver
|
import com.tangem.domain.common.util.cardTypesResolver
|
||||||
|
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
|
||||||
|
import com.tangem.domain.tokens.TokenWithBlockchain
|
||||||
|
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||||
|
import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles
|
||||||
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
|
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
|
||||||
import com.tangem.tap.common.extensions.getGreyedOutIconRes
|
import com.tangem.tap.common.extensions.getGreyedOutIconRes
|
||||||
import com.tangem.tap.common.extensions.getNetworkName
|
import com.tangem.tap.common.extensions.getNetworkName
|
||||||
|
|
@ -26,14 +30,11 @@ import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor
|
||||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||||
import com.tangem.tap.features.tokens.impl.domain.models.Token.Network
|
import com.tangem.tap.features.tokens.impl.domain.models.Token.Network
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState
|
import com.tangem.tap.features.tokens.impl.presentation.models.SupportTokensState
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.models.TokensListArgs
|
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
|
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
|
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
|
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder
|
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder
|
||||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState
|
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokenWithBlockchain
|
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
|
|
||||||
import com.tangem.tap.proxy.AppStateHolder
|
import com.tangem.tap.proxy.AppStateHolder
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||||
|
|
@ -43,9 +44,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.properties.Delegates
|
||||||
import com.tangem.blockchain.common.Token as BlockchainToken
|
import com.tangem.blockchain.common.Token as BlockchainToken
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -59,6 +62,7 @@ import com.tangem.blockchain.common.Token as BlockchainToken
|
||||||
*
|
*
|
||||||
[REDACTED_AUTHOR]
|
[REDACTED_AUTHOR]
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
internal class TokensListViewModel @Inject constructor(
|
internal class TokensListViewModel @Inject constructor(
|
||||||
private val interactor: TokensListInteractor,
|
private val interactor: TokensListInteractor,
|
||||||
|
|
@ -66,9 +70,12 @@ internal class TokensListViewModel @Inject constructor(
|
||||||
private val dispatchers: AppCoroutineDispatcherProvider,
|
private val dispatchers: AppCoroutineDispatcherProvider,
|
||||||
private val reduxStateHolder: AppStateHolder,
|
private val reduxStateHolder: AppStateHolder,
|
||||||
analyticsEventHandler: AnalyticsEventHandler,
|
analyticsEventHandler: AnalyticsEventHandler,
|
||||||
|
getCurrenciesUseCase: GetCryptoCurrenciesUseCase,
|
||||||
|
getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||||
|
walletFeatureToggles: WalletFeatureToggles,
|
||||||
) : ViewModel(), DefaultLifecycleObserver {
|
) : ViewModel(), DefaultLifecycleObserver {
|
||||||
|
|
||||||
private val args = TokensListArgs()
|
private val isManageAccess = store.state.tokensState.isManageAccess
|
||||||
private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler)
|
private val analyticsSender = TokensListAnalyticsSender(analyticsEventHandler)
|
||||||
private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer())
|
private val actionsHandler = ActionsHandler(router = router, debouncer = Debouncer())
|
||||||
|
|
||||||
|
|
@ -76,15 +83,36 @@ internal class TokensListViewModel @Inject constructor(
|
||||||
var uiState by mutableStateOf(value = getInitialUiState())
|
var uiState by mutableStateOf(value = getInitialUiState())
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private val changedTokensList: MutableList<TokenWithBlockchain> = args.mainScreenTokenList.toMutableList()
|
private var currentTokensList: List<TokenWithBlockchain> by Delegates.notNull()
|
||||||
private val changedBlockchainList: MutableList<Blockchain> = args.mainScreenBlockchainList.toMutableList()
|
private var currentBlockchainList: List<Blockchain> by Delegates.notNull()
|
||||||
|
|
||||||
|
private var changedTokensList: MutableList<TokenWithBlockchain> by Delegates.notNull()
|
||||||
|
private var changedBlockchainList: MutableList<Blockchain> by Delegates.notNull()
|
||||||
|
|
||||||
|
private val tokensListMigration = TokensListMigration(
|
||||||
|
walletFeatureToggles = walletFeatureToggles,
|
||||||
|
getSelectedWalletUseCase = getSelectedWalletUseCase,
|
||||||
|
getCurrenciesUseCase = getCurrenciesUseCase,
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch(dispatchers.main) {
|
||||||
|
val (currentCoins, currentTokens) = tokensListMigration.getCurrentCryptoCurrencies()
|
||||||
|
|
||||||
|
currentBlockchainList = currentCoins
|
||||||
|
currentTokensList = currentTokens
|
||||||
|
|
||||||
|
changedBlockchainList = currentCoins.toMutableList()
|
||||||
|
changedTokensList = currentTokens.toMutableList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(owner: LifecycleOwner) {
|
override fun onCreate(owner: LifecycleOwner) {
|
||||||
if (args.isManageAccess) analyticsSender.sendWhenScreenOpened()
|
if (isManageAccess) analyticsSender.sendWhenScreenOpened()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getInitialUiState(): TokensListStateHolder {
|
private fun getInitialUiState(): TokensListStateHolder {
|
||||||
return if (args.isManageAccess) {
|
return if (isManageAccess) {
|
||||||
TokensListStateHolder.ManageContent(
|
TokensListStateHolder.ManageContent(
|
||||||
toolbarState = getInitialToolbarState(),
|
toolbarState = getInitialToolbarState(),
|
||||||
isLoading = true,
|
isLoading = true,
|
||||||
|
|
@ -105,7 +133,7 @@ internal class TokensListViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getInitialToolbarState(): TokensListToolbarState {
|
private fun getInitialToolbarState(): TokensListToolbarState {
|
||||||
return if (args.isManageAccess) {
|
return if (isManageAccess) {
|
||||||
TokensListToolbarState.Title.Manage(
|
TokensListToolbarState.Title.Manage(
|
||||||
titleResId = R.string.add_tokens_title,
|
titleResId = R.string.add_tokens_title,
|
||||||
onBackButtonClick = actionsHandler::onBackButtonClick,
|
onBackButtonClick = actionsHandler::onBackButtonClick,
|
||||||
|
|
@ -130,7 +158,7 @@ internal class TokensListViewModel @Inject constructor(
|
||||||
|
|
||||||
return interactor.getTokensList(searchText = searchText).map {
|
return interactor.getTokensList(searchText = searchText).map {
|
||||||
it.map { token ->
|
it.map { token ->
|
||||||
if (args.isManageAccess) createManageTokenContent(token) else createReadTokenContent(token)
|
if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +292,12 @@ internal class TokensListViewModel @Inject constructor(
|
||||||
|
|
||||||
fun onSaveButtonClick() {
|
fun onSaveButtonClick() {
|
||||||
analyticsSender.sendWhenSaveButtonClicked()
|
analyticsSender.sendWhenSaveButtonClicked()
|
||||||
store.dispatch(TokensAction.SaveChanges(changedTokensList, changedBlockchainList))
|
tokensListMigration.onSaveButtonClick(
|
||||||
|
currentTokensList = currentTokensList,
|
||||||
|
currentBlockchainList = currentBlockchainList,
|
||||||
|
changedTokensList = changedTokensList,
|
||||||
|
changedBlockchainList = changedBlockchainList,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSearchValueChange(newValue: String) {
|
private fun onSearchValueChange(newValue: String) {
|
||||||
|
|
@ -291,7 +324,7 @@ internal class TokensListViewModel @Inject constructor(
|
||||||
|
|
||||||
if (isRemoveAction) {
|
if (isRemoveAction) {
|
||||||
val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain }
|
val isTokenWithSameBlockchainFound = changedTokensList.any { it.blockchain == blockchain }
|
||||||
val isAddedOnMainScreen = args.mainScreenBlockchainList.contains(blockchain)
|
val isAddedOnMainScreen = currentBlockchainList.contains(blockchain)
|
||||||
|
|
||||||
if (isTokenWithSameBlockchainFound) {
|
if (isTokenWithSameBlockchainFound) {
|
||||||
router.openUnableHideMainTokenAlert(
|
router.openUnableHideMainTokenAlert(
|
||||||
|
|
@ -341,7 +374,7 @@ internal class TokensListViewModel @Inject constructor(
|
||||||
val isRemoveAction = changedTokensList.contains(token)
|
val isRemoveAction = changedTokensList.contains(token)
|
||||||
|
|
||||||
if (isRemoveAction) {
|
if (isRemoveAction) {
|
||||||
val isAddedOnMainScreen = args.mainScreenTokenList.contains(token)
|
val isAddedOnMainScreen = currentTokensList.contains(token)
|
||||||
|
|
||||||
if (isAddedOnMainScreen) {
|
if (isAddedOnMainScreen) {
|
||||||
router.openRemoveWalletAlert(
|
router.openRemoveWalletAlert(
|
||||||
|
|
|
||||||
|
|
@ -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.guard
|
||||||
import com.tangem.common.extensions.toMapKey
|
import com.tangem.common.extensions.toMapKey
|
||||||
import com.tangem.common.flatMap
|
import com.tangem.common.flatMap
|
||||||
import com.tangem.core.analytics.Analytics
|
|
||||||
import com.tangem.core.navigation.AppScreen
|
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.crypto.hdWallet.DerivationPath
|
import com.tangem.crypto.hdWallet.DerivationPath
|
||||||
import com.tangem.domain.DomainWrapped
|
|
||||||
import com.tangem.domain.common.configs.CardConfig
|
import com.tangem.domain.common.configs.CardConfig
|
||||||
import com.tangem.domain.common.util.derivationStyleProvider
|
import com.tangem.domain.common.util.derivationStyleProvider
|
||||||
import com.tangem.domain.common.util.hasDerivation
|
|
||||||
import com.tangem.domain.common.util.supportsHdWallet
|
import com.tangem.domain.common.util.supportsHdWallet
|
||||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
|
||||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
|
||||||
import com.tangem.domain.models.scan.ScanResponse
|
import com.tangem.domain.models.scan.ScanResponse
|
||||||
import com.tangem.domain.redux.domainStore
|
import com.tangem.domain.tokens.TokenWithBlockchain
|
||||||
|
import com.tangem.domain.tokens.TokensAction
|
||||||
|
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||||
|
import com.tangem.domain.wallets.models.UserWalletId
|
||||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||||
import com.tangem.tap.*
|
import com.tangem.tap.*
|
||||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
|
||||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
import com.tangem.tap.common.redux.global.GlobalAction
|
import com.tangem.tap.common.redux.global.GlobalAction
|
||||||
import com.tangem.tap.domain.TapError
|
import com.tangem.tap.domain.TapError
|
||||||
import com.tangem.tap.domain.model.WalletDataModel
|
|
||||||
import com.tangem.tap.features.wallet.models.Currency
|
import com.tangem.tap.features.wallet.models.Currency
|
||||||
|
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.rekotlin.Middleware
|
import org.rekotlin.Middleware
|
||||||
|
|
@ -43,29 +39,69 @@ object TokensMiddleware {
|
||||||
{ next ->
|
{ next ->
|
||||||
{ action ->
|
{ action ->
|
||||||
when (action) {
|
when (action) {
|
||||||
is TokensAction.SaveChanges -> handleSaveChanges(action)
|
is TokensAction.LegacySaveChanges -> handleLegacySaveChanges(action)
|
||||||
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken()
|
is TokensAction.NewSaveChanges -> handleNewSaveChanges(action)
|
||||||
}
|
}
|
||||||
next(action)
|
next(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
|
private fun handleNewSaveChanges(action: TokensAction.NewSaveChanges) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val scanResponse = store.state.globalState.scanResponse ?: return@launch
|
val scanResponse = action.userWallet.scanResponse
|
||||||
|
|
||||||
val currentTokens = store.state.tokensState.addedTokens
|
val currentTokens = action.currentTokens
|
||||||
val currentBlockchains = store.state.tokensState.addedBlockchains
|
val currentBlockchains = action.currentCoins
|
||||||
|
|
||||||
val blockchainsToAdd = action.blockchains.filterNot(currentBlockchains::contains)
|
val blockchainsToAdd = action.changedCoins.filterNot(currentBlockchains::contains)
|
||||||
val blockchainsToRemove =
|
val blockchainsToRemove = currentBlockchains.filterNot(action.changedCoins::contains)
|
||||||
store.state.tokensState.addedBlockchains.filterNot(action.blockchains::contains)
|
|
||||||
|
|
||||||
val tokensToAdd = action.tokens.filterNot(currentTokens::contains)
|
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
|
||||||
val tokensToRemove = currentTokens.filterNot { token -> action.tokens.any { it.token == token.token } }
|
val tokensToRemove = currentTokens.filterNot { token -> action.changedTokens.any { it == token } }
|
||||||
|
|
||||||
removeCurrenciesIfNeeded(
|
removeNewCurrenciesIfNeeded(
|
||||||
|
userWalletId = action.userWallet.walletId,
|
||||||
|
currencies = blockchainsToRemove + tokensToRemove,
|
||||||
|
)
|
||||||
|
|
||||||
|
val isNothingToDoWithTokens = tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
|
||||||
|
val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
|
||||||
|
if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) {
|
||||||
|
store.dispatchDebugErrorNotification(message = "Nothing to save")
|
||||||
|
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val currencyList = blockchainsToAdd + tokensToAdd
|
||||||
|
|
||||||
|
if (scanResponse.supportsHdWallet()) {
|
||||||
|
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
|
||||||
|
submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList)
|
||||||
|
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
submitNewAdd(userWalletId = action.userWallet.walletId, currencyList = currencyList)
|
||||||
|
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleLegacySaveChanges(action: TokensAction.LegacySaveChanges) {
|
||||||
|
scope.launch {
|
||||||
|
val scanResponse = action.scanResponse
|
||||||
|
|
||||||
|
val currentTokens = action.currentTokens
|
||||||
|
val currentBlockchains = action.currentBlockchains
|
||||||
|
|
||||||
|
val blockchainsToAdd = action.changedBlockchains.filterNot(currentBlockchains::contains)
|
||||||
|
val blockchainsToRemove = currentBlockchains.filterNot(action.changedBlockchains::contains)
|
||||||
|
|
||||||
|
val tokensToAdd = action.changedTokens.filterNot(currentTokens::contains)
|
||||||
|
val tokensToRemove =
|
||||||
|
currentTokens.filterNot { token -> action.changedTokens.any { it.token == token.token } }
|
||||||
|
|
||||||
|
removeLegacyCurrenciesIfNeeded(
|
||||||
currencies = convertToCurrencies(
|
currencies = convertToCurrencies(
|
||||||
blockchains = blockchainsToRemove,
|
blockchains = blockchainsToRemove,
|
||||||
tokens = tokensToRemove,
|
tokens = tokensToRemove,
|
||||||
|
|
@ -89,11 +125,11 @@ object TokensMiddleware {
|
||||||
|
|
||||||
if (scanResponse.supportsHdWallet()) {
|
if (scanResponse.supportsHdWallet()) {
|
||||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
deriveMissingBlockchains(scanResponse, currencyList) {
|
||||||
submitAdd(it, currencyList)
|
submitLegacyAdd(it, currencyList)
|
||||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
submitAdd(scanResponse, currencyList)
|
submitLegacyAdd(scanResponse, currencyList)
|
||||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -104,15 +140,14 @@ object TokensMiddleware {
|
||||||
tokens: List<TokenWithBlockchain>,
|
tokens: List<TokenWithBlockchain>,
|
||||||
derivationStyle: DerivationStyle?,
|
derivationStyle: DerivationStyle?,
|
||||||
): List<Currency> {
|
): List<Currency> {
|
||||||
return blockchains.map {
|
return blockchains.map { Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath) } +
|
||||||
Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath)
|
tokens.map {
|
||||||
} + tokens.map {
|
Currency.Token(
|
||||||
Currency.Token(
|
token = it.token,
|
||||||
it.token,
|
blockchain = it.blockchain,
|
||||||
it.blockchain,
|
derivationPath = it.blockchain.derivationPath(derivationStyle)?.rawPath,
|
||||||
it.blockchain.derivationPath(derivationStyle)?.rawPath,
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deriveMissingBlockchains(
|
private fun deriveMissingBlockchains(
|
||||||
|
|
@ -123,7 +158,7 @@ object TokensMiddleware {
|
||||||
val config = CardConfig.createConfig(scanResponse.card)
|
val config = CardConfig.createConfig(scanResponse.card)
|
||||||
val derivationDataList = currencyList.mapNotNull {
|
val derivationDataList = currencyList.mapNotNull {
|
||||||
val curve = config.primaryCurve(it.blockchain)
|
val curve = config.primaryCurve(it.blockchain)
|
||||||
curve?.let { getDerivations(curve, scanResponse, currencyList) }
|
curve?.let { getLegacyDerivations(curve, scanResponse, currencyList) }
|
||||||
}
|
}
|
||||||
val derivations = derivationDataList.associate { it.derivations }
|
val derivations = derivationDataList.associate { it.derivations }
|
||||||
if (derivations.isEmpty()) {
|
if (derivations.isEmpty()) {
|
||||||
|
|
@ -163,7 +198,55 @@ object TokensMiddleware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDerivations(
|
private fun deriveMissingCoins(
|
||||||
|
scanResponse: ScanResponse,
|
||||||
|
currencyList: List<CryptoCurrency>,
|
||||||
|
onSuccess: (ScanResponse) -> Unit,
|
||||||
|
) {
|
||||||
|
val config = CardConfig.createConfig(scanResponse.card)
|
||||||
|
val derivationDataList = currencyList.mapNotNull {
|
||||||
|
config.primaryCurve(blockchain = Blockchain.fromId(it.network.id.value))
|
||||||
|
?.let { curve -> getNewDerivations(curve, scanResponse, currencyList) }
|
||||||
|
}
|
||||||
|
val derivations = derivationDataList.associate(DerivationData::derivations)
|
||||||
|
if (derivations.isEmpty()) {
|
||||||
|
onSuccess(scanResponse)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
val result = tangemSdkManager.derivePublicKeys(
|
||||||
|
cardId = null,
|
||||||
|
derivations = derivations,
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
is CompletionResult.Success -> {
|
||||||
|
val newDerivedKeys = result.data.entries
|
||||||
|
val oldDerivedKeys = scanResponse.derivedKeys
|
||||||
|
|
||||||
|
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
|
||||||
|
|
||||||
|
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
|
||||||
|
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
|
||||||
|
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||||
|
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||||
|
}
|
||||||
|
val updatedScanResponse = scanResponse.copy(
|
||||||
|
derivedKeys = updatedDerivedKeys,
|
||||||
|
)
|
||||||
|
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
|
||||||
|
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||||
|
|
||||||
|
onSuccess(updatedScanResponse)
|
||||||
|
}
|
||||||
|
is CompletionResult.Failure -> {
|
||||||
|
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLegacyDerivations(
|
||||||
curve: EllipticCurve,
|
curve: EllipticCurve,
|
||||||
scanResponse: ScanResponse,
|
scanResponse: ScanResponse,
|
||||||
currencyList: List<Currency>,
|
currencyList: List<Currency>,
|
||||||
|
|
@ -200,9 +283,48 @@ object TokensMiddleware {
|
||||||
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
|
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getNewDerivations(
|
||||||
|
curve: EllipticCurve,
|
||||||
|
scanResponse: ScanResponse,
|
||||||
|
currencyList: List<CryptoCurrency>,
|
||||||
|
): DerivationData? {
|
||||||
|
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||||
|
|
||||||
|
val manageTokensCandidates = currencyList
|
||||||
|
.map { Blockchain.fromId(it.network.id.value) }
|
||||||
|
.distinct()
|
||||||
|
.filter { it.getSupportedCurves().contains(curve) }
|
||||||
|
.mapNotNull { it.derivationPath(scanResponse.derivationStyleProvider.getDerivationStyle()) }
|
||||||
|
|
||||||
|
val customTokensCandidates = currencyList
|
||||||
|
.filter { Blockchain.fromId(it.network.id.value).getSupportedCurves().contains(curve) }
|
||||||
|
.mapNotNull(CryptoCurrency::derivationPath)
|
||||||
|
.map(::DerivationPath)
|
||||||
|
|
||||||
|
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct().toMutableList()
|
||||||
|
if (bothCandidates.isEmpty()) return null
|
||||||
|
|
||||||
|
currencyList.find { it is CryptoCurrency.Coin && Blockchain.fromId(it.network.id.value) == Blockchain.Cardano }
|
||||||
|
?.let { currency ->
|
||||||
|
currency.derivationPath?.let {
|
||||||
|
bothCandidates.add(CardanoUtils.extendedDerivationPath(DerivationPath(it)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||||
|
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||||
|
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||||
|
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||||
|
|
||||||
|
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||||
|
if (toDerive.isEmpty()) return null
|
||||||
|
|
||||||
|
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
|
||||||
|
}
|
||||||
|
|
||||||
class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
|
class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
|
||||||
|
|
||||||
private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
|
private fun submitLegacyAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
|
||||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
||||||
Timber.e("Unable to add currencies, no user wallet selected")
|
Timber.e("Unable to add currencies, no user wallet selected")
|
||||||
return
|
return
|
||||||
|
|
@ -223,7 +345,15 @@ object TokensMiddleware {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
|
private fun submitNewAdd(userWalletId: UserWalletId, currencyList: List<CryptoCurrency>) {
|
||||||
|
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun removeLegacyCurrenciesIfNeeded(currencies: List<Currency>) {
|
||||||
if (currencies.isEmpty()) return
|
if (currencies.isEmpty()) return
|
||||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
||||||
Timber.e("Unable to remove currencies, no user wallet selected")
|
Timber.e("Unable to remove currencies, no user wallet selected")
|
||||||
|
|
@ -232,55 +362,10 @@ object TokensMiddleware {
|
||||||
walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies)
|
walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
|
private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||||
return currency.derivationPath?.let {
|
if (currencies.isEmpty()) return
|
||||||
!scanResponse.hasDerivation(currency.blockchain, it)
|
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleAddingCustomToken() = scope.launch {
|
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
|
||||||
val onAddCustomToken = fun(customCurrency: CustomCurrency) {
|
|
||||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
|
||||||
|
|
||||||
fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List<Currency>) {
|
|
||||||
submitAdd(scanResponse, currencyList)
|
|
||||||
// pop from the AddCustomTokenScreen
|
|
||||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
|
||||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
|
||||||
}
|
|
||||||
|
|
||||||
Analytics.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency))
|
|
||||||
val currency = Currency.fromCustomCurrency(customCurrency)
|
|
||||||
val isNeedToDerive = isNeedToDerive(scanResponse, currency)
|
|
||||||
val currencyList = listOf(currency)
|
|
||||||
if (isNeedToDerive) {
|
|
||||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
|
||||||
submitAndPopBack(it, currencyList)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
submitAndPopBack(scanResponse, currencyList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val addedCurrencies = store.state.walletState.walletsStores
|
|
||||||
.map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) }
|
|
||||||
.flatten()
|
|
||||||
.map { currency ->
|
|
||||||
when (currency) {
|
|
||||||
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(
|
|
||||||
currency.blockchain,
|
|
||||||
currency.derivationPath,
|
|
||||||
)
|
|
||||||
|
|
||||||
is Currency.Token -> DomainWrapped.Currency.Token(
|
|
||||||
currency.token,
|
|
||||||
currency.blockchain,
|
|
||||||
currency.derivationPath,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
|
|
||||||
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken))
|
|
||||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,7 @@
|
||||||
package com.tangem.tap.features.tokens.legacy.redux
|
package com.tangem.tap.features.tokens.legacy.redux
|
||||||
|
|
||||||
import com.tangem.blockchain.common.Blockchain
|
import com.tangem.domain.tokens.TokensAction
|
||||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
import com.tangem.tap.domain.model.WalletDataModel
|
|
||||||
import com.tangem.tap.features.wallet.models.Currency
|
|
||||||
import com.tangem.tap.features.wallet.models.Currency.Token
|
|
||||||
import org.rekotlin.Action
|
import org.rekotlin.Action
|
||||||
|
|
||||||
object TokensReducer {
|
object TokensReducer {
|
||||||
|
|
@ -16,40 +12,8 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
|
||||||
if (action !is TokensAction) return state.tokensState
|
if (action !is TokensAction) return state.tokensState
|
||||||
|
|
||||||
return when (action) {
|
return when (action) {
|
||||||
is TokensAction.SetArgs.ManageAccess -> {
|
is TokensAction.SetArgs.ManageAccess -> state.tokensState.copy(isManageAccess = true)
|
||||||
state.tokensState.copy(
|
is TokensAction.SetArgs.ReadAccess -> state.tokensState.copy(isManageAccess = false)
|
||||||
isManageAccess = true,
|
|
||||||
addedWallets = action.wallets,
|
|
||||||
addedBlockchains = action.wallets.toNonCustomBlockchains(action.derivationStyle),
|
|
||||||
addedTokens = action.wallets.toNonCustomTokensWithBlockchains(action.derivationStyle),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
is TokensAction.SetArgs.ReadAccess -> {
|
|
||||||
state.tokensState.copy(isManageAccess = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> state.tokensState
|
else -> state.tokensState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<WalletDataModel>.toNonCustomBlockchains(derivationStyle: DerivationStyle?): List<Blockchain> {
|
|
||||||
return mapNotNull { walletDataModel ->
|
|
||||||
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
(walletDataModel.currency as? Currency.Blockchain)?.blockchain
|
|
||||||
}
|
|
||||||
}.distinct()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<WalletDataModel>.toNonCustomTokensWithBlockchains(
|
|
||||||
derivationStyle: DerivationStyle?,
|
|
||||||
): List<TokenWithBlockchain> {
|
|
||||||
return mapNotNull { walletDataModel ->
|
|
||||||
if (walletDataModel.currency !is Token) return@mapNotNull null
|
|
||||||
if (walletDataModel.currency.isCustomCurrency(derivationStyle)) return@mapNotNull null
|
|
||||||
|
|
||||||
TokenWithBlockchain(walletDataModel.currency.token, walletDataModel.currency.blockchain)
|
|
||||||
}.distinct()
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +1,5 @@
|
||||||
package com.tangem.tap.features.tokens.legacy.redux
|
package com.tangem.tap.features.tokens.legacy.redux
|
||||||
|
|
||||||
import com.tangem.blockchain.common.Blockchain
|
|
||||||
import com.tangem.blockchain.common.Token
|
|
||||||
import com.tangem.tap.domain.model.WalletDataModel
|
|
||||||
import org.rekotlin.StateType
|
import org.rekotlin.StateType
|
||||||
|
|
||||||
data class TokensState(
|
data class TokensState(val isManageAccess: Boolean = false) : StateType
|
||||||
val isManageAccess: Boolean = false,
|
|
||||||
val addedWallets: List<WalletDataModel> = emptyList(),
|
|
||||||
val addedTokens: List<TokenWithBlockchain> = emptyList(),
|
|
||||||
val addedBlockchains: List<Blockchain> = emptyList(),
|
|
||||||
) : StateType
|
|
||||||
|
|
||||||
// TODO: [REDACTED_TASK_KEY] Remove this class
|
|
||||||
data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain)
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
package com.tangem.tap.features.wallet.converters
|
package com.tangem.tap.features.wallet.converters
|
||||||
|
|
||||||
|
import com.tangem.blockchain.common.Blockchain
|
||||||
|
import com.tangem.blockchain.common.Token
|
||||||
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
|
||||||
import com.tangem.domain.common.util.derivationStyleProvider
|
import com.tangem.domain.common.util.derivationStyleProvider
|
||||||
import com.tangem.domain.tokens.models.CryptoCurrency
|
import com.tangem.domain.tokens.models.CryptoCurrency
|
||||||
import com.tangem.tap.features.wallet.models.Currency
|
import com.tangem.tap.features.wallet.models.Currency
|
||||||
import com.tangem.tap.store
|
import com.tangem.tap.store
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.TwoWayConverter
|
||||||
|
|
||||||
internal class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
|
internal class CryptoCurrencyConverter : TwoWayConverter<Currency, CryptoCurrency> {
|
||||||
|
|
||||||
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
|
private val cryptoCurrencyFactory by lazy { CryptoCurrencyFactory() }
|
||||||
|
|
||||||
|
|
@ -40,4 +42,26 @@ internal class CryptoCurrencyConverter : Converter<Currency, CryptoCurrency> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun convertBack(value: CryptoCurrency): Currency {
|
||||||
|
val blockchain = Blockchain.fromId(value.network.id.value)
|
||||||
|
if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain")
|
||||||
|
return when (value) {
|
||||||
|
is CryptoCurrency.Coin -> Currency.Blockchain(
|
||||||
|
blockchain = blockchain,
|
||||||
|
derivationPath = value.derivationPath,
|
||||||
|
)
|
||||||
|
is CryptoCurrency.Token -> Currency.Token(
|
||||||
|
token = Token(
|
||||||
|
name = value.name,
|
||||||
|
symbol = value.symbol,
|
||||||
|
contractAddress = value.contractAddress,
|
||||||
|
decimals = value.decimals,
|
||||||
|
id = value.id.value,
|
||||||
|
),
|
||||||
|
blockchain = blockchain,
|
||||||
|
derivationPath = value.derivationPath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,9 +18,11 @@ import com.tangem.feature.swap.presentation.SwapFragment
|
||||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||||
import com.tangem.tap.common.analytics.events.Token
|
import com.tangem.tap.common.analytics.events.Token
|
||||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||||
|
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||||
import com.tangem.tap.common.redux.AppState
|
import com.tangem.tap.common.redux.AppState
|
||||||
|
import com.tangem.tap.domain.TapError
|
||||||
import com.tangem.tap.domain.tokens.getIconUrl
|
import com.tangem.tap.domain.tokens.getIconUrl
|
||||||
import com.tangem.tap.features.demo.DemoHelper
|
import com.tangem.tap.features.demo.DemoHelper
|
||||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||||
|
|
@ -39,7 +41,10 @@ import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
|
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
class TradeCryptoMiddleware {
|
class TradeCryptoMiddleware {
|
||||||
|
|
||||||
|
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||||
fun handle(state: () -> AppState?, action: TradeCryptoAction) {
|
fun handle(state: () -> AppState?, action: TradeCryptoAction) {
|
||||||
if (DemoHelper.tryHandle(state, action)) return
|
if (DemoHelper.tryHandle(state, action)) return
|
||||||
|
|
||||||
|
|
@ -52,11 +57,10 @@ class TradeCryptoMiddleware {
|
||||||
openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency())
|
openSwap(currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency())
|
||||||
}
|
}
|
||||||
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
|
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
|
||||||
TradeCryptoAction.New.Send -> store.dispatch(WalletAction.Send())
|
|
||||||
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
|
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
|
||||||
is TradeCryptoAction.New.Swap -> {
|
is TradeCryptoAction.New.Swap -> openSwap(currency = action.cryptoCurrency.toSwapCurrency())
|
||||||
openSwap(currency = action.cryptoCurrency.toSwapCurrency())
|
is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action)
|
||||||
}
|
is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -301,4 +305,98 @@ class TradeCryptoMiddleware {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) {
|
||||||
|
val cryptoStatus = action.tokenStatus
|
||||||
|
val currency = cryptoStatus.currency
|
||||||
|
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
val walletManager = store.state.daggerGraphState
|
||||||
|
.get(DaggerGraphState::walletManagersFacade)
|
||||||
|
.getOrCreateWalletManager(
|
||||||
|
userWallet = action.userWallet,
|
||||||
|
blockchain = blockchain,
|
||||||
|
derivationPath = blockchain.derivationPath(
|
||||||
|
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (walletManager == null) {
|
||||||
|
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
|
||||||
|
store.dispatchErrorNotification(error)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type is AmountType.Token }
|
||||||
|
when (currency) {
|
||||||
|
is CryptoCurrency.Coin -> error("Action.tokenStatus.currency is Coin")
|
||||||
|
is CryptoCurrency.Token -> {
|
||||||
|
store.dispatchOnMain(
|
||||||
|
action = PrepareSendScreen(
|
||||||
|
walletManager = walletManager,
|
||||||
|
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
|
||||||
|
coinRate = action.coinFiatRate,
|
||||||
|
tokenAmount = sendableAmounts.first(),
|
||||||
|
tokenRate = cryptoStatus.value.fiatRate,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleNewSendCoin(action: TradeCryptoAction.New.SendCoin) {
|
||||||
|
val cryptoStatus = action.coinStatus
|
||||||
|
val currency = cryptoStatus.currency
|
||||||
|
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
val walletManager = store.state.daggerGraphState
|
||||||
|
.get(DaggerGraphState::walletManagersFacade)
|
||||||
|
.getOrCreateWalletManager(
|
||||||
|
userWallet = action.userWallet,
|
||||||
|
blockchain = blockchain,
|
||||||
|
derivationPath = blockchain.derivationPath(
|
||||||
|
style = action.userWallet.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (walletManager == null) {
|
||||||
|
val error = TapError.UnsupportedState(stateError = "WalletManager is null")
|
||||||
|
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
|
||||||
|
store.dispatchErrorNotification(error)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type == AmountType.Coin }
|
||||||
|
when (currency) {
|
||||||
|
is CryptoCurrency.Coin -> {
|
||||||
|
val amountToSend = sendableAmounts.find { it.currencySymbol == currency.symbol }
|
||||||
|
|
||||||
|
if (amountToSend == null) {
|
||||||
|
val error = TapError.UnsupportedState(stateError = "Amount to send is null")
|
||||||
|
FirebaseCrashlytics.getInstance()
|
||||||
|
.recordException(IllegalStateException(error.stateError))
|
||||||
|
store.dispatchErrorNotification(error)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatchOnMain(
|
||||||
|
action = PrepareSendScreen(
|
||||||
|
walletManager = walletManager,
|
||||||
|
coinAmount = amountToSend,
|
||||||
|
coinRate = cryptoStatus.value.fiatRate,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is CryptoCurrency.Token -> error("Action.tokenStatus.currency is Token")
|
||||||
|
}
|
||||||
|
|
||||||
|
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Send))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -37,7 +37,6 @@ import com.tangem.tap.common.utils.SafeStoreSubscriber
|
||||||
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
|
||||||
import com.tangem.tap.domain.statePrinter.printScanResponseState
|
import com.tangem.tap.domain.statePrinter.printScanResponseState
|
||||||
import com.tangem.tap.domain.statePrinter.printWalletState
|
import com.tangem.tap.domain.statePrinter.printWalletState
|
||||||
import com.tangem.tap.features.details.redux.DetailsAction
|
|
||||||
import com.tangem.tap.features.wallet.redux.ErrorType
|
import com.tangem.tap.features.wallet.redux.ErrorType
|
||||||
import com.tangem.tap.features.wallet.redux.ProgressState
|
import com.tangem.tap.features.wallet.redux.ProgressState
|
||||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||||
|
|
@ -311,17 +310,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
|
||||||
return when (item.itemId) {
|
return when (item.itemId) {
|
||||||
R.id.details_menu -> {
|
R.id.details_menu -> {
|
||||||
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
|
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
|
||||||
store.state.globalState.scanResponse?.let { scanResponse ->
|
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
|
||||||
store.dispatch(
|
|
||||||
DetailsAction.PrepareScreen(
|
true
|
||||||
scanResponse = scanResponse,
|
|
||||||
wallets = store.state.walletState.walletManagers.map { it.wallet },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.badoo.mvicore.modelWatcher
|
||||||
import com.tangem.core.analytics.Analytics
|
import com.tangem.core.analytics.Analytics
|
||||||
import com.tangem.core.navigation.AppScreen
|
import com.tangem.core.navigation.AppScreen
|
||||||
import com.tangem.core.navigation.NavigationAction
|
import com.tangem.core.navigation.NavigationAction
|
||||||
import com.tangem.domain.common.util.derivationStyleProvider
|
import com.tangem.domain.tokens.TokensAction
|
||||||
import com.tangem.tap.common.analytics.events.MainScreen
|
import com.tangem.tap.common.analytics.events.MainScreen
|
||||||
import com.tangem.tap.common.analytics.events.Portfolio
|
import com.tangem.tap.common.analytics.events.Portfolio
|
||||||
import com.tangem.tap.common.entities.FiatCurrency
|
import com.tangem.tap.common.entities.FiatCurrency
|
||||||
|
|
@ -14,7 +14,6 @@ import com.tangem.tap.common.extensions.getQuantityString
|
||||||
import com.tangem.tap.common.extensions.hide
|
import com.tangem.tap.common.extensions.hide
|
||||||
import com.tangem.tap.common.extensions.show
|
import com.tangem.tap.common.extensions.show
|
||||||
import com.tangem.tap.domain.model.TotalFiatBalance
|
import com.tangem.tap.domain.model.TotalFiatBalance
|
||||||
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
|
|
||||||
import com.tangem.tap.features.wallet.redux.ErrorType
|
import com.tangem.tap.features.wallet.redux.ErrorType
|
||||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||||
import com.tangem.tap.features.wallet.redux.WalletState
|
import com.tangem.tap.features.wallet.redux.WalletState
|
||||||
|
|
@ -106,14 +105,8 @@ class MultiWalletView : WalletView() {
|
||||||
binding.btnAddToken.setOnClickListener {
|
binding.btnAddToken.setOnClickListener {
|
||||||
Analytics.send(Portfolio.ButtonManageTokens())
|
Analytics.send(Portfolio.ButtonManageTokens())
|
||||||
|
|
||||||
store.dispatch(
|
store.dispatch(action = TokensAction.SetArgs.ManageAccess)
|
||||||
TokensAction.SetArgs.ManageAccess(
|
store.dispatch(action = NavigationAction.NavigateTo(screen = AppScreen.AddTokens))
|
||||||
wallets = state.walletsDataFromStores,
|
|
||||||
derivationStyle = store.state.globalState.scanResponse
|
|
||||||
?.derivationStyleProvider?.getDerivationStyle(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
|
|
||||||
}
|
}
|
||||||
handleErrorStates(state = state, binding = binding, fragment = fragment)
|
handleErrorStates(state = state, binding = binding, fragment = fragment)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.tokens.UserTokensRepository
|
||||||
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
import com.tangem.tap.domain.walletStores.WalletStoresManager
|
||||||
import com.tangem.tap.features.wallet.redux.WalletState
|
import com.tangem.tap.features.wallet.redux.WalletState
|
||||||
|
import com.tangem.tap.network.exchangeServices.ExchangeService
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import org.rekotlin.Action
|
import org.rekotlin.Action
|
||||||
|
|
@ -45,6 +46,7 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl
|
||||||
var tangemSdkManager: TangemSdkManager? = null
|
var tangemSdkManager: TangemSdkManager? = null
|
||||||
var walletStoresManager: WalletStoresManager? = null
|
var walletStoresManager: WalletStoresManager? = null
|
||||||
var appFiatCurrency: FiatCurrency = FiatCurrency.Default
|
var appFiatCurrency: FiatCurrency = FiatCurrency.Default
|
||||||
|
var exchangeService: ExchangeService? = null
|
||||||
|
|
||||||
fun getActualCard(): CardDTO? {
|
fun getActualCard(): CardDTO? {
|
||||||
return scanResponse?.card
|
return scanResponse?.card
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,11 @@ package com.tangem.tap.proxy.redux
|
||||||
import com.tangem.datasource.asset.AssetReader
|
import com.tangem.datasource.asset.AssetReader
|
||||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||||
|
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||||
import com.tangem.domain.card.ScanCardProcessor
|
import com.tangem.domain.card.ScanCardProcessor
|
||||||
import com.tangem.domain.card.ScanCardUseCase
|
import com.tangem.domain.card.ScanCardUseCase
|
||||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||||
|
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||||
import com.tangem.features.tester.api.TesterRouter
|
import com.tangem.features.tester.api.TesterRouter
|
||||||
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
|
import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles
|
||||||
|
|
@ -16,6 +18,7 @@ import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
|
||||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
|
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
|
||||||
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
|
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
|
||||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||||
|
import com.tangem.tap.proxy.AppStateHolder
|
||||||
import org.rekotlin.StateType
|
import org.rekotlin.StateType
|
||||||
|
|
||||||
data class DaggerGraphState(
|
data class DaggerGraphState(
|
||||||
|
|
@ -35,6 +38,11 @@ data class DaggerGraphState(
|
||||||
val cardSdkConfigRepository: CardSdkConfigRepository? = null,
|
val cardSdkConfigRepository: CardSdkConfigRepository? = null,
|
||||||
val appCurrencyRepository: AppCurrencyRepository? = null,
|
val appCurrencyRepository: AppCurrencyRepository? = null,
|
||||||
val walletManagersFacade: WalletManagersFacade? = null,
|
val walletManagersFacade: WalletManagersFacade? = null,
|
||||||
|
val appStateHolder: AppStateHolder? = null,
|
||||||
|
val appThemeModeRepository: AppThemeModeRepository? = null,
|
||||||
|
|
||||||
|
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
|
||||||
|
val currenciesRepository: CurrenciesRepository? = null,
|
||||||
) : StateType {
|
) : StateType {
|
||||||
|
|
||||||
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {
|
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<!-- 24% opacity -->
|
<!-- 24% opacity -->
|
||||||
<item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_enabled="true" android:state_selected="true" />
|
<item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_enabled="true" android:state_selected="true" />
|
||||||
<item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_checked="true" android:state_enabled="true" />
|
<item android:alpha="0.1" android:color="?attr/colorSecondary" android:state_checked="true" android:state_enabled="true" />
|
||||||
|
|
||||||
<item android:color="@color/backgroundLightGray" android:state_enabled="true" />
|
<item android:color="@color/background_secondary" android:state_enabled="true" />
|
||||||
<item android:color="@color/backgroundLightGray" />
|
<item android:color="@color/background_secondary" />
|
||||||
|
|
||||||
</selector>
|
</selector>
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="?attr/colorSecondary" android:state_enabled="true" android:state_selected="true" />
|
<item android:color="@color/accent" android:state_enabled="true" android:state_selected="true" />
|
||||||
<item android:color="?attr/colorSecondary" android:state_checked="true" android:state_enabled="true" />
|
<item android:color="@color/accent" android:state_checked="true" android:state_enabled="true" />
|
||||||
<!-- 12% of 87% opacity -->
|
|
||||||
<item android:alpha="0.10" android:color="?attr/colorOnSurface" android:state_enabled="true" />
|
<item android:color="@color/text_secondary" android:state_enabled="true" />
|
||||||
<item android:alpha="0.12" android:color="?attr/colorOnSurface" />
|
<item android:color="@color/text_secondary" />
|
||||||
|
|
||||||
</selector>
|
</selector>
|
||||||
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:viewportWidth="14"
|
||||||
android:viewportHeight="19">
|
android:viewportHeight="19">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@color/blue"
|
android:fillColor="@color/accent"
|
||||||
android:pathData="M11,14.51V7.5H9V14.51H6L10,18.5L14,14.51H11ZM4,0.5L0,4.49H3V11.5H5V4.49H8L4,0.5Z" />
|
android:pathData="M11,14.51V7.5H9V14.51H6L10,18.5L14,14.51H11ZM4,0.5L0,4.49H3V11.5H5V4.49H8L4,0.5Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportWidth="16"
|
android:viewportWidth="16"
|
||||||
android:viewportHeight="19">
|
android:viewportHeight="19">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#1C1C1E"
|
android:fillColor="@color/text_primary_1"
|
||||||
android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z" />
|
android:pathData="M13.8333,1.6667H10.35C10,0.7 9.0833,0 8,0C6.9167,0 6,0.7 5.65,1.6667H2.1667C1.25,1.6667 0.5,2.4167 0.5,3.3333V16.6667C0.5,17.5833 1.25,18.3333 2.1667,18.3333H13.8333C14.75,18.3333 15.5,17.5833 15.5,16.6667V3.3333C15.5,2.4167 14.75,1.6667 13.8333,1.6667ZM8,1.6667C8.4583,1.6667 8.8333,2.0417 8.8333,2.5C8.8333,2.9583 8.4583,3.3333 8,3.3333C7.5417,3.3333 7.1667,2.9583 7.1667,2.5C7.1667,2.0417 7.5417,1.6667 8,1.6667ZM13.8333,16.6667H2.1667V3.3333H3.8333V5.8333H12.1667V3.3333H13.8333V16.6667Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,6 @@
|
||||||
android:viewportHeight="19">
|
android:viewportHeight="19">
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#99838383"
|
android:fillColor="@color/text_disabled"
|
||||||
android:pathData="M13.8333 1.66667H10.35C10 0.7 9.08333 0 8 0C6.91667 0 6 0.7 5.65 1.66667H2.16667C1.25 1.66667 0.5 2.41667 0.5 3.33333V16.6667C0.5 17.5833 1.25 18.3333 2.16667 18.3333H13.8333C14.75 18.3333 15.5 17.5833 15.5 16.6667V3.33333C15.5 2.41667 14.75 1.66667 13.8333 1.66667ZM8 1.66667C8.45833 1.66667 8.83333 2.04167 8.83333 2.5C8.83333 2.95833 8.45833 3.33333 8 3.33333C7.54167 3.33333 7.16667 2.95833 7.16667 2.5C7.16667 2.04167 7.54167 1.66667 8 1.66667ZM13.8333 16.6667H2.16667V3.33333H3.83333V5.83333H12.1667V3.33333H13.8333V16.6667Z" />
|
android:pathData="M13.8333 1.66667H10.35C10 0.7 9.08333 0 8 0C6.91667 0 6 0.7 5.65 1.66667H2.16667C1.25 1.66667 0.5 2.41667 0.5 3.33333V16.6667C0.5 17.5833 1.25 18.3333 2.16667 18.3333H13.8333C14.75 18.3333 15.5 17.5833 15.5 16.6667V3.33333C15.5 2.41667 14.75 1.66667 13.8333 1.66667ZM8 1.66667C8.45833 1.66667 8.83333 2.04167 8.83333 2.5C8.83333 2.95833 8.45833 3.33333 8 3.33333C7.54167 3.33333 7.16667 2.95833 7.16667 2.5C7.16667 2.04167 7.54167 1.66667 8 1.66667ZM13.8333 16.6667H2.16667V3.33333H3.83333V5.83333H12.1667V3.33333H13.8333V16.6667Z" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
@ -5,5 +5,5 @@
|
||||||
android:viewportHeight="18">
|
android:viewportHeight="18">
|
||||||
<path
|
<path
|
||||||
android:pathData="M1.0712,6.8139C1.5571,6.8139 1.8138,6.5481 1.8138,6.0622V3.8986C1.8138,2.9451 2.318,2.4593 3.2348,2.4593H5.4534C5.9392,2.4593 6.2051,2.1934 6.2051,1.7167C6.2051,1.2491 5.9392,0.9833 5.4534,0.9833H3.2164C1.3004,0.9833 0.3378,1.9275 0.3378,3.8161V6.0622C0.3378,6.5481 0.6036,6.8139 1.0712,6.8139ZM16.4271,6.8139C16.913,6.8139 17.1697,6.5481 17.1697,6.0622V3.8161C17.1697,1.9275 16.2071,0.9833 14.2911,0.9833H12.045C11.5682,0.9833 11.3024,1.2491 11.3024,1.7167C11.3024,2.1934 11.5682,2.4593 12.045,2.4593H14.2636C15.1712,2.4593 15.6937,2.9451 15.6937,3.8986V6.0622C15.6937,6.5481 15.9596,6.8139 16.4271,6.8139ZM8.3595,8.6016V5.4021C8.3595,5.1821 8.1854,4.9987 7.9562,4.9987H4.7566C4.5366,4.9987 4.3624,5.1821 4.3624,5.4021V8.6016C4.3624,8.8217 4.5366,8.9958 4.7566,8.9958H7.9562C8.1854,8.9958 8.3595,8.8217 8.3595,8.6016ZM9.9455,5.7963H12.3475V8.1983H9.9455V5.7963ZM11.6507,7.5015V6.5022H10.6423V7.5015H11.6507ZM6.856,7.5015V6.5022H5.8567V7.5015H6.856ZM5.1508,10.591H7.5619V12.993H5.1508V10.591ZM13.0259,10.9027V9.9035H12.0266V10.9027H13.0259ZM10.2664,10.9027V9.9035H9.2671V10.9027H10.2664ZM6.856,12.2871V11.2878H5.8567V12.2871H6.856ZM11.6416,12.2871V11.2878H10.6423V12.2871H11.6416ZM12.045,17.806H14.2911C16.2071,17.806 17.1697,16.8526 17.1697,14.964V12.7271C17.1697,12.2412 16.9039,11.9754 16.4271,11.9754C15.9504,11.9754 15.6937,12.2412 15.6937,12.7271V14.8907C15.6937,15.8441 15.1712,16.33 14.2636,16.33H12.045C11.5682,16.33 11.3024,16.5959 11.3024,17.0726C11.3024,17.5402 11.5682,17.806 12.045,17.806ZM3.2164,17.806H5.4534C5.9392,17.806 6.2051,17.5402 6.2051,17.0726C6.2051,16.5959 5.9392,16.33 5.4534,16.33H3.2348C2.318,16.33 1.8138,15.8441 1.8138,14.8907V12.7271C1.8138,12.2412 1.5479,11.9754 1.0712,11.9754C0.5945,11.9754 0.3378,12.2412 0.3378,12.7271V14.964C0.3378,16.8618 1.3004,17.806 3.2164,17.806ZM10.2664,13.6714V12.6721H9.2671V13.6714H10.2664ZM13.0259,13.6714V12.6721H12.0266V13.6714H13.0259ZM13.1451,8.6016V5.4021C13.1451,5.1821 12.9709,4.9987 12.7417,4.9987H9.5513C9.3222,4.9987 9.148,5.1821 9.148,5.4021V8.6016C9.148,8.8217 9.3222,8.9958 9.5513,8.9958H12.7417C12.9709,8.9958 13.1451,8.8217 13.1451,8.6016ZM5.1508,5.7963H7.5619V8.1983H5.1508V5.7963ZM8.3595,13.3872V10.1877C8.3595,9.9676 8.1854,9.7935 7.9562,9.7935H4.7566C4.5366,9.7935 4.3624,9.9676 4.3624,10.1877V13.3872C4.3624,13.6072 4.5366,13.7906 4.7566,13.7906H7.9562C8.1854,13.7906 8.3595,13.6072 8.3595,13.3872Z"
|
android:pathData="M1.0712,6.8139C1.5571,6.8139 1.8138,6.5481 1.8138,6.0622V3.8986C1.8138,2.9451 2.318,2.4593 3.2348,2.4593H5.4534C5.9392,2.4593 6.2051,2.1934 6.2051,1.7167C6.2051,1.2491 5.9392,0.9833 5.4534,0.9833H3.2164C1.3004,0.9833 0.3378,1.9275 0.3378,3.8161V6.0622C0.3378,6.5481 0.6036,6.8139 1.0712,6.8139ZM16.4271,6.8139C16.913,6.8139 17.1697,6.5481 17.1697,6.0622V3.8161C17.1697,1.9275 16.2071,0.9833 14.2911,0.9833H12.045C11.5682,0.9833 11.3024,1.2491 11.3024,1.7167C11.3024,2.1934 11.5682,2.4593 12.045,2.4593H14.2636C15.1712,2.4593 15.6937,2.9451 15.6937,3.8986V6.0622C15.6937,6.5481 15.9596,6.8139 16.4271,6.8139ZM8.3595,8.6016V5.4021C8.3595,5.1821 8.1854,4.9987 7.9562,4.9987H4.7566C4.5366,4.9987 4.3624,5.1821 4.3624,5.4021V8.6016C4.3624,8.8217 4.5366,8.9958 4.7566,8.9958H7.9562C8.1854,8.9958 8.3595,8.8217 8.3595,8.6016ZM9.9455,5.7963H12.3475V8.1983H9.9455V5.7963ZM11.6507,7.5015V6.5022H10.6423V7.5015H11.6507ZM6.856,7.5015V6.5022H5.8567V7.5015H6.856ZM5.1508,10.591H7.5619V12.993H5.1508V10.591ZM13.0259,10.9027V9.9035H12.0266V10.9027H13.0259ZM10.2664,10.9027V9.9035H9.2671V10.9027H10.2664ZM6.856,12.2871V11.2878H5.8567V12.2871H6.856ZM11.6416,12.2871V11.2878H10.6423V12.2871H11.6416ZM12.045,17.806H14.2911C16.2071,17.806 17.1697,16.8526 17.1697,14.964V12.7271C17.1697,12.2412 16.9039,11.9754 16.4271,11.9754C15.9504,11.9754 15.6937,12.2412 15.6937,12.7271V14.8907C15.6937,15.8441 15.1712,16.33 14.2636,16.33H12.045C11.5682,16.33 11.3024,16.5959 11.3024,17.0726C11.3024,17.5402 11.5682,17.806 12.045,17.806ZM3.2164,17.806H5.4534C5.9392,17.806 6.2051,17.5402 6.2051,17.0726C6.2051,16.5959 5.9392,16.33 5.4534,16.33H3.2348C2.318,16.33 1.8138,15.8441 1.8138,14.8907V12.7271C1.8138,12.2412 1.5479,11.9754 1.0712,11.9754C0.5945,11.9754 0.3378,12.2412 0.3378,12.7271V14.964C0.3378,16.8618 1.3004,17.806 3.2164,17.806ZM10.2664,13.6714V12.6721H9.2671V13.6714H10.2664ZM13.0259,13.6714V12.6721H12.0266V13.6714H13.0259ZM13.1451,8.6016V5.4021C13.1451,5.1821 12.9709,4.9987 12.7417,4.9987H9.5513C9.3222,4.9987 9.148,5.1821 9.148,5.4021V8.6016C9.148,8.8217 9.3222,8.9958 9.5513,8.9958H12.7417C12.9709,8.9958 13.1451,8.8217 13.1451,8.6016ZM5.1508,5.7963H7.5619V8.1983H5.1508V5.7963ZM8.3595,13.3872V10.1877C8.3595,9.9676 8.1854,9.7935 7.9562,9.7935H4.7566C4.5366,9.7935 4.3624,9.9676 4.3624,10.1877V13.3872C4.3624,13.6072 4.5366,13.7906 4.7566,13.7906H7.9562C8.1854,13.7906 8.3595,13.6072 8.3595,13.3872Z"
|
||||||
android:fillColor="#000000"/>
|
android:fillColor="@color/text_primary_1" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<clip-path android:pathData="M40 20C40 31.0457 31.0457 40 20 40C8.95431 40 0 31.0457 0 20C0 8.95431 8.95431 0 20 0C31.0457 0 40 8.95431 40 20Z" />
|
<clip-path android:pathData="M40 20C40 31.0457 31.0457 40 20 40C8.95431 40 0 31.0457 0 20C0 8.95431 8.95431 0 20 0C31.0457 0 40 8.95431 40 20Z" />
|
||||||
|
|
||||||
<path
|
<path
|
||||||
android:fillColor="#F4F5F6"
|
android:fillColor="@color/button_secondary"
|
||||||
android:pathData="M0 0V40H40V0" />
|
android:pathData="M0 0V40H40V0" />
|
||||||
|
|
||||||
</group>
|
</group>
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:elevation="18dp"
|
android:elevation="18dp"
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
android:indeterminateTint="@color/backgroundLightGray"
|
android:indeterminateTint="@color/background_secondary"
|
||||||
android:visibility="invisible" />
|
android:visibility="invisible" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
android:padding="5dp"
|
android:padding="5dp"
|
||||||
app:srcCompat="@drawable/ic_angle_bracket_up" />
|
app:srcCompat="@drawable/ic_angle_bracket_up"
|
||||||
|
app:tint="@color/icon_primary_1" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@color/backgroundWhite"
|
android:background="@color/background_primary"
|
||||||
android:minHeight="420dp"
|
android:minHeight="420dp"
|
||||||
tools:layout_gravity="bottom">
|
tools:layout_gravity="bottom">
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
android:layout_width="32dp"
|
android:layout_width="32dp"
|
||||||
android:layout_height="32dp"
|
android:layout_height="32dp"
|
||||||
android:background="@drawable/shape_circle"
|
android:background="@drawable/shape_circle"
|
||||||
android:backgroundTint="@color/backgroundWhite"
|
android:backgroundTint="@color/background_primary"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/iv_cross"
|
app:layout_constraintBottom_toBottomOf="@id/iv_cross"
|
||||||
app:layout_constraintEnd_toEndOf="@id/iv_cross"
|
app:layout_constraintEnd_toEndOf="@id/iv_cross"
|
||||||
app:layout_constraintStart_toStartOf="@id/iv_cross"
|
app:layout_constraintStart_toStartOf="@id/iv_cross"
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
android:layout_height="56dp"
|
android:layout_height="56dp"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:text="@string/wallet_choose_trade_action"
|
android:text="@string/wallet_choose_trade_action"
|
||||||
android:textColor="@color/darkGray2"
|
android:textColor="@color/text_secondary"
|
||||||
android:textSize="14sp" />
|
android:textSize="14sp" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:text="@string/common_buy"
|
android:text="@string/common_buy"
|
||||||
android:textColor="@color/darkGray3"
|
android:textColor="@color/text_primary_1"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:drawableStartCompat="@drawable/ic_arrow_up_24" />
|
app:drawableStartCompat="@drawable/ic_arrow_up_24" />
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:text="@string/common_sell"
|
android:text="@string/common_sell"
|
||||||
android:textColor="@color/darkGray3"
|
android:textColor="@color/text_primary_1"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:drawableStartCompat="@drawable/ic_arrow_down_24" />
|
app:drawableStartCompat="@drawable/ic_arrow_down_24" />
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:padding="16dp"
|
android:padding="16dp"
|
||||||
android:text="@string/swapping_swap_action"
|
android:text="@string/swapping_swap_action"
|
||||||
android:textColor="@color/darkGray3"
|
android:textColor="@color/text_primary_1"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
app:drawableStartCompat="@drawable/ic_exchange_vertical_24" />
|
app:drawableStartCompat="@drawable/ic_exchange_vertical_24" />
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue