Updated on 2026-08-14
This commit is contained in:
parent
537731103f
commit
6d95abf3b8
51 changed files with 19 additions and 3133 deletions
|
|
@ -22,7 +22,6 @@ import com.tangem.datasource.config.ConfigManager
|
|||
import com.tangem.datasource.config.FeaturesLocalLoader
|
||||
import com.tangem.datasource.config.models.Config
|
||||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.common.LogConfig
|
||||
|
|
@ -210,7 +209,6 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
activityResultCaller = foregroundActivityObserver
|
||||
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
|
||||
|
||||
DomainLayer.init()
|
||||
preferencesStorage = preferencesDataSource
|
||||
walletConnectRepository = WalletConnectRepository(this)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
package com.tangem.tap.common
|
||||
|
||||
import timber.log.Timber
|
||||
|
||||
class CompositionCounter(
|
||||
val id: String,
|
||||
count: Int = 0,
|
||||
) {
|
||||
var count: Int = count
|
||||
private set
|
||||
|
||||
fun increase(id: String): CompositionCounter {
|
||||
if (this.id != id) return this
|
||||
|
||||
count += 1
|
||||
return CompositionCounter(id, count)
|
||||
}
|
||||
}
|
||||
|
||||
class CompositionLogger(
|
||||
private val recomposeViewId: String,
|
||||
private val tag: String = recomposeViewId,
|
||||
private var turnOnForIds: List<String> = listOf(recomposeViewId),
|
||||
) {
|
||||
val count: Int
|
||||
get() = compositionCounter.count
|
||||
|
||||
private var compositionCounter: CompositionCounter = CompositionCounter(recomposeViewId)
|
||||
|
||||
fun nextComposition() {
|
||||
compositionCounter = compositionCounter.increase(recomposeViewId)
|
||||
log("")
|
||||
}
|
||||
|
||||
fun log(message: String) {
|
||||
if (!turnOnForIds.contains(recomposeViewId)) return
|
||||
|
||||
Timber.d("$tag[$recomposeViewId]:[${compositionCounter.count}]: $message")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,277 +0,0 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.CompositionLogger
|
||||
import com.tangem.tap.common.compose.extensions.stringResourceDefault
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun OutlinedTextFieldWidget(
|
||||
fieldData: Field.Data<String>,
|
||||
labelId: Int? = null,
|
||||
label: String = "",
|
||||
placeholderId: Int? = null,
|
||||
placeholder: String = "",
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
isEnabled: Boolean = true,
|
||||
isVisible: Boolean = true,
|
||||
isLoading: Boolean = false,
|
||||
error: ModuleError? = null,
|
||||
errorConverter: ModuleMessageConverter? = null,
|
||||
debounceTextChanges: Long = 400,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
onTextChange: (String) -> Unit,
|
||||
) {
|
||||
if (!isVisible) return
|
||||
|
||||
Column(modifier = Modifier.animateContentSize()) {
|
||||
OutlinedProgressTextField(
|
||||
fieldData = fieldData,
|
||||
label = stringResourceDefault(labelId, label),
|
||||
placeholder = stringResourceDefault(placeholderId, placeholder),
|
||||
trailingIcon = trailingIcon,
|
||||
isEnabled = isEnabled,
|
||||
isLoading = isLoading,
|
||||
error = error,
|
||||
debounce = debounceTextChanges,
|
||||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
onTextChange = onTextChange,
|
||||
)
|
||||
errorConverter?.let { AnimatedErrorView(errorConverter = it, error = error) }
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "NestedBlockDepth", "MagicNumber", "MaxLineLength")
|
||||
@Composable
|
||||
private fun OutlinedProgressTextField(
|
||||
fieldData: Field.Data<String>,
|
||||
label: String = "",
|
||||
placeholder: String = "",
|
||||
isEnabled: Boolean = true,
|
||||
isLoading: Boolean = false,
|
||||
error: ModuleError? = null,
|
||||
debounce: Long = 400,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
colors: TextFieldColors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
onTextChange: (String) -> Unit,
|
||||
) {
|
||||
val logger = remember {
|
||||
CompositionLogger(label, "OutlinedProgressTextField", listOf("Символ токена"))
|
||||
}
|
||||
logger.nextComposition()
|
||||
|
||||
val textValueState = remember { mutableStateOf(fieldData.value) }
|
||||
val textDebouncer = valueDebouncerAsState(
|
||||
initialValue = fieldData.value,
|
||||
debounce = debounce,
|
||||
onEmitValueReceive = {
|
||||
logger.log("DEBOUNCER: onEmitValueReceived: [$it]")
|
||||
logger.log("DEBOUNCER: start RECOMPOSE by new value for textValueState.value = [$it]")
|
||||
textValueState.value = it
|
||||
},
|
||||
onValueChange = {
|
||||
logger.log("DEBOUNCER: onValueChanged: >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> dispatch.toStore([$it])")
|
||||
onTextChange(it)
|
||||
},
|
||||
)
|
||||
|
||||
logger.log("RECOMPOSE ---------------------------------------------------------------START [${logger.count}]")
|
||||
logger.log("RECOMPOSE --data: fieldData.value: [$fieldData]")
|
||||
logger.log("RECOMPOSE --data: textValueState.value: [${textValueState.value}]")
|
||||
logger.log("RECOMPOSE --data: textDebouncer.emittedValue = [${textDebouncer.emittedValue}]")
|
||||
logger.log("RECOMPOSE --data: textDebouncer.debounced = [${textDebouncer.debounced}]")
|
||||
|
||||
if (!fieldData.isUserInput) {
|
||||
// initial value is not from an user
|
||||
val isNotUserInput = "-- IS NOT USER INPUT"
|
||||
logger.log("recompose $isNotUserInput")
|
||||
if (textValueState.value == fieldData.value) {
|
||||
logger.log("$isNotUserInput: внешние данные ОДИНАКОВЫ с данными в поле")
|
||||
} else {
|
||||
logger.log("$isNotUserInput: внешние данные РАЗЛИЧАЮТСЯ с данными в поле")
|
||||
if (textDebouncer.emittedValue != textDebouncer.debounced || textDebouncer.emitsCountBeforeDebounce > 0) {
|
||||
logger.log("$isNotUserInput: пользователь ВВОДИТ данные -> внешние данные игнорируем, ждем RECOMPOSE")
|
||||
} else {
|
||||
logger.log("$isNotUserInput: пользователь НЕ вводит данные -> пытаемся обработать внешние данные")
|
||||
if (textValueState.value != textDebouncer.emittedValue ||
|
||||
textValueState.value != textDebouncer.debounced
|
||||
) {
|
||||
logger.log("$isNotUserInput: даннные в поле не соответствуют данным из textDebouncer")
|
||||
if (textDebouncer.emittedValue.isEmpty() && textDebouncer.debounced.isEmpty()) {
|
||||
logger.log(
|
||||
"$isNotUserInput: даннные в textDebouncer ПУСТЫ -> start RECOMPOSE новые данные для " +
|
||||
"textValueState.value = [${fieldData.value}]",
|
||||
)
|
||||
textValueState.value = fieldData.value
|
||||
} else {
|
||||
logger.log(
|
||||
"$isNotUserInput: даннные в textDebouncer НЕ ПУСТЫ -> start RECOMPOSE новые данные для " +
|
||||
"textValueState.value = [${fieldData.value}]",
|
||||
)
|
||||
textValueState.value = fieldData.value
|
||||
}
|
||||
} else {
|
||||
logger.log(
|
||||
"$isNotUserInput: в пустое поле вставляются данные -> start RECOMPOSE новые данные для " +
|
||||
"textValueState.value = [${fieldData.value}]",
|
||||
)
|
||||
textValueState.value = fieldData.value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.log("recompose --------------------------------------------------------------FINISH [${logger.count}]")
|
||||
|
||||
Box {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
value = textValueState.value,
|
||||
onValueChange = {
|
||||
logger.log("WIDGET: textDebouncer.emmit([$it])")
|
||||
textDebouncer.emmit(it)
|
||||
},
|
||||
keyboardOptions = keyboardOptions,
|
||||
label = {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.caption,
|
||||
color = colors.labelColor(
|
||||
enabled = isEnabled,
|
||||
error = error != null,
|
||||
interactionSource = interactionSource,
|
||||
).value,
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = placeholder,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = colors.placeholderColor(enabled = isEnabled).value,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
trailingIcon = trailingIcon,
|
||||
singleLine = true,
|
||||
enabled = isEnabled,
|
||||
isError = error != null,
|
||||
visualTransformation = visualTransformation,
|
||||
colors = colors,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(start = 6.dp, top = 0.dp, end = 6.dp, bottom = 6.dp),
|
||||
visible = isLoading,
|
||||
) {
|
||||
LinearProgressIndicator(
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AnimatedErrorView(errorConverter: ModuleMessageConverter, error: ModuleError? = null) {
|
||||
AnimatedVisibility(
|
||||
visible = error != null,
|
||||
enter = fadeIn() + slideInVertically(),
|
||||
exit = slideOutVertically() + fadeOut(),
|
||||
) {
|
||||
error?.let {
|
||||
ErrorView(
|
||||
text = errorConverter.convert(it).message,
|
||||
style = TextStyle(fontSize = 14.sp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun OutlinedTextFieldWithErrorTest() {
|
||||
val context = LocalContext.current
|
||||
val converter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
class SimpleError(
|
||||
override val code: Int = 1,
|
||||
override val message: String = "Error message",
|
||||
override val data: Any? = null,
|
||||
) : ModuleError()
|
||||
|
||||
val modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
Column {
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = Field.Data("", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = null,
|
||||
errorConverter = converter,
|
||||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = null,
|
||||
errorConverter = converter,
|
||||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
isLoading = true,
|
||||
error = null,
|
||||
errorConverter = converter,
|
||||
) {}
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = Field.Data("First", false),
|
||||
label = "First label",
|
||||
placeholder = "1 placeholder",
|
||||
error = SimpleError(),
|
||||
errorConverter = converter,
|
||||
) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.tap.common.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Compose views are not typically used as a main or base view.
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun TitleSubtitle(title: String, subtitle: String) {
|
||||
Column {
|
||||
Text(text = title)
|
||||
Text(
|
||||
text = subtitle,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.tap.common.compose.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun stringResourceDefault(@StringRes id: Int?, default: String = ""): String {
|
||||
val resources = LocalContext.current.resources
|
||||
return try {
|
||||
resources.getString(requireNotNull(id))
|
||||
} catch (ex: Resources.NotFoundException) {
|
||||
default
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
default
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import com.tangem.core.navigation.AppScreen
|
|||
import com.tangem.core.navigation.FragmentShareTransition
|
||||
import com.tangem.feature.referral.ReferralFragment
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.tap.features.customtoken.legacy.AddCustomTokenFragment
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
|
||||
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
|
||||
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
|
||||
|
|
@ -33,7 +33,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
|
|||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import timber.log.Timber
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment as RedesignedAddCustomTokenFragment
|
||||
|
||||
fun FragmentActivity.openFragment(
|
||||
screen: AppScreen,
|
||||
|
|
@ -155,18 +154,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
|
||||
AppScreen.Disclaimer -> DisclaimerFragment()
|
||||
AppScreen.AddTokens -> TokensListFragment()
|
||||
|
||||
AppScreen.AddCustomToken -> {
|
||||
val featureToggles = store.state.daggerGraphState.get(
|
||||
getDependency = DaggerGraphState::customTokenFeatureToggles,
|
||||
)
|
||||
if (featureToggles.isRedesignedScreenEnabled) {
|
||||
RedesignedAddCustomTokenFragment()
|
||||
} else {
|
||||
AddCustomTokenFragment()
|
||||
}
|
||||
}
|
||||
|
||||
AppScreen.AddCustomToken -> AddCustomTokenFragment()
|
||||
AppScreen.WalletDetails -> {
|
||||
val featureToggles = store.state.daggerGraphState.get(
|
||||
getDependency = DaggerGraphState::tokenDetailsFeatureToggles,
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
typealias ValueCallback<T> = (T) -> Unit
|
||||
|
|
@ -7,8 +7,5 @@ package com.tangem.tap.features.customtoken.api.featuretoggles
|
|||
*/
|
||||
interface CustomTokenFeatureToggles {
|
||||
|
||||
/** Availability of redesigned screen (internal feature) */
|
||||
val isRedesignedScreenEnabled: Boolean
|
||||
|
||||
val isNewCardScanningEnabled: Boolean
|
||||
}
|
||||
|
|
@ -14,9 +14,6 @@ internal class DefaultCustomTokenFeatureToggles(
|
|||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : CustomTokenFeatureToggles {
|
||||
|
||||
override val isRedesignedScreenEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED")
|
||||
|
||||
override val isNewCardScanningEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.features.BaseStoreFragment
|
||||
import com.tangem.tap.features.FragmentOnBackPressedHandler
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.AddCustomTokenScreen
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.ClosePopupTrigger
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber<AddCustomTokenState> {
|
||||
|
||||
private var state: MutableState<AddCustomTokenState> = mutableStateOf(domainStore.state.addCustomTokensState)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Analytics.send(ManageTokens.CustomToken.ScreenOpened)
|
||||
}
|
||||
|
||||
override fun subscribeToStore() {
|
||||
domainStore.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.addCustomTokensState == newState.addCustomTokensState
|
||||
}.select { it.addCustomTokensState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun newState(state: AddCustomTokenState) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
this.state.value = state
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
view.findViewById<Toolbar>(R.id.toolbar)?.setTitle(R.string.add_custom_token_title)
|
||||
|
||||
val closePopupTrigger = initClosingPopupTriggerEvent()
|
||||
view.findViewById<ComposeView>(R.id.view_compose)?.setContent {
|
||||
TangemTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
AddCustomTokenScreen(state, closePopupTrigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initClosingPopupTriggerEvent(): ClosePopupTrigger = ClosePopupTrigger().apply {
|
||||
onCloseComplete = ::handleOnBackPressed
|
||||
addBackPressHandler(
|
||||
object : FragmentOnBackPressedHandler {
|
||||
override fun handleOnBackPressed() = close()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconStart
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.common.form.DataField
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ScreenState
|
||||
import com.tangem.domain.features.addCustomToken.redux.ViewStates
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.AddCustomTokenWarning
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCase
|
||||
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun AddCustomTokenScreen(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
|
||||
val selectedTestCase = remember { mutableStateOf(TestCase.ContractAddress) }
|
||||
|
||||
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed),
|
||||
)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val toggleBottomSheet = { coroutineScope.launch { bottomSheetScaffoldState.toggle() } }
|
||||
|
||||
BottomSheetScaffold(
|
||||
scaffoldState = bottomSheetScaffoldState,
|
||||
sheetContent = {
|
||||
Surface(color = colorResource(id = R.color.lightGray5)) {
|
||||
selectedTestCase.value.content(toggleBottomSheet)
|
||||
}
|
||||
},
|
||||
sheetPeekHeight = 0.dp,
|
||||
) {
|
||||
Column {
|
||||
TestCasesList(
|
||||
onItemClick = {
|
||||
selectedTestCase.value = it
|
||||
toggleBottomSheet()
|
||||
},
|
||||
)
|
||||
ScreenContent(state, closePopupTrigger)
|
||||
}
|
||||
}
|
||||
|
||||
ComposeDialogManager()
|
||||
LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) })
|
||||
DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } })
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
private suspend fun BottomSheetScaffoldState.toggle() {
|
||||
if (bottomSheetState.isCollapsed) {
|
||||
bottomSheetState.expand()
|
||||
} else {
|
||||
bottomSheetState.collapse()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScreenContent(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
|
||||
val scaffoldState = rememberScaffoldState()
|
||||
|
||||
Scaffold(
|
||||
scaffoldState = scaffoldState,
|
||||
backgroundColor = colorResource(id = R.color.backgroundLightGray),
|
||||
floatingActionButton = {
|
||||
HangingOverKeyboardView(keyboardState = keyboardAsState()) {
|
||||
AddButton(state)
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
) { paddings ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddings)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(bottom = 90.dp),
|
||||
) {
|
||||
item {
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
elevation = 4.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
FormFields(state, closePopupTrigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
item { Warnings(state.value.warnings.toList()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FormFields(state: MutableState<AddCustomTokenState>, closePopupTrigger: ClosePopupTrigger) {
|
||||
val context = LocalContext.current
|
||||
val errorConverter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
val stateValue = state.value
|
||||
stateValue.form.fieldList.forEach { field ->
|
||||
val data = ScreenFieldData.fromState(field, stateValue, errorConverter)
|
||||
when (field.id) {
|
||||
ContractAddress -> TokenContractAddressView(data)
|
||||
Network -> TokenNetworkView(data, stateValue, closePopupTrigger)
|
||||
Name -> TokenNameView(data)
|
||||
Symbol -> TokenSymbolView(data)
|
||||
Decimals -> TokenDecimalsView(data)
|
||||
DerivationPath -> TokenDerivationPathView(data, stateValue, closePopupTrigger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
|
||||
if (warnings.isEmpty()) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val warningConverter = remember { ModuleMessageConverter(context) }
|
||||
|
||||
Column {
|
||||
warnings.forEachIndexed { index, item ->
|
||||
val modifier = when (index) {
|
||||
0 -> Modifier.padding(vertical = 0.dp)
|
||||
warnings.lastIndex -> Modifier.padding(top = 8.dp, bottom = 16.dp)
|
||||
else -> Modifier.padding(top = 8.dp, bottom = 0.dp)
|
||||
}
|
||||
AddCustomTokenWarning(
|
||||
modifier = modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
warning = item,
|
||||
converter = warningConverter,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddButton(state: MutableState<AddCustomTokenState>) {
|
||||
PrimaryButtonIconStart(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.common_add),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
enabled = state.value.screenState.addButton.isEnabled,
|
||||
onClick = { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) },
|
||||
)
|
||||
}
|
||||
|
||||
data class ScreenFieldData(
|
||||
val field: DataField<*>,
|
||||
val error: AddCustomTokenError?,
|
||||
val errorConverter: ModuleMessageConverter,
|
||||
val viewState: ViewStates.TokenField,
|
||||
) {
|
||||
companion object {
|
||||
fun fromState(
|
||||
field: DataField<*>,
|
||||
state: AddCustomTokenState,
|
||||
errorConverter: ModuleMessageConverter,
|
||||
): ScreenFieldData {
|
||||
return ScreenFieldData(
|
||||
field = field,
|
||||
error = state.getError(field.id),
|
||||
errorConverter = errorConverter,
|
||||
viewState = selectField(field.id, state.screenState),
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField {
|
||||
return when (id) {
|
||||
ContractAddress -> screenState.contractAddressField
|
||||
Network -> screenState.network
|
||||
Name -> screenState.name
|
||||
Symbol -> screenState.symbol
|
||||
Decimals -> screenState.decimals
|
||||
DerivationPath -> screenState.derivationPath
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.form.Field
|
||||
|
||||
@Composable
|
||||
fun BlockchainSpinner(
|
||||
@StringRes title: Int,
|
||||
itemList: List<Blockchain>,
|
||||
selectedItem: Field.Data<Blockchain>,
|
||||
isEnabled: Boolean = true,
|
||||
textFieldConverter: (Blockchain) -> String,
|
||||
dropdownItemView: @Composable ((Blockchain) -> Unit)? = null,
|
||||
closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(),
|
||||
onItemSelect: (Blockchain) -> Unit,
|
||||
) {
|
||||
OutlinedSpinner(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(id = title),
|
||||
itemList = itemList,
|
||||
selectedItem = selectedItem,
|
||||
textFieldConverter = textFieldConverter,
|
||||
dropdownItemView = dropdownItemView,
|
||||
isEnabled = isEnabled,
|
||||
onItemSelected = onItemSelect,
|
||||
closePopupTrigger = closePopupTrigger,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
@Composable
|
||||
internal fun ComposeDialogManager() {
|
||||
val dialogSate = remember { mutableStateOf<DomainDialog?>(null) }
|
||||
val subscriber = remember {
|
||||
object : StoreSubscriber<DomainGlobalState> {
|
||||
override fun newState(state: DomainGlobalState) {
|
||||
dialogSate.value = state.dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShowTheDialog(dialogSate)
|
||||
|
||||
LaunchedEffect(
|
||||
key1 = Unit,
|
||||
block = {
|
||||
domainStore.subscribe(subscriber) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.globalState == newState.globalState
|
||||
}.select { it.globalState }
|
||||
}
|
||||
},
|
||||
)
|
||||
DisposableEffect(
|
||||
key1 = Unit,
|
||||
effect = {
|
||||
onDispose { domainStore.unsubscribe(subscriber) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
|
||||
if (dialogState.value == null) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val errorConverter = remember { ModuleMessageConverter(context) }
|
||||
val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) }
|
||||
|
||||
when (val dialog = dialogState.value) {
|
||||
is DomainDialog.DialogError -> ErrorDialog(
|
||||
title = stringResource(id = R.string.common_error),
|
||||
body = errorConverter.convert(dialog.error).message,
|
||||
onDismissRequest,
|
||||
)
|
||||
is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog with single item selection
|
||||
*/
|
||||
@Composable
|
||||
fun SimpleDialog(
|
||||
title: String,
|
||||
items: List<CoinsResponse.Coin.Network>,
|
||||
onSelect: (CoinsResponse.Coin.Network) -> Unit,
|
||||
onDismissRequest: () -> Unit,
|
||||
itemContent: @Composable (CoinsResponse.Coin.Network) -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||
onDismissRequest = { },
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) {
|
||||
Column(modifier = Modifier.padding(TangemTheme.dimens.spacing22)) {
|
||||
DialogTitle(title = title)
|
||||
LazyColumn {
|
||||
items(items = items, key = CoinsResponse.Coin.Network::networkId) { item ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp)
|
||||
.clickable {
|
||||
onSelect(item)
|
||||
onDismissRequest()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) { itemContent(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DialogTitle(title: String) {
|
||||
Text(
|
||||
text = title,
|
||||
style = LocalTextStyle.provides(
|
||||
TextStyle(
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
),
|
||||
).value,
|
||||
)
|
||||
SpacerH16()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ErrorDialog(title: String, body: String, onDismissRequest: () -> Unit) {
|
||||
AlertDialog(
|
||||
title = { DialogTitle(title) },
|
||||
text = { Text(body) },
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
Button(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(id = R.string.common_ok))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
|
||||
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
|
||||
import com.tangem.domain.features.addCustomToken.TokenField
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenContractAddressChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDecimalsChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenDerivationPathChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNameChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenNetworkChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
|
||||
import com.tangem.tap.common.compose.TitleSubtitle
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_contract_address_input_title,
|
||||
placeholder = "0x0000000000000000000000000000000000000000",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
isLoading = screenFieldData.viewState.isLoading,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
// trailingIcon = { PasteClearButton(showFirst = tokenField.data.value.isEmpty()) }
|
||||
) {
|
||||
domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it, true)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenNameView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_name_input_title,
|
||||
placeholderId = R.string.custom_token_name_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) {
|
||||
domainStore.dispatch(OnTokenNameChanged(Field.Data(it, true)))
|
||||
}
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenNetworkView(
|
||||
screenFieldData: ScreenFieldData,
|
||||
state: AddCustomTokenState,
|
||||
closePopupTrigger: ClosePopupTrigger,
|
||||
) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected)
|
||||
val networkField = screenFieldData.field as TokenBlockchainField
|
||||
|
||||
BlockchainSpinner(
|
||||
title = R.string.custom_token_network_input_title,
|
||||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
closePopupTrigger = closePopupTrigger,
|
||||
) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenSymbolView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_token_symbol_input_title,
|
||||
placeholderId = R.string.custom_token_token_symbol_input_placeholder,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenDecimalsView(screenFieldData: ScreenFieldData) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val tokenField = screenFieldData.field as TokenField
|
||||
|
||||
OutlinedTextFieldWidget(
|
||||
fieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_decimals_input_title,
|
||||
placeholder = "8",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
error = screenFieldData.error,
|
||||
errorConverter = screenFieldData.errorConverter,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TokenDerivationPathView(
|
||||
screenFieldData: ScreenFieldData,
|
||||
state: AddCustomTokenState,
|
||||
closePopupTrigger: ClosePopupTrigger,
|
||||
) {
|
||||
if (!screenFieldData.viewState.isVisible) return
|
||||
|
||||
val notSelected = stringResource(id = R.string.custom_token_derivation_path_default)
|
||||
val networkField = screenFieldData.field as TokenDerivationPathField
|
||||
|
||||
BlockchainSpinner(
|
||||
title = R.string.custom_token_derivation_path_input_title,
|
||||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
dropdownItemView = { blockchain ->
|
||||
val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected
|
||||
val blockchainName = state.blockchainToName(blockchain) ?: notSelected
|
||||
TitleSubtitle(derivationPathName, blockchainName)
|
||||
},
|
||||
closePopupTrigger = closePopupTrigger,
|
||||
) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it, true))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.Keyboard
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun HangingOverKeyboardView(
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardState: State<Keyboard>,
|
||||
spaceBetweenKeyboard: Dp = 0.dp,
|
||||
content: @Composable (BoxScope.() -> Unit),
|
||||
) {
|
||||
val padding = remember(keyboardState) {
|
||||
when (val state = keyboardState.value) {
|
||||
is Keyboard.Closed -> 0.dp
|
||||
is Keyboard.Opened -> state.height + spaceBetweenKeyboard
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier.padding(bottom = padding)) { content() }
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.os.postDelayed
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.tap.common.compose.TangemTextFieldsDefault
|
||||
import com.tangem.tap.common.extensions.ValueCallback
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
internal fun <T> OutlinedSpinner(
|
||||
label: String,
|
||||
itemList: List<T>,
|
||||
selectedItem: Field.Data<T>,
|
||||
onItemSelected: ValueCallback<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
textFieldConverter: (T) -> String = { it.toString() },
|
||||
dropdownItemView: @Composable ((T) -> Unit)? = null,
|
||||
isEnabled: Boolean = true,
|
||||
onClose: VoidCallback = {},
|
||||
closePopupTrigger: ClosePopupTrigger = ClosePopupTrigger(),
|
||||
) {
|
||||
val rIsExpanded = remember { mutableStateOf(false) }
|
||||
val stateSelectedItem = remember { mutableStateOf(selectedItem.value) }
|
||||
if (!selectedItem.isUserInput) {
|
||||
stateSelectedItem.value = selectedItem.value
|
||||
}
|
||||
|
||||
val onDropDownItemSelectedInternal: (T) -> Unit = {
|
||||
stateSelectedItem.value = it
|
||||
rIsExpanded.value = false
|
||||
onItemSelected(it)
|
||||
}
|
||||
val onDismissRequest = {
|
||||
rIsExpanded.value = false
|
||||
onClose()
|
||||
}
|
||||
|
||||
closePopupTrigger.close = {
|
||||
onDismissRequest()
|
||||
Handler(Looper.getMainLooper()).postDelayed(100) {
|
||||
closePopupTrigger.onCloseComplete()
|
||||
}
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = rIsExpanded.value,
|
||||
onExpandedChange = { rIsExpanded.value = !rIsExpanded.value },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
readOnly = true,
|
||||
enabled = isEnabled,
|
||||
value = textFieldConverter(stateSelectedItem.value),
|
||||
onValueChange = {},
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
|
||||
colors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
)
|
||||
|
||||
if (isEnabled) {
|
||||
ExposedDropdownMenu(expanded = rIsExpanded.value, onDismissRequest = onDismissRequest) {
|
||||
itemList.forEach { item ->
|
||||
key(item) {
|
||||
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
|
||||
if (dropdownItemView == null) Text(textFieldConverter(item)) else dropdownItemView(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClosePopupTrigger {
|
||||
var close: () -> Unit = {}
|
||||
var onCloseComplete: () -> Unit = {}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TestSpinnerPreview() {
|
||||
OutlinedSpinner(
|
||||
label = "Blockchain name",
|
||||
itemList = listOf(Blockchain.values()),
|
||||
selectedItem = Field.Data(Blockchain.Avalanche, false),
|
||||
onItemSelected = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.tap.common.compose.TitleSubtitle
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) {
|
||||
SimpleDialog(
|
||||
title = stringResource(id = R.string.custom_token_network_input_title),
|
||||
items = dialog.items,
|
||||
onSelect = dialog.onSelect,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) { network -> TitleSubtitle(dialog.networkIdConverter(network.networkId), network.contractAddress ?: "") }
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose.test
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.redux.domainStore
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun ContractAddressTests(onItemClick: VoidCallback) {
|
||||
val casesInfo = listOf(
|
||||
"USDC on ETH" to "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
"BUSD on ETH" to "0x4fabb145d64652a948d72533023f6e7a623c7c53",
|
||||
"ETH on AVALANCHE" to "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15",
|
||||
"USDC on ETH (invalid - cut address)" to "0xa0b86991c6218b36c1d1",
|
||||
"Custom EVM" to "0x1111111111111111112111111111111111111113",
|
||||
"Supported by several networks" to "0xa1faa113cbe53436df28ff0aee54275c13b40975",
|
||||
"Invalid" to "!@#_ _-%%^&&*((){P P2iOWsdfFQLA",
|
||||
)
|
||||
CasesListContent(casesInfo, onItemClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SolanaAddressTests(onItemClick: VoidCallback) {
|
||||
val casesInfo = listOf(
|
||||
"USDT (full)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB",
|
||||
"USDT (valid - 2/3 of address)" to "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben",
|
||||
"USDT (invalid - 1/3 of address)" to "Es9vMFrzaCERmJ",
|
||||
"ETH (full)" to "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk",
|
||||
)
|
||||
CasesListContent(casesInfo, onItemClick)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CasesListContent(casesList: List<Pair<String, String>>, onItemClick: VoidCallback) {
|
||||
LazyColumn(
|
||||
content = {
|
||||
item {
|
||||
Row {
|
||||
ResetContractAddressButton(onItemClick)
|
||||
Text("", modifier = Modifier.weight(1f))
|
||||
ResetAllFieldsButton(onItemClick)
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
items(casesList.size) {
|
||||
val (info, address) = casesList[it]
|
||||
ContractAddressButton(info, address, onItemClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResetAllFieldsButton(onItemClick: VoidCallback) {
|
||||
ActionButton(
|
||||
name = "Reset",
|
||||
onClick = {
|
||||
onItemClick()
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false)))
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false)))
|
||||
domainStore.dispatch(
|
||||
AddCustomTokenAction.OnTokenDerivationPathChanged(
|
||||
Field.Data(
|
||||
Blockchain.Unknown,
|
||||
false,
|
||||
),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ResetContractAddressButton(onItemClick: VoidCallback) {
|
||||
ActionButton(
|
||||
name = "Set empty address",
|
||||
onClick = {
|
||||
onItemClick()
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data("", false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContractAddressButton(name: String, address: String, onItemClick: VoidCallback) {
|
||||
ActionButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
name = name,
|
||||
onClick = {
|
||||
onItemClick()
|
||||
domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionButton(name: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
Button(
|
||||
modifier = modifier.padding(horizontal = 8.dp),
|
||||
onClick = onClick,
|
||||
) { Text(name, fontSize = 12.sp) }
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.legacy.compose.test
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun TestCasesList(onItemClick: (TestCase) -> Unit) {
|
||||
if (!BuildConfig.TEST_ACTION_ENABLED) return
|
||||
|
||||
Surface(color = colorResource(id = R.color.lightGray5)) {
|
||||
Column(Modifier.padding(horizontal = 16.dp)) {
|
||||
listOf(TestCase.ContractAddress, TestCase.SolanaTokens)
|
||||
.map { case -> TestCaseListItem(testCase = case, onItemClick = { onItemClick(case) }) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TestCaseListItem(testCase: TestCase, onItemClick: () -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = testCase.description,
|
||||
)
|
||||
Button(
|
||||
onClick = onItemClick,
|
||||
) { Text("Start") }
|
||||
}
|
||||
}
|
||||
|
||||
enum class TestCase(val description: String, val content: @Composable (VoidCallback) -> Unit) {
|
||||
ContractAddress("Test contract address field", { ContractAddressTests(it) }),
|
||||
Auto("Test contract address field", { ContractAddressTests(it) }),
|
||||
SolanaTokens("Test Solana contract addresses", { SolanaAddressTests(it) }),
|
||||
;
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.tap.features.tokens.impl.di
|
||||
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter
|
||||
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
|
||||
import dagger.Module
|
||||
|
|
@ -18,7 +17,5 @@ internal object TokensListRouterModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter {
|
||||
return DefaultTokensListRouter(customTokenFeatureToggles)
|
||||
}
|
||||
fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter()
|
||||
}
|
||||
|
|
@ -6,8 +6,6 @@ import com.tangem.core.navigation.NavigationAction
|
|||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchNotification
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
|
||||
import com.tangem.tap.features.wallet.redux.models.WalletDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -18,20 +16,14 @@ import com.tangem.wallet.R
|
|||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultTokensListRouter(
|
||||
private val customTokenFeatureToggles: CustomTokenFeatureToggles,
|
||||
) : TokensListRouter {
|
||||
internal class DefaultTokensListRouter : TokensListRouter {
|
||||
|
||||
override fun popBackStack() {
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
}
|
||||
|
||||
override fun openAddCustomTokenScreen() {
|
||||
if (customTokenFeatureToggles.isRedesignedScreenEnabled) {
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
} else {
|
||||
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
|
||||
}
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
}
|
||||
|
||||
override fun showAddressCopiedNotification() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,4 @@ sealed interface TokensAction : Action {
|
|||
|
||||
// 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,28 +9,19 @@ import com.tangem.common.extensions.ByteArrayKey
|
|||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.flatMap
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.configs.CardConfig
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.util.hasDerivation
|
||||
import com.tangem.domain.common.util.supportsHdWallet
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.model.WalletDataModel
|
||||
import com.tangem.tap.features.wallet.models.Currency
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -44,7 +35,6 @@ object TokensMiddleware {
|
|||
{ action ->
|
||||
when (action) {
|
||||
is TokensAction.SaveChanges -> handleSaveChanges(action)
|
||||
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken()
|
||||
}
|
||||
next(action)
|
||||
}
|
||||
|
|
@ -231,56 +221,4 @@ object TokensMiddleware {
|
|||
}
|
||||
walletCurrenciesManager.removeCurrencies(selectedUserWallet, currencies)
|
||||
}
|
||||
|
||||
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
|
||||
return currency.derivationPath?.let {
|
||||
!scanResponse.hasDerivation(currency.blockchain, it)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun handleAddingCustomToken() = scope.launch {
|
||||
val onAddCustomToken = fun(customCurrency: CustomCurrency) {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
|
||||
fun submitAndPopBack(scanResponse: ScanResponse, currencyList: List<Currency>) {
|
||||
submitAdd(scanResponse, currencyList)
|
||||
// pop from the AddCustomTokenScreen
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
|
||||
Analytics.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency))
|
||||
val currency = Currency.fromCustomCurrency(customCurrency)
|
||||
val isNeedToDerive = isNeedToDerive(scanResponse, currency)
|
||||
val currencyList = listOf(currency)
|
||||
if (isNeedToDerive) {
|
||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
||||
submitAndPopBack(it, currencyList)
|
||||
}
|
||||
} else {
|
||||
submitAndPopBack(scanResponse, currencyList)
|
||||
}
|
||||
}
|
||||
|
||||
val addedCurrencies = store.state.walletState.walletsStores
|
||||
.map { walletStore -> walletStore.walletsData.map(WalletDataModel::currency) }
|
||||
.flatten()
|
||||
.map { currency ->
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(
|
||||
currency.blockchain,
|
||||
currency.derivationPath,
|
||||
)
|
||||
|
||||
is Currency.Token -> DomainWrapped.Currency.Token(
|
||||
currency.token,
|
||||
currency.blockchain,
|
||||
currency.derivationPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/coordinator_wallet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/backgroundLightGray"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
style="@style/Widget.MaterialComponents.Toolbar.Surface"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/backgroundLightGray"
|
||||
android:fitsSystemWindows="true"
|
||||
app:liftOnScroll="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_baseline_arrow_back_24" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/view_compose"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.common
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface Filter<T> {
|
||||
fun filter(value: T): Boolean
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.common
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface Validator<Data, Error> {
|
||||
fun validate(data: Data? = null): Error?
|
||||
}
|
||||
|
|
@ -17,11 +17,6 @@ abstract class ModuleError : Throwable(), ModuleMessage {
|
|||
abstract val data: Any?
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception marked as FbConsumeException should be submitted to Firebase.Crashlytics as a non-fatal issue.
|
||||
*/
|
||||
interface FbConsumeException
|
||||
|
||||
interface ModuleMessageConverter<ModuleMessage, R> {
|
||||
fun convert(message: ModuleMessage): R
|
||||
}
|
||||
|
|
@ -3,10 +3,6 @@
|
|||
"name": "OPTIMISM_SWAP_FEATURE_ENABLED",
|
||||
"version": "4.3.1"
|
||||
},
|
||||
{
|
||||
"name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED",
|
||||
"version": "4.7.0"
|
||||
},
|
||||
{
|
||||
"name": "NEW_CARD_SCANNING_ENABLED",
|
||||
"version": "4.11.0"
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed interface DomainDialog {
|
||||
|
||||
data class DialogError(val error: DomainModuleError) : DomainDialog
|
||||
|
||||
data class SelectTokenDialog(
|
||||
val items: List<CoinsResponse.Coin.Network>,
|
||||
val networkIdConverter: (String) -> String,
|
||||
val onSelect: (CoinsResponse.Coin.Network) -> Unit,
|
||||
val onClose: VoidCallback = {},
|
||||
) : DomainDialog
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.state.ActionStateLoggerImpl
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object DomainLayer {
|
||||
internal val actionStateLogger = ActionStateLoggerImpl()
|
||||
|
||||
var onInitComplete: ((DomainModuleError?) -> Unit)? = null
|
||||
|
||||
fun init() {
|
||||
initActionStateLogger()
|
||||
|
||||
onInitComplete?.invoke(null)
|
||||
}
|
||||
|
||||
private fun initActionStateLogger() {
|
||||
val factory = actionStateLogger.actionStateConvertersFactory
|
||||
|
||||
factory.addConverter(AddCustomTokenAction::class.java, AddCustomTokenState.Converter())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.common.module.FbConsumeException
|
||||
import com.tangem.common.module.ModuleError
|
||||
import com.tangem.common.module.ModuleErrorCode
|
||||
import com.tangem.common.module.ModuleMessage
|
||||
|
|
@ -36,32 +35,14 @@ sealed class AddCustomTokenError(
|
|||
) {
|
||||
|
||||
object FieldIsEmpty : AddCustomTokenError()
|
||||
object FieldIsNotEmpty : AddCustomTokenError()
|
||||
object InvalidContractAddress : AddCustomTokenError()
|
||||
object NetworkIsNotSelected : AddCustomTokenError()
|
||||
object InvalidDecimalsCount : AddCustomTokenError()
|
||||
object InvalidDerivationPath : AddCustomTokenError()
|
||||
|
||||
sealed class Network : AddCustomTokenError() {
|
||||
object CheckAddressRequestError : Network()
|
||||
}
|
||||
|
||||
sealed class Warning : AddCustomTokenError() {
|
||||
object PotentialScamToken : Warning()
|
||||
object TokenAlreadyAdded : Warning()
|
||||
object UnsupportedSolanaToken : Warning()
|
||||
}
|
||||
|
||||
data class SelectTokeNetworkError(val networkId: String) :
|
||||
AddCustomTokenError(
|
||||
message = "Unknown network [$networkId] should not be included in the network selection dialog.",
|
||||
),
|
||||
FbConsumeException
|
||||
|
||||
data class UnAppropriateInitialization(
|
||||
val of: String,
|
||||
val info: String? = null,
|
||||
) : AddCustomTokenError(
|
||||
message = "The [$of], must be properly initialized. Info [$info]",
|
||||
)
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package com.tangem.domain
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Provides a temporary copies of the app module classes, data structures, etc.
|
||||
*/
|
||||
// TODO: refactoring: : after refactoring they should be unwrapped and moved
|
||||
// to appropriate parts of module
|
||||
@Deprecated("After refactoring they should be unwrapped and moved to appropriate parts of module")
|
||||
sealed interface DomainWrapped {
|
||||
|
||||
// Mirror reflection ot the com.tangem.tap.features.wallet.redux.Currency
|
||||
sealed interface Currency {
|
||||
val blockchain: com.tangem.blockchain.common.Blockchain
|
||||
val currencySymbol: String
|
||||
val derivationPath: String?
|
||||
|
||||
data class Token(
|
||||
val token: com.tangem.blockchain.common.Token,
|
||||
override val blockchain: com.tangem.blockchain.common.Blockchain,
|
||||
override val derivationPath: String?,
|
||||
) : Currency {
|
||||
override val currencySymbol = token.symbol
|
||||
}
|
||||
|
||||
data class Blockchain(
|
||||
override val blockchain: com.tangem.blockchain.common.Blockchain,
|
||||
override val derivationPath: String?,
|
||||
) : Currency {
|
||||
override val currencySymbol: String = blockchain.currency
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain.common
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.FirmwareVersion
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import java.util.*
|
||||
|
|
@ -69,6 +68,4 @@ object TapWorkarounds {
|
|||
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
|
||||
return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER
|
||||
}
|
||||
|
||||
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] ?: null
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package com.tangem.domain.common.form
|
||||
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface DataConverterVisitor<Data, Result> {
|
||||
fun visit(data: Data?)
|
||||
fun getConvertedData(): Result
|
||||
}
|
||||
|
||||
interface FieldDataConverter<Result> : DataConverterVisitor<FieldData, Result>
|
||||
|
||||
abstract class BaseFieldDataConverter<Result> : FieldDataConverter<Result> {
|
||||
private val collectIds: List<FieldId>
|
||||
get() = getIdToCollect()
|
||||
|
||||
protected val collectedData: MutableMap<FieldId, Any?> = mutableMapOf()
|
||||
|
||||
override fun visit(data: Pair<FieldId, Field.Data<*>>?) {
|
||||
val id = data?.first ?: return
|
||||
|
||||
if (collectIds.contains(id)) {
|
||||
collectedData[id] = data.second.value
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getIdToCollect(): List<FieldId>
|
||||
}
|
||||
|
||||
class FieldToJsonConverter(
|
||||
private val fieldsToConvert: List<FieldId> = listOf(),
|
||||
private val jsonConverter: MoshiJsonConverter,
|
||||
) : BaseFieldDataConverter<String>() {
|
||||
|
||||
override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ")
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = fieldsToConvert
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
package com.tangem.domain.common.form
|
||||
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
|
||||
import com.tangem.blockchain.blockchains.solana.SolanaAddressService
|
||||
import com.tangem.blockchain.blockchains.tron.TronAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.AddressService
|
||||
import com.tangem.common.Validator
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface CustomTokenValidator<T> : Validator<T, AddCustomTokenError>
|
||||
|
||||
class StringIsEmptyValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
return if (data.isNullOrEmpty()) null else AddCustomTokenError.FieldIsNotEmpty
|
||||
}
|
||||
}
|
||||
|
||||
class StringIsNotEmptyValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
return if (data.isNullOrEmpty()) AddCustomTokenError.FieldIsEmpty else null
|
||||
}
|
||||
}
|
||||
|
||||
class TokenContractAddressValidator : CustomTokenValidator<String> {
|
||||
|
||||
private var blockchain: Blockchain = Blockchain.Unknown
|
||||
|
||||
private val successAddressValidator = object : AddressService() {
|
||||
override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun validate(address: String): Boolean = true
|
||||
}
|
||||
|
||||
fun nextValidationFor(blockchain: Blockchain) {
|
||||
this.blockchain = blockchain
|
||||
}
|
||||
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
return when {
|
||||
data.isNullOrEmpty() -> AddCustomTokenError.FieldIsEmpty
|
||||
getAddressService().validate(data) -> null
|
||||
else -> AddCustomTokenError.InvalidContractAddress
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAddressService(): AddressService {
|
||||
return when (blockchain) {
|
||||
Blockchain.Unknown -> successAddressValidator
|
||||
Blockchain.Binance, Blockchain.BinanceTestnet -> successAddressValidator
|
||||
Blockchain.Solana, Blockchain.SolanaTestnet -> SolanaAddressService()
|
||||
Blockchain.Tron, Blockchain.TronTestnet -> TronAddressService()
|
||||
else -> {
|
||||
if (blockchain.isEvm()) {
|
||||
EthereumAddressService()
|
||||
} else {
|
||||
Timber.e("Throw for blockchain: ${blockchain.fullName}")
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TokenNetworkValidator : CustomTokenValidator<Blockchain> {
|
||||
override fun validate(data: Blockchain?): AddCustomTokenError? {
|
||||
return when (data) {
|
||||
null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TokenNameValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data)
|
||||
}
|
||||
|
||||
class TokenSymbolValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data)
|
||||
}
|
||||
|
||||
class TokenDecimalsValidator : CustomTokenValidator<String> {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty
|
||||
|
||||
return if (decimal > INVALID_DECIMALS_COUNT) AddCustomTokenError.InvalidDecimalsCount else null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val INVALID_DECIMALS_COUNT = 30
|
||||
}
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
package com.tangem.domain.common.form
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class Form(
|
||||
fieldList: List<DataField<*>>,
|
||||
) {
|
||||
private val _fieldList: MutableList<DataField<*>> = fieldList.toMutableList()
|
||||
|
||||
val fieldList: List<DataField<*>>
|
||||
get() = _fieldList.toList()
|
||||
|
||||
fun getField(id: FieldId): DataField<*>? = fieldList.firstOrNull { it.id == id }
|
||||
|
||||
fun getData(id: FieldId): Pair<FieldId, *>? = getField(id)?.getData()
|
||||
|
||||
fun setField(field: DataField<*>) {
|
||||
val oldField = getField(field.id) ?: return
|
||||
val oldIndexOfField = _fieldList.indexOf(oldField)
|
||||
if (oldIndexOfField == -1) return
|
||||
|
||||
_fieldList.removeAt(oldIndexOfField)
|
||||
_fieldList.add(oldIndexOfField, field)
|
||||
}
|
||||
|
||||
// convert this form data whatever you want
|
||||
fun visitDataConverter(converter: FieldDataConverter<*>) {
|
||||
fieldList.forEach { it.visitDataConverter(converter) }
|
||||
}
|
||||
}
|
||||
|
||||
interface FieldId
|
||||
|
||||
interface Field<T> {
|
||||
val id: FieldId
|
||||
var data: Data<T>
|
||||
|
||||
data class Data<Data>(
|
||||
val value: Data,
|
||||
val isUserInput: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
typealias FieldData = Pair<FieldId, Field.Data<*>>
|
||||
|
||||
interface DataField<T> : Field<T> {
|
||||
fun getData(): Pair<FieldId, Field.Data<T>>
|
||||
fun visitDataConverter(dataConverter: FieldDataConverter<*>)
|
||||
}
|
||||
|
||||
abstract class BaseDataField<T>(
|
||||
override val id: FieldId,
|
||||
override var data: Field.Data<T>,
|
||||
) : DataField<T> {
|
||||
|
||||
override fun getData(): Pair<FieldId, Field.Data<T>> = id to data
|
||||
|
||||
override fun visitDataConverter(dataConverter: FieldDataConverter<*>) {
|
||||
dataConverter.visit(getData())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class AddCustomTokenService(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val supportedTokenNetworkIds: List<String>,
|
||||
) {
|
||||
|
||||
suspend fun findToken(contractAddress: String, networkId: String?): List<CoinsResponse.Coin> {
|
||||
return withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
tangemTechApi.getCoins(
|
||||
contractAddress = contractAddress,
|
||||
networkIds = selectNetworksForSearch(networkId),
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onSuccess = { response ->
|
||||
var coinsList = mutableListOf<CoinsResponse.Coin>()
|
||||
response.coins.forEach { coin ->
|
||||
val networksWithTheSameAddress = coin.networks
|
||||
.filter { it.contractAddress != null || it.decimalCount != null }
|
||||
.filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true }
|
||||
.filter { supportedTokenNetworkIds.contains(it.networkId) }
|
||||
if (networksWithTheSameAddress.isNotEmpty()) {
|
||||
val newToken = coin.copy(networks = networksWithTheSameAddress)
|
||||
coinsList.add(newToken)
|
||||
}
|
||||
}
|
||||
if (coinsList.size > 1) {
|
||||
// https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679
|
||||
coinsList = mutableListOf(coinsList[0])
|
||||
}
|
||||
coinsList
|
||||
},
|
||||
onFailure = { emptyList() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectNetworksForSearch(networkId: String?): String {
|
||||
return networkId ?: supportedTokenNetworkIds.joinToString(",")
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,7 @@ package com.tangem.domain.features.addCustomToken
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.form.BaseFieldDataConverter
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -16,65 +12,16 @@ sealed class CustomCurrency(
|
|||
val derivationPath: DerivationPath?,
|
||||
) {
|
||||
|
||||
@Deprecated("It will be removed in next releases")
|
||||
class CustomBlockchain(
|
||||
network: Blockchain,
|
||||
derivationPath: DerivationPath?,
|
||||
) : CustomCurrency(network, derivationPath) {
|
||||
|
||||
class Converter(
|
||||
private val derivationStyle: DerivationStyle?,
|
||||
) : BaseFieldDataConverter<CustomBlockchain>() {
|
||||
override fun getConvertedData(): CustomBlockchain {
|
||||
val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain
|
||||
val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain
|
||||
val derivationPath = AddCustomTokenState.getDerivationPath(
|
||||
mainNetwork,
|
||||
derivationPathNetwork,
|
||||
derivationStyle,
|
||||
)
|
||||
return CustomBlockchain(mainNetwork, derivationPath)
|
||||
}
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> =
|
||||
listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath)
|
||||
}
|
||||
}
|
||||
) : CustomCurrency(network, derivationPath)
|
||||
|
||||
@Deprecated("It will be removed in next releases")
|
||||
class CustomToken(
|
||||
val token: Token,
|
||||
network: Blockchain,
|
||||
derivationPath: DerivationPath?,
|
||||
) : CustomCurrency(network, derivationPath) {
|
||||
|
||||
class Converter(
|
||||
private val tokenId: String?,
|
||||
private val derivationStyle: DerivationStyle?,
|
||||
) : BaseFieldDataConverter<CustomToken>() {
|
||||
|
||||
override fun getConvertedData(): CustomToken {
|
||||
val mainNetwork = collectedData[CustomTokenFieldId.Network] as Blockchain
|
||||
val derivationPathNetwork = collectedData[CustomTokenFieldId.DerivationPath] as Blockchain
|
||||
val derivationPath = AddCustomTokenState.getDerivationPath(
|
||||
mainNetwork,
|
||||
derivationPathNetwork,
|
||||
derivationStyle,
|
||||
)
|
||||
|
||||
val token = Token(
|
||||
name = collectedData[CustomTokenFieldId.Name] as String,
|
||||
symbol = collectedData[CustomTokenFieldId.Symbol] as String,
|
||||
contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String,
|
||||
decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(),
|
||||
id = tokenId,
|
||||
)
|
||||
return CustomToken(
|
||||
token,
|
||||
collectedData[CustomTokenFieldId.Network] as Blockchain,
|
||||
derivationPath,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = CustomTokenFieldId.values().toList()
|
||||
}
|
||||
}
|
||||
) : CustomCurrency(network, derivationPath)
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.form.BaseDataField
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class CustomTokenFieldId : FieldId {
|
||||
ContractAddress,
|
||||
Network,
|
||||
Name,
|
||||
Symbol,
|
||||
Decimals,
|
||||
DerivationPath,
|
||||
}
|
||||
|
||||
data class TokenField(
|
||||
override val id: FieldId,
|
||||
) : BaseDataField<String>(id, Field.Data("", false))
|
||||
|
||||
data class TokenBlockchainField(
|
||||
override val id: FieldId,
|
||||
val itemList: List<Blockchain>,
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown, false))
|
||||
|
||||
data class TokenDerivationPathField(
|
||||
override val id: FieldId,
|
||||
val itemList: List<Blockchain>,
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown, false))
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.form.Field
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class AddCustomTokenAction : Action {
|
||||
sealed class Init : AddCustomTokenAction() {
|
||||
data class SetAddedCurrencies(val addedCurrencies: List<DomainWrapped.Currency>) : AddCustomTokenAction()
|
||||
data class SetOnAddTokenCallback(val callback: (CustomCurrency) -> Unit) : AddCustomTokenAction()
|
||||
}
|
||||
|
||||
object OnCreate : AddCustomTokenAction()
|
||||
|
||||
object OnDestroy : AddCustomTokenAction()
|
||||
|
||||
// from user, ui
|
||||
data class OnTokenContractAddressChanged(val contractAddress: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnTokenNetworkChanged(val blockchainNetwork: Field.Data<Blockchain>) : AddCustomTokenAction()
|
||||
data class OnTokenNameChanged(val tokenName: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnTokenSymbolChanged(val tokenSymbol: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnTokenDerivationPathChanged(
|
||||
val blockchainDerivationPath: Field.Data<Blockchain>,
|
||||
) : AddCustomTokenAction()
|
||||
|
||||
data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data<String>) : AddCustomTokenAction()
|
||||
object OnAddCustomTokenClicked : AddCustomTokenAction()
|
||||
|
||||
data class SetFoundTokenInfo(val foundToken: CoinsResponse.Coin?) : AddCustomTokenAction()
|
||||
|
||||
// form fields
|
||||
data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction()
|
||||
|
||||
sealed class FieldError : AddCustomTokenAction() {
|
||||
data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : FieldError()
|
||||
data class Remove(val id: CustomTokenFieldId) : FieldError()
|
||||
}
|
||||
|
||||
// warnings
|
||||
sealed class Warning : AddCustomTokenAction() {
|
||||
data class Add(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
|
||||
data class Remove(val warnings: Set<AddCustomTokenError.Warning>) : Warning()
|
||||
data class Replace(
|
||||
val remove: Set<AddCustomTokenError.Warning>,
|
||||
val add: Set<AddCustomTokenError.Warning>,
|
||||
) : Warning()
|
||||
}
|
||||
|
||||
// To change the screenState
|
||||
sealed class Screen : AddCustomTokenAction() {
|
||||
data class UpdateTokenFields(val pairs: List<Pair<FieldId, ViewStates.TokenField>>) : Screen()
|
||||
data class UpdateAddButton(val addButton: ViewStates.AddButton) : Screen()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,720 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import android.webkit.ValueCallback
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.AddCustomTokenError.Warning.*
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.extensions.canHandleToken
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.form.*
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.features.addCustomToken.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.*
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState.Companion.createInitialScreenState
|
||||
import com.tangem.domain.redux.BaseStoreHub
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.ReStoreReducer
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.extensions.dispatchOnMain
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LargeClass")
|
||||
internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomTokenHub") {
|
||||
|
||||
private val hubState: AddCustomTokenState
|
||||
get() = domainStore.state.addCustomTokensState
|
||||
|
||||
override fun getReducer(): ReStoreReducer<AddCustomTokenState> = AddCustomTokenReducer(globalState)
|
||||
|
||||
override fun getHubState(storeState: DomainState): AddCustomTokenState = hubState
|
||||
|
||||
override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState {
|
||||
return storeState.copy(addCustomTokensState = newHubState)
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
override suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback<Action>) {
|
||||
if (action !is AddCustomTokenAction) return
|
||||
|
||||
when (action) {
|
||||
is OnCreate -> {
|
||||
hubState.appSavedCurrencies.guard {
|
||||
return throwUnAppropriateInitialization("addedTokens")
|
||||
}
|
||||
}
|
||||
is OnDestroy -> cancelAll()
|
||||
is OnTokenContractAddressChanged -> {
|
||||
validateContractAddressAndNotify(action.contractAddress.value)
|
||||
}
|
||||
is OnTokenNetworkChanged -> {
|
||||
if (!action.blockchainNetwork.isUserInput) return
|
||||
|
||||
validateContractAddressAndNotify(ContractAddress.getFieldValue())
|
||||
}
|
||||
is OnTokenDerivationPathChanged -> {
|
||||
updateAddButton()
|
||||
}
|
||||
is OnTokenNameChanged, is OnTokenSymbolChanged, is OnTokenDecimalsChanged -> {
|
||||
updateAddButton()
|
||||
}
|
||||
is OnAddCustomTokenClicked -> {
|
||||
val state = hubState
|
||||
val completeData = when {
|
||||
state.getCustomTokenType() == CustomTokenType.Token && state.networkIsSelected() -> {
|
||||
state.gatherUserToken()
|
||||
}
|
||||
state.getCustomTokenType() == CustomTokenType.Blockchain && state.networkIsSelected() -> {
|
||||
state.gatherBlockchain()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (completeData == null) {
|
||||
// normally it can't be, because the AddButton must be blocked
|
||||
} else {
|
||||
hubScope.launch(Dispatchers.Main) {
|
||||
state.onTokenAddCallback?.invoke(completeData)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun validateContractAddressAndNotify(contractAddress: String) {
|
||||
val error = ContractAddress.validateValue(contractAddress)
|
||||
if (Network.isFilled()) {
|
||||
when (error) {
|
||||
null -> {
|
||||
// valid contract address
|
||||
ContractAddress.removeError()
|
||||
findTokenAndUpdateFields(contractAddress)
|
||||
}
|
||||
AddCustomTokenError.InvalidContractAddress -> {
|
||||
ContractAddress.addError(error)
|
||||
enableDisableTokenDetailFields(hubState.tokensAnyFieldsIsFilled())
|
||||
}
|
||||
AddCustomTokenError.FieldIsEmpty -> {
|
||||
ContractAddress.removeError()
|
||||
clearTokenDetailsFields()
|
||||
disableTokenDetailFields()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
} else {
|
||||
// is default selection (Blockchain.Unknown)
|
||||
when (error) {
|
||||
null -> {
|
||||
// Blockchain.Unknown has always valid contract address
|
||||
ContractAddress.removeError()
|
||||
findTokenAndUpdateFields(contractAddress)
|
||||
}
|
||||
else -> {
|
||||
ContractAddress.removeError()
|
||||
clearTokenDetailsFields()
|
||||
disableTokenDetailFields()
|
||||
}
|
||||
}
|
||||
}
|
||||
updateDerivationPath(Network.getFieldValue())
|
||||
updateWarnings()
|
||||
updateAddButton()
|
||||
}
|
||||
|
||||
private suspend fun findTokenAndUpdateFields(contractAddress: String) {
|
||||
val foundTokens = requestInfoAboutToken(contractAddress)
|
||||
if (foundTokens.isEmpty()) {
|
||||
// token not found - it's completely custom
|
||||
dispatchOnMain(SetFoundTokenInfo(null))
|
||||
enableTokenDetailFields()
|
||||
return
|
||||
}
|
||||
|
||||
// foundToken - contains all info about the token
|
||||
val foundToken = foundTokens[0]
|
||||
dispatchOnMain(SetFoundTokenInfo(foundToken))
|
||||
when {
|
||||
foundToken.networks.isEmpty() -> {
|
||||
Timber.e("Unexpected state -> throw to FB")
|
||||
}
|
||||
foundToken.networks.size == 1 -> {
|
||||
// token with single contract address
|
||||
val singleTokenContract = foundToken.networks[0]
|
||||
fillTokenFields(foundToken, singleTokenContract)
|
||||
disableTokenDetailFields()
|
||||
}
|
||||
else -> {
|
||||
val dialog = DomainDialog.SelectTokenDialog(
|
||||
items = foundToken.networks,
|
||||
networkIdConverter = { networkId ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
throw AddCustomTokenError.SelectTokeNetworkError(networkId)
|
||||
}
|
||||
hubState.blockchainToName(blockchain) ?: ""
|
||||
},
|
||||
onSelect = { selectedContract ->
|
||||
hubScope.launch {
|
||||
// find how to connect to the upper coroutineContext and dispatch through them
|
||||
fillTokenFields(foundToken, selectedContract)
|
||||
disableTokenDetailFields()
|
||||
}
|
||||
},
|
||||
)
|
||||
dispatchOnMain(DomainGlobalAction.ShowDialog(dialog))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateDerivationPath(blockchainNetwork: Blockchain) {
|
||||
val state = hubState
|
||||
val derivationIsSupportedByNetwork = blockchainNetwork.isEvm() || blockchainNetwork == Blockchain.Unknown
|
||||
|
||||
if (DerivationPath.isFilled() && !derivationIsSupportedByNetwork) {
|
||||
// reset to default
|
||||
val derivationField = DerivationPath.getField<TokenDerivationPathField>()
|
||||
derivationField.data = derivationField.data.copy(
|
||||
value = Blockchain.Unknown,
|
||||
isUserInput = false,
|
||||
)
|
||||
state.setField(derivationField)
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
if (state.screenState.derivationPath.isEnabled != derivationIsSupportedByNetwork) {
|
||||
val action = Screen.UpdateTokenFields(
|
||||
listOf(
|
||||
DerivationPath to state.screenState.derivationPath.copy(
|
||||
isEnabled = derivationIsSupportedByNetwork,
|
||||
),
|
||||
),
|
||||
)
|
||||
dispatchOnMain(action)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateWarnings() {
|
||||
val state = hubState
|
||||
val warningsAdd = mutableSetOf<AddCustomTokenError.Warning>()
|
||||
val warningsRemove = mutableSetOf<AddCustomTokenError.Warning>()
|
||||
|
||||
val tokenIsSupported = tokenIsSupported(Network.getFieldValue())
|
||||
val alreadyAdded = isPersistIntoAppSavedTokensList()
|
||||
when (state.getCustomTokenType()) {
|
||||
CustomTokenType.Blockchain -> {
|
||||
warningsRemove.add(UnsupportedSolanaToken)
|
||||
|
||||
if (alreadyAdded) warningsAdd.add(TokenAlreadyAdded) else warningsRemove.add(TokenAlreadyAdded)
|
||||
|
||||
if (state.derivationPathIsSelected()) {
|
||||
warningsAdd.add(PotentialScamToken)
|
||||
} else {
|
||||
warningsRemove.add(PotentialScamToken)
|
||||
}
|
||||
}
|
||||
CustomTokenType.Token -> {
|
||||
if (tokenIsSupported) {
|
||||
warningsRemove.add(UnsupportedSolanaToken)
|
||||
} else {
|
||||
val validationResult = ContractAddress.validateValue(ContractAddress.getFieldValue())
|
||||
if (validationResult == AddCustomTokenError.FieldIsEmpty) {
|
||||
warningsRemove.add(UnsupportedSolanaToken)
|
||||
} else {
|
||||
warningsAdd.add(UnsupportedSolanaToken)
|
||||
}
|
||||
}
|
||||
|
||||
if (isPersistIntoAppSavedTokensList()) {
|
||||
warningsAdd.add(TokenAlreadyAdded)
|
||||
} else {
|
||||
warningsRemove.add(TokenAlreadyAdded)
|
||||
}
|
||||
|
||||
if (state.foundToken == null) {
|
||||
if (state.tokensAnyFieldsIsFilled()) {
|
||||
warningsAdd.add(PotentialScamToken)
|
||||
} else {
|
||||
warningsRemove.add(PotentialScamToken)
|
||||
}
|
||||
} else {
|
||||
if (state.foundToken.active) {
|
||||
warningsRemove.add(PotentialScamToken)
|
||||
} else {
|
||||
warningsAdd.add(PotentialScamToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchOnMain(
|
||||
Warning.Replace(
|
||||
remove = warningsRemove,
|
||||
add = warningsAdd,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateAddButton() {
|
||||
if (isPersistIntoAppSavedTokensList()) {
|
||||
TokenAlreadyAdded.add()
|
||||
disableAddButton()
|
||||
return
|
||||
} else {
|
||||
TokenAlreadyAdded.remove()
|
||||
}
|
||||
|
||||
val state = hubState
|
||||
when {
|
||||
// token
|
||||
state.tokensFieldsIsFilled() && state.networkIsSelected() -> {
|
||||
val error = ContractAddress.validateValue(ContractAddress.getFieldValue<String>())
|
||||
val tokenIsSupported = tokenIsSupported(Network.getFieldValue())
|
||||
enableDisableAddButton(tokenIsSupported && error == null)
|
||||
}
|
||||
// token
|
||||
state.tokensAnyFieldsIsFilled() -> {
|
||||
disableAddButton()
|
||||
}
|
||||
// blockchain
|
||||
else -> {
|
||||
if (state.networkIsSelected()) {
|
||||
if (isBlockchainPersistIntoAppSavedTokensList()) disableAddButton() else enableAddButton()
|
||||
} else {
|
||||
disableAddButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestInfoAboutToken(contractAddress: String): List<CoinsResponse.Coin> {
|
||||
val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager)
|
||||
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true))))
|
||||
|
||||
val field = hubState.getField<TokenBlockchainField>(Network)
|
||||
val selectedNetworkId: String? = field.data.value.let {
|
||||
if (it == Blockchain.Unknown) null else it
|
||||
}?.toNetworkId()
|
||||
|
||||
// simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress()
|
||||
// got the result faster than 500ms and the delay would only be the difference between them.
|
||||
delay(timeMillis = 500)
|
||||
|
||||
val result = tangemTechServiceManager.findToken(contractAddress, selectedNetworkId)
|
||||
|
||||
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = false))))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* These are helper functions.
|
||||
*/
|
||||
private fun isPersistIntoAppSavedTokensList(): Boolean = when (hubState.getCustomTokenType()) {
|
||||
CustomTokenType.Blockchain -> isBlockchainPersistIntoAppSavedTokensList()
|
||||
CustomTokenType.Token -> isTokenPersistIntoAppSavedTokensList()
|
||||
}
|
||||
|
||||
private fun isTokenPersistIntoAppSavedTokensList(): Boolean {
|
||||
val savedCurrencies = hubState.appSavedCurrencies ?: return false
|
||||
|
||||
val tokenId = hubState.foundToken?.id
|
||||
val tokenContractAddress = ContractAddress.getFieldValue<String>()
|
||||
val tokenNetworkId = Network.getFieldValue<Blockchain>().toNetworkId()
|
||||
val selectedDerivation = DerivationPath.getFieldValue<Blockchain>()
|
||||
|
||||
val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation)
|
||||
savedCurrencies.forEach { wrappedCurrency ->
|
||||
when (wrappedCurrency) {
|
||||
is DomainWrapped.Currency.Blockchain -> Unit
|
||||
is DomainWrapped.Currency.Token -> {
|
||||
val sameId = tokenId == wrappedCurrency.token.id
|
||||
val sameAddress = tokenContractAddress == wrappedCurrency.token.contractAddress
|
||||
val sameBlockchain = Blockchain.fromNetworkId(tokenNetworkId) == wrappedCurrency.blockchain
|
||||
val sameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath
|
||||
@Suppress("ComplexCondition")
|
||||
if (sameId && sameAddress && sameBlockchain && sameDerivationPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isBlockchainPersistIntoAppSavedTokensList(): Boolean {
|
||||
val savedCurrencies = hubState.appSavedCurrencies ?: return false
|
||||
val selectedNetwork = Network.getFieldValue<Blockchain>()
|
||||
val selectedDerivation = DerivationPath.getFieldValue<Blockchain>()
|
||||
val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation)
|
||||
|
||||
savedCurrencies.forEach { wrappedCurrency ->
|
||||
when (wrappedCurrency) {
|
||||
is DomainWrapped.Currency.Blockchain -> {
|
||||
val isSameBlockchain = selectedNetwork == wrappedCurrency.blockchain
|
||||
val isSameDerivationPath = derivationPath?.rawPath == wrappedCurrency.derivationPath
|
||||
if (isSameBlockchain && isSameDerivationPath) return true
|
||||
}
|
||||
|
||||
is DomainWrapped.Currency.Token -> Unit
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getDerivationPathFromSelectedBlockchain(
|
||||
selectedDerivationBlockchain: Blockchain,
|
||||
): com.tangem.crypto.hdWallet.DerivationPath? = AddCustomTokenState.getDerivationPath(
|
||||
mainNetwork = Network.getFieldValue(),
|
||||
derivationNetwork = selectedDerivationBlockchain,
|
||||
derivationStyle = hubState.cardDerivationStyle,
|
||||
)
|
||||
|
||||
private suspend fun fillTokenFields(token: CoinsResponse.Coin, coinNetwork: CoinsResponse.Coin.Network) {
|
||||
val blockchain = Blockchain.fromNetworkId(coinNetwork.networkId) ?: Blockchain.Unknown
|
||||
Network.setFieldValue(Field.Data(blockchain, false))
|
||||
Name.setFieldValue(Field.Data(token.name, false))
|
||||
Symbol.setFieldValue(Field.Data(token.symbol, false))
|
||||
Decimals.setFieldValue(Field.Data(coinNetwork.decimalCount.toString(), false))
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
private suspend fun clearTokenDetailsFields() {
|
||||
Name.setFieldValue(Field.Data("", false))
|
||||
Symbol.setFieldValue(Field.Data("", false))
|
||||
Decimals.setFieldValue(Field.Data("", false))
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
private suspend fun enableTokenDetailFields() {
|
||||
enableDisableTokenDetailFields(true)
|
||||
}
|
||||
|
||||
private suspend fun disableTokenDetailFields() {
|
||||
enableDisableTokenDetailFields(false)
|
||||
}
|
||||
|
||||
private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean) {
|
||||
val state = hubState
|
||||
val action = Screen.UpdateTokenFields(
|
||||
listOf(
|
||||
Name to state.screenState.name.copy(isEnabled = isEnabled),
|
||||
Symbol to state.screenState.symbol.copy(isEnabled = isEnabled),
|
||||
Decimals to state.screenState.decimals.copy(isEnabled = isEnabled),
|
||||
),
|
||||
)
|
||||
dispatchOnMain(action)
|
||||
}
|
||||
|
||||
private suspend fun enableAddButton() {
|
||||
enableDisableAddButton(true)
|
||||
}
|
||||
|
||||
private suspend fun disableAddButton() {
|
||||
enableDisableAddButton(false)
|
||||
}
|
||||
|
||||
private suspend fun enableDisableAddButton(isEnabled: Boolean) {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(isEnabled)))
|
||||
}
|
||||
|
||||
private fun tokenIsSupported(blockchain: Blockchain): Boolean = when (blockchain) {
|
||||
Blockchain.Unknown -> true
|
||||
else -> {
|
||||
val scanResponse = globalState.scanResponse
|
||||
scanResponse?.card?.canHandleToken(
|
||||
blockchain = blockchain,
|
||||
cardTypesResolver = scanResponse.cardTypesResolver,
|
||||
) ?: false
|
||||
}
|
||||
}
|
||||
|
||||
@Throws
|
||||
private fun throwUnAppropriateInitialization(objName: String) {
|
||||
throw AddCustomTokenError.UnAppropriateInitialization(
|
||||
"AddCustomTokenHub",
|
||||
"$objName must be not NULL",
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun CustomTokenFieldId.addError(error: AddCustomTokenError) {
|
||||
dispatchOnMain(FieldError.Add(this, error))
|
||||
}
|
||||
|
||||
private suspend fun CustomTokenFieldId.removeError() {
|
||||
dispatchOnMain(FieldError.Remove(this))
|
||||
}
|
||||
|
||||
private inline fun <reified T> CustomTokenFieldId.getField(): T {
|
||||
val state = hubState
|
||||
val value = when (this) {
|
||||
ContractAddress -> state.getField<TokenField>(this)
|
||||
Network -> state.getField<TokenBlockchainField>(this)
|
||||
Name -> state.getField<TokenField>(this)
|
||||
Symbol -> state.getField<TokenField>(this)
|
||||
Decimals -> state.getField<TokenField>(this)
|
||||
DerivationPath -> state.getField<TokenDerivationPathField>(this)
|
||||
}
|
||||
return value as T
|
||||
}
|
||||
|
||||
private inline fun <reified T> CustomTokenFieldId.getFieldValue(): T {
|
||||
val value = when (this) {
|
||||
ContractAddress -> getField<TokenField>().data.value
|
||||
Network -> getField<TokenBlockchainField>().data.value
|
||||
Name -> getField<TokenField>().data.value
|
||||
Symbol -> getField<TokenField>().data.value
|
||||
Decimals -> getField<TokenField>().data.value
|
||||
DerivationPath -> getField<TokenDerivationPathField>().data.value
|
||||
}
|
||||
return value as T
|
||||
}
|
||||
|
||||
private fun CustomTokenFieldId.setFieldValue(fieldData: Field.Data<*>) {
|
||||
when (this) {
|
||||
ContractAddress -> getField<TokenField>().data = fieldData as Field.Data<String>
|
||||
Network -> getField<TokenBlockchainField>().data = fieldData as Field.Data<Blockchain>
|
||||
Name -> getField<TokenField>().data = fieldData as Field.Data<String>
|
||||
Symbol -> getField<TokenField>().data = fieldData as Field.Data<String>
|
||||
Decimals -> getField<TokenField>().data = fieldData as Field.Data<String>
|
||||
DerivationPath -> getField<TokenDerivationPathField>().data = fieldData as Field.Data<Blockchain>
|
||||
}
|
||||
}
|
||||
|
||||
private fun CustomTokenFieldId.validateValue(value: Any): AddCustomTokenError? {
|
||||
return when (this) {
|
||||
ContractAddress -> {
|
||||
val contractAddressValidator: TokenContractAddressValidator = hubState.getValidator(ContractAddress)
|
||||
contractAddressValidator.nextValidationFor(Network.getFieldValue())
|
||||
contractAddressValidator.validate(value as String)
|
||||
}
|
||||
Network, DerivationPath -> {
|
||||
hubState.getValidator<TokenNetworkValidator>(Network).validate(value as Blockchain)
|
||||
}
|
||||
Name -> {
|
||||
hubState.getValidator<TokenNameValidator>(Name).validate(value as String)
|
||||
}
|
||||
Symbol -> {
|
||||
hubState.getValidator<TokenSymbolValidator>(Symbol).validate(value as String)
|
||||
}
|
||||
Decimals -> {
|
||||
hubState.getValidator<TokenDecimalsValidator>(Decimals).validate(value as String)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun CustomTokenFieldId.isFilled(): Boolean {
|
||||
return when (this) {
|
||||
ContractAddress -> getFieldValue<String>().isNotEmpty()
|
||||
Network -> getFieldValue<Blockchain>() != Blockchain.Unknown
|
||||
Name -> getFieldValue<String>().isNotEmpty()
|
||||
Symbol -> getFieldValue<String>().isNotEmpty()
|
||||
Decimals -> getFieldValue<String>().isNotEmpty()
|
||||
DerivationPath -> getFieldValue<Blockchain>() != Blockchain.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun AddCustomTokenError.Warning.add() {
|
||||
dispatchOnMain(Warning.Add(setOf(this)))
|
||||
}
|
||||
|
||||
private suspend fun AddCustomTokenError.Warning.remove() {
|
||||
dispatchOnMain(Warning.Remove(setOf(this)))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod")
|
||||
private class AddCustomTokenReducer(
|
||||
private val globalState: DomainGlobalState,
|
||||
) : ReStoreReducer<AddCustomTokenState> {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState {
|
||||
return when (action) {
|
||||
is Init.SetAddedCurrencies -> {
|
||||
state.copy(appSavedCurrencies = action.addedCurrencies)
|
||||
}
|
||||
is Init.SetOnAddTokenCallback -> {
|
||||
state.copy(onTokenAddCallback = action.callback)
|
||||
}
|
||||
is OnCreate -> {
|
||||
val scanResponse = requireNotNull(globalState.scanResponse)
|
||||
val card = globalState.scanResponse.card
|
||||
val supportedTokenNetworkIds = card.supportedBlockchains(scanResponse.cardTypesResolver)
|
||||
.filter(Blockchain::canHandleTokens)
|
||||
.map(Blockchain::toNetworkId)
|
||||
|
||||
val tangemTechServiceManager = AddCustomTokenService(
|
||||
tangemTechApi = globalState.networkServices.tangemTechService.api,
|
||||
dispatchers = AppCoroutineDispatcherProvider(),
|
||||
supportedTokenNetworkIds = supportedTokenNetworkIds,
|
||||
)
|
||||
|
||||
state.copy(
|
||||
cardDerivationStyle = globalState.scanResponse.derivationStyleProvider.getDerivationStyle(),
|
||||
form = Form(
|
||||
AddCustomTokenState.createFormFields(
|
||||
cardTypesResolver = globalState.scanResponse.cardTypesResolver,
|
||||
card = card,
|
||||
type = CustomTokenType.Blockchain,
|
||||
),
|
||||
),
|
||||
tangemTechServiceManager = tangemTechServiceManager,
|
||||
screenState = createInitialScreenState(card.settings.isHDWalletAllowed),
|
||||
)
|
||||
}
|
||||
is OnDestroy -> {
|
||||
val scanResponse = requireNotNull(globalState.scanResponse)
|
||||
val card = scanResponse.card
|
||||
state.reset(scanResponse.cardTypesResolver, card)
|
||||
}
|
||||
is UpdateForm -> {
|
||||
updateFormState(action.state)
|
||||
}
|
||||
is OnTokenContractAddressChanged -> {
|
||||
val field: TokenField = state.getField(ContractAddress)
|
||||
field.data = action.contractAddress
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenNetworkChanged -> {
|
||||
val field: TokenBlockchainField = state.getField(Network)
|
||||
field.data = action.blockchainNetwork
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenNameChanged -> {
|
||||
val field: TokenField = state.getField(Name)
|
||||
field.data = action.tokenName
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenSymbolChanged -> {
|
||||
val field: TokenField = state.getField(Symbol)
|
||||
field.data = action.tokenSymbol
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenDecimalsChanged -> {
|
||||
val field: TokenField = state.getField(Decimals)
|
||||
field.data = action.tokenDecimals
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenDerivationPathChanged -> {
|
||||
val field: TokenDerivationPathField = state.getField(DerivationPath)
|
||||
field.data = action.blockchainDerivationPath
|
||||
updateFormState(state)
|
||||
}
|
||||
is FieldError.Add -> {
|
||||
val newMap = state.formErrors.toMutableMap().apply { this[action.id] = action.error }
|
||||
state.copy(formErrors = newMap)
|
||||
}
|
||||
is FieldError.Remove -> {
|
||||
val newMap = state.formErrors.toMutableMap().apply { remove(action.id) }
|
||||
state.copy(formErrors = newMap)
|
||||
}
|
||||
is SetFoundTokenInfo -> {
|
||||
state.copy(foundToken = action.foundToken)
|
||||
}
|
||||
is Warning.Add -> {
|
||||
val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) }
|
||||
state.copy(warnings = newList.toSet())
|
||||
}
|
||||
is Warning.Remove -> {
|
||||
val newList = state.warnings.toMutableSet().apply { removeAll(action.warnings) }
|
||||
state.copy(warnings = newList.toSet())
|
||||
}
|
||||
is Warning.Replace -> {
|
||||
val newList = state.warnings.toMutableSet().apply {
|
||||
removeAll(action.remove)
|
||||
addAll(action.add)
|
||||
}
|
||||
state.copy(warnings = newList.toSet())
|
||||
}
|
||||
is Screen.UpdateTokenFields -> {
|
||||
var newScreenState = state.screenState
|
||||
action.pairs.forEach {
|
||||
newScreenState = when (it.first) {
|
||||
ContractAddress -> {
|
||||
if (state.screenState.contractAddressField == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(contractAddressField = it.second)
|
||||
}
|
||||
}
|
||||
Network -> {
|
||||
if (state.screenState.network == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(network = it.second)
|
||||
}
|
||||
}
|
||||
Name -> {
|
||||
if (state.screenState.name == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(name = it.second)
|
||||
}
|
||||
}
|
||||
Symbol -> {
|
||||
if (state.screenState.symbol == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(symbol = it.second)
|
||||
}
|
||||
}
|
||||
Decimals -> {
|
||||
if (state.screenState.decimals == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(decimals = it.second)
|
||||
}
|
||||
}
|
||||
DerivationPath -> {
|
||||
if (state.screenState.derivationPath == it.second) {
|
||||
newScreenState
|
||||
} else {
|
||||
newScreenState.copy(derivationPath = it.second)
|
||||
}
|
||||
}
|
||||
else -> newScreenState
|
||||
}
|
||||
}
|
||||
if (state.screenState == newScreenState) {
|
||||
state
|
||||
} else {
|
||||
state.copy(screenState = newScreenState)
|
||||
}
|
||||
}
|
||||
is Screen.UpdateAddButton -> {
|
||||
val newScreenState = if (state.screenState.addButton == action.addButton) {
|
||||
state.screenState
|
||||
} else {
|
||||
state.screenState.copy(addButton = action.addButton)
|
||||
}
|
||||
if (newScreenState == state.screenState) {
|
||||
state
|
||||
} else {
|
||||
state.copy(screenState = newScreenState)
|
||||
}
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState {
|
||||
return state.copy(form = Form(state.form.fieldList))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,311 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.derivation.DerivationStyle
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.AddCustomTokenError
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.extensions.isSupportedInApp
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.extensions.supportedTokens
|
||||
import com.tangem.domain.common.form.*
|
||||
import com.tangem.domain.features.addCustomToken.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.state.StringActionStateConverter
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class AddCustomTokenState(
|
||||
val appSavedCurrencies: List<DomainWrapped.Currency>? = null,
|
||||
val onTokenAddCallback: ((CustomCurrency) -> Unit)? = null,
|
||||
val cardDerivationStyle: DerivationStyle? = null,
|
||||
val form: Form = Form(listOf()),
|
||||
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<out Any>> = createFormValidators(),
|
||||
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
|
||||
val foundToken: CoinsResponse.Coin? = null,
|
||||
val warnings: Set<AddCustomTokenError.Warning> = emptySet(),
|
||||
val screenState: ScreenState = createInitialScreenState(),
|
||||
val tangemTechServiceManager: AddCustomTokenService? = null,
|
||||
) : StateType {
|
||||
|
||||
inline fun <reified T> getField(id: FieldId): T = form.getField(id) as T
|
||||
|
||||
fun setField(field: DataField<*>) {
|
||||
form.setField(field)
|
||||
}
|
||||
|
||||
inline fun <reified T> getValidator(id: FieldId): T = formValidators[id] as T
|
||||
|
||||
fun getError(id: FieldId): AddCustomTokenError? = formErrors[id]
|
||||
|
||||
inline fun <reified T> visitDataConverter(converter: FieldDataConverter<T>): T {
|
||||
form.visitDataConverter(converter)
|
||||
return converter.getConvertedData()
|
||||
}
|
||||
|
||||
fun blockchainToName(blockchain: Blockchain, isDerivationPath: Boolean = false): String? {
|
||||
return when {
|
||||
isDerivationPath -> blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath
|
||||
else -> {
|
||||
when (blockchain) {
|
||||
Blockchain.Unknown -> null
|
||||
else -> blockchain.fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// except network
|
||||
fun tokensFieldsIsFilled(): Boolean {
|
||||
val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals)
|
||||
val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) }
|
||||
val validator = StringIsNotEmptyValidator()
|
||||
fieldsToCheck.forEach { field ->
|
||||
val error = validator.validate(field.data.value?.toString())
|
||||
if (error != null) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// except network
|
||||
fun tokensAnyFieldsIsFilled(): Boolean {
|
||||
val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals)
|
||||
val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) }
|
||||
val validator = StringIsEmptyValidator()
|
||||
val errorsList = fieldsToCheck.mapNotNull { field ->
|
||||
validator.validate(field.data.value?.toString())
|
||||
}
|
||||
return errorsList.isNotEmpty()
|
||||
}
|
||||
|
||||
fun networkIsSelected(): Boolean {
|
||||
val network = getField<TokenBlockchainField>(Network)
|
||||
return network.data.value != Blockchain.Unknown
|
||||
}
|
||||
|
||||
fun derivationPathIsSelected(): Boolean {
|
||||
val network = getField<TokenDerivationPathField>(DerivationPath)
|
||||
return network.data.value != Blockchain.Unknown
|
||||
}
|
||||
|
||||
fun getCustomTokenType(): CustomTokenType {
|
||||
return if (tokensAnyFieldsIsFilled() || tokensFieldsIsFilled()) {
|
||||
CustomTokenType.Token
|
||||
} else {
|
||||
CustomTokenType.Blockchain
|
||||
}
|
||||
}
|
||||
|
||||
fun gatherUserToken(): CustomCurrency.CustomToken? = try {
|
||||
getToken()
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
fun gatherBlockchain(): CustomCurrency.CustomBlockchain? = try {
|
||||
getBlockchain()
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
fun reset(cardTypesResolver: CardTypesResolver, card: CardDTO): AddCustomTokenState {
|
||||
return this.copy(
|
||||
appSavedCurrencies = null,
|
||||
onTokenAddCallback = null,
|
||||
cardDerivationStyle = null,
|
||||
form = Form(createFormFields(cardTypesResolver, card, CustomTokenType.Blockchain)),
|
||||
formErrors = emptyMap(),
|
||||
foundToken = null,
|
||||
warnings = emptySet(),
|
||||
screenState = createInitialScreenState(card.settings.isHDWalletAllowed),
|
||||
tangemTechServiceManager = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getToken(): CustomCurrency.CustomToken {
|
||||
return CustomCurrency.CustomToken.Converter(foundToken?.id, cardDerivationStyle)
|
||||
.apply { visitDataConverter(this) }
|
||||
.getConvertedData()
|
||||
}
|
||||
|
||||
private fun getBlockchain(): CustomCurrency.CustomBlockchain {
|
||||
return CustomCurrency.CustomBlockchain.Converter(cardDerivationStyle)
|
||||
.apply { visitDataConverter(this) }
|
||||
.getConvertedData()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* If an user select derivation path (derivationNetwork) as Blockchain.Unknown,
|
||||
* then we should use a blockchain from the mainNetwork to determine a DerivationPath
|
||||
*/
|
||||
internal fun getDerivationPath(
|
||||
mainNetwork: Blockchain,
|
||||
derivationNetwork: Blockchain,
|
||||
derivationStyle: DerivationStyle?,
|
||||
): com.tangem.crypto.hdWallet.DerivationPath? {
|
||||
// If we allow user to select derivations, we need to provide different derivations
|
||||
// (Legacy style derivations).
|
||||
// But the mainNetwork derivation depends on whether a user has a card
|
||||
// with legacy derivations or new style derivations.
|
||||
val derivationStyleToUse = if (derivationNetwork == Blockchain.Unknown) {
|
||||
derivationStyle
|
||||
} else {
|
||||
DerivationStyle.LEGACY
|
||||
}
|
||||
return when (derivationNetwork) {
|
||||
Blockchain.Unknown -> mainNetwork
|
||||
else -> derivationNetwork
|
||||
}.derivationPath(derivationStyleToUse)
|
||||
}
|
||||
|
||||
internal fun createFormFields(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
card: CardDTO,
|
||||
type: CustomTokenType,
|
||||
): List<DataField<*>> {
|
||||
return listOf(
|
||||
TokenField(ContractAddress),
|
||||
TokenBlockchainField(Network, getNetworksList(cardTypesResolver, card, type)),
|
||||
TokenField(Name),
|
||||
TokenField(Symbol),
|
||||
TokenField(Decimals),
|
||||
TokenDerivationPathField(DerivationPath, getSupportedDerivations(card)),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serves to determine the networks (blockchains & tokens) that can be selected by Form.Networks.
|
||||
* Blockchain.Unknown - is the default selection
|
||||
*/
|
||||
private fun getNetworksList(
|
||||
cardTypesResolver: CardTypesResolver,
|
||||
card: CardDTO,
|
||||
type: CustomTokenType,
|
||||
): List<Blockchain> {
|
||||
val evmBlockchains = Blockchain.values()
|
||||
.filter { it.isEvm() }
|
||||
.filter { card.isTestCard == it.isTestnet() }
|
||||
|
||||
val additionalBlockchains = listOf(
|
||||
Blockchain.Binance,
|
||||
Blockchain.BinanceTestnet,
|
||||
Blockchain.Solana,
|
||||
Blockchain.SolanaTestnet,
|
||||
Blockchain.Tron,
|
||||
Blockchain.TronTestnet,
|
||||
)
|
||||
|
||||
val supportedByCard = when (type) {
|
||||
CustomTokenType.Blockchain -> card.supportedBlockchains(cardTypesResolver)
|
||||
CustomTokenType.Token -> card.supportedTokens(cardTypesResolver)
|
||||
}
|
||||
val typedNetworksList = (evmBlockchains + additionalBlockchains)
|
||||
.filter { supportedByCard.contains(it) }
|
||||
.toMutableList()
|
||||
|
||||
val default = Blockchain.Unknown
|
||||
typedNetworksList.add(0, default)
|
||||
|
||||
return typedNetworksList.sortByName()
|
||||
}
|
||||
|
||||
private fun createFormValidators(): Map<CustomTokenFieldId, CustomTokenValidator<out Any>> {
|
||||
return mapOf(
|
||||
ContractAddress to TokenContractAddressValidator(),
|
||||
Network to TokenNetworkValidator(),
|
||||
Name to TokenNameValidator(),
|
||||
Symbol to TokenSymbolValidator(),
|
||||
Decimals to TokenDecimalsValidator(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportedDerivations(card: CardDTO): List<Blockchain> {
|
||||
val evmBlockchains = Blockchain.values()
|
||||
.filter { card.isTestCard == it.isTestnet() && it.isEvm() }
|
||||
.filter { it.isSupportedInApp() }
|
||||
|
||||
return (listOf(Blockchain.Unknown) + evmBlockchains).sortByName()
|
||||
}
|
||||
|
||||
internal fun createInitialScreenState(showDerivationPathField: Boolean = false): ScreenState {
|
||||
return ScreenState(
|
||||
contractAddressField = ViewStates.TokenField(),
|
||||
network = ViewStates.TokenField(),
|
||||
name = ViewStates.TokenField(isEnabled = false),
|
||||
symbol = ViewStates.TokenField(isEnabled = false),
|
||||
decimals = ViewStates.TokenField(isEnabled = false),
|
||||
derivationPath = ViewStates.TokenField(isVisible = showDerivationPathField),
|
||||
addButton = ViewStates.AddButton(isEnabled = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Converter : StringActionStateConverter<DomainState> {
|
||||
private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE
|
||||
private var builder: StringBuilder = StringBuilder()
|
||||
|
||||
override fun convert(action: Action, stateHolder: DomainState): String? {
|
||||
if (action !is AddCustomTokenAction) return null
|
||||
|
||||
val state = stateHolder.addCustomTokensState
|
||||
val fieldConverter =
|
||||
FieldToJsonConverter(
|
||||
listOf(
|
||||
ContractAddress,
|
||||
Network,
|
||||
Name,
|
||||
Symbol,
|
||||
Decimals,
|
||||
DerivationPath,
|
||||
),
|
||||
jsonConverter,
|
||||
)
|
||||
state.visitDataConverter(fieldConverter)
|
||||
val errors = state.formErrors.map {
|
||||
"${it.key}: ${it.value::class.java.simpleName}"
|
||||
}
|
||||
val warnings = state.warnings.map { it::class.java.simpleName }
|
||||
|
||||
printAction(action, state)
|
||||
printStateValue("fields", fieldConverter.getConvertedData())
|
||||
printStateValue("fieldErrors", toJson(errors))
|
||||
printStateValue("warnings", toJson(warnings))
|
||||
printStateValue("screenState", toJson(state.screenState))
|
||||
printMessage("------------------------------------------------------")
|
||||
|
||||
val printed = builder.toString()
|
||||
builder = StringBuilder()
|
||||
|
||||
return printed
|
||||
}
|
||||
|
||||
private fun printStateValue(name: String, value: String) {
|
||||
printMessage("$name: $value")
|
||||
}
|
||||
|
||||
private fun printAction(action: AddCustomTokenAction, state: AddCustomTokenState) {
|
||||
printMessage("action: $action, state: ${state::class.java.simpleName}")
|
||||
}
|
||||
|
||||
private fun toJson(value: Any): String {
|
||||
return jsonConverter.prettyPrint(value)
|
||||
}
|
||||
|
||||
private fun printMessage(message: String) {
|
||||
builder.append("$message\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Blockchain>.sortByName(): List<Blockchain> = this.sortedBy { it.fullName }
|
||||
|
||||
enum class CustomTokenType {
|
||||
Token, Blockchain
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
// describes state the screen, except the form fields
|
||||
data class ScreenState(
|
||||
val contractAddressField: ViewStates.TokenField,
|
||||
val network: ViewStates.TokenField,
|
||||
val name: ViewStates.TokenField,
|
||||
val symbol: ViewStates.TokenField,
|
||||
val decimals: ViewStates.TokenField,
|
||||
val derivationPath: ViewStates.TokenField,
|
||||
val addButton: ViewStates.AddButton,
|
||||
)
|
||||
|
||||
sealed class ViewStates {
|
||||
data class TokenField(
|
||||
val isLoading: Boolean = false,
|
||||
val isEnabled: Boolean = true,
|
||||
val isVisible: Boolean = true,
|
||||
) : ViewStates()
|
||||
|
||||
data class AddButton(
|
||||
val isEnabled: Boolean = true,
|
||||
) : ViewStates()
|
||||
}
|
||||
|
|
@ -1,13 +1,9 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
import org.rekotlin.StateType
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class DomainState(
|
||||
val globalState: DomainGlobalState = DomainGlobalState(),
|
||||
val addCustomTokensState: AddCustomTokenState = AddCustomTokenState(),
|
||||
) : StateType
|
||||
data class DomainState(val globalState: DomainGlobalState = DomainGlobalState()) : StateType
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import com.tangem.domain.DomainLayer
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub
|
||||
import com.tangem.domain.redux.global.DomainGlobalHub
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Store
|
||||
|
|
@ -9,10 +7,7 @@ import org.rekotlin.Store
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(
|
||||
DomainGlobalHub(),
|
||||
AddCustomTokenHub(),
|
||||
)
|
||||
private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(DomainGlobalHub())
|
||||
|
||||
val domainStore = Store(
|
||||
state = DomainState(),
|
||||
|
|
@ -37,7 +32,6 @@ private fun reduce(action: Action, domainState: DomainState?): DomainState {
|
|||
assembleReducedDomainState
|
||||
}
|
||||
}
|
||||
DomainLayer.actionStateLogger.log(reducedStatesByAction)
|
||||
|
||||
return assembleReducedDomainState
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package com.tangem.domain.redux.global
|
||||
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -10,5 +9,4 @@ import org.rekotlin.Action
|
|||
// TODO: refactoring: is alias for the GlobalAction
|
||||
sealed class DomainGlobalAction : Action {
|
||||
data class SaveScanNoteResponse(val scanResponse: ScanResponse) : DomainGlobalAction()
|
||||
data class ShowDialog(val stateDialog: DomainDialog?) : DomainGlobalAction()
|
||||
}
|
||||
|
|
@ -47,9 +47,6 @@ private class DomainGlobalReducer : ReStoreReducer<DomainGlobalState> {
|
|||
)
|
||||
state.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
is DomainGlobalAction.ShowDialog -> {
|
||||
state.copy(dialog = action.stateDialog)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package com.tangem.domain.redux.global
|
|||
|
||||
import com.tangem.datasource.api.paymentology.PaymentologyApiService
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechService
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
||||
/**
|
||||
|
|
@ -14,7 +13,6 @@ data class DomainGlobalState(
|
|||
val scanResponse: ScanResponse? = null,
|
||||
//
|
||||
val networkServices: NetworkServices = NetworkServices(),
|
||||
val dialog: DomainDialog? = null,
|
||||
)
|
||||
|
||||
data class NetworkServices(
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
package com.tangem.domain.redux.state
|
||||
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface StringStateConverter<StateHolder> {
|
||||
fun convert(stateHolder: StateHolder): String
|
||||
}
|
||||
|
||||
interface StringActionStateConverter<StateHolder> {
|
||||
fun convert(action: Action, stateHolder: StateHolder): String?
|
||||
}
|
||||
|
||||
class ActionStateConvertersFactory {
|
||||
private val stateConverters = mutableMapOf<Class<out Action>, StringActionStateConverter<DomainState>>()
|
||||
|
||||
fun addConverter(classOfAction: Class<out Action>, converter: StringActionStateConverter<DomainState>) {
|
||||
stateConverters[classOfAction] = converter
|
||||
}
|
||||
|
||||
fun getConverter(action: Action): StringActionStateConverter<DomainState>? {
|
||||
val converter = stateConverters.firstNotNullOfOrNull { (classOfAction, converter) ->
|
||||
if (classOfAction.isAssignableFrom(action::class.java)) {
|
||||
converter
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
return converter
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package com.tangem.domain.redux.state
|
||||
|
||||
import com.tangem.domain.features.BuildConfig
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Use it only in debug mode!
|
||||
*/
|
||||
internal interface ActionStateLogger {
|
||||
fun log(reducedSates: List<Pair<Action, DomainState>>)
|
||||
}
|
||||
|
||||
internal class ActionStateLoggerImpl : ActionStateLogger {
|
||||
|
||||
val actionStateConvertersFactory = ActionStateConvertersFactory()
|
||||
|
||||
override fun log(reducedSates: List<Pair<Action, DomainState>>) {
|
||||
if (!BuildConfig.LOG_ENABLED) return
|
||||
|
||||
logStates(reducedSates)
|
||||
}
|
||||
|
||||
private fun logStates(reducedSates: List<Pair<Action, DomainState>>) {
|
||||
reducedSates.forEach { (action, domainState) ->
|
||||
val messageToPrint = actionStateConvertersFactory.getConverter(action)
|
||||
?.convert(action, domainState)
|
||||
?: return@forEach
|
||||
|
||||
Timber.d(messageToPrint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.redux.state
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface StringStateConverter<StateHolder> {
|
||||
fun convert(stateHolder: StateHolder): String
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue