Updated on 2026-08-14
This commit is contained in:
commit
eb800d32cc
25 changed files with 792 additions and 399 deletions
|
|
@ -17,7 +17,6 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.DomainStateDialog
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.domain.redux.global.DomainGlobalState
|
||||
|
|
@ -28,7 +27,7 @@ import org.rekotlin.StoreSubscriber
|
|||
|
||||
@Composable
|
||||
fun ComposeDialogManager() {
|
||||
val dialogSate = remember { mutableStateOf<DomainStateDialog?>(null) }
|
||||
val dialogSate = remember { mutableStateOf<DomainDialog?>(null) }
|
||||
val subscriber = remember {
|
||||
object : StoreSubscriber<DomainGlobalState> {
|
||||
override fun newState(state: DomainGlobalState) {
|
||||
|
|
@ -52,7 +51,7 @@ fun ComposeDialogManager() {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowTheDialog(dialogState: MutableState<DomainStateDialog?>) {
|
||||
private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
|
||||
if (dialogState.value == null) return
|
||||
|
||||
val context = LocalContext.current
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
|
|
@ -35,7 +33,7 @@ fun <T> OutlinedSpinner(
|
|||
rSelectedItem.value = selectedItem.value
|
||||
}
|
||||
|
||||
val onItemSelectedInternal: (T) -> Unit = {
|
||||
val onDropDownItemSelectedInternal: (T) -> Unit = {
|
||||
rSelectedItem.value = it
|
||||
rIsExpanded.value = false
|
||||
onItemSelected(it)
|
||||
|
|
@ -49,23 +47,23 @@ fun <T> OutlinedSpinner(
|
|||
expanded = rIsExpanded.value,
|
||||
onExpandedChange = { rIsExpanded.value = !rIsExpanded.value },
|
||||
) {
|
||||
ProvideTextStyle(value = TextStyle(color = Color.Blue)) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
readOnly = true,
|
||||
enabled = isEnabled,
|
||||
value = textFieldConverter(rSelectedItem.value),
|
||||
onValueChange = {},
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
|
||||
)
|
||||
}
|
||||
OutlinedTextField(
|
||||
modifier = modifier,
|
||||
readOnly = true,
|
||||
enabled = isEnabled,
|
||||
value = textFieldConverter(rSelectedItem.value),
|
||||
onValueChange = {},
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) },
|
||||
)
|
||||
|
||||
if (!isEnabled) return@ExposedDropdownMenuBox
|
||||
ExposedDropdownMenu(
|
||||
expanded = rIsExpanded.value,
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
itemList.forEach { item ->
|
||||
DropdownMenuItem(onClick = { onItemSelectedInternal(item) }) {
|
||||
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
|
||||
when (dropdownItemView) {
|
||||
null -> Text(textFieldConverter(item))
|
||||
else -> dropdownItemView(item)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
|
|
@ -105,7 +106,13 @@ private fun OutlinedProgressTextField(
|
|||
onValueChange = ::updateFieldValueAndEmmit,
|
||||
keyboardOptions = keyboardOptions,
|
||||
label = { Text(label) },
|
||||
placeholder = { Text(placeholder) },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = placeholder,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
trailingIcon = trailingIcon,
|
||||
singleLine = true,
|
||||
enabled = isEnabled,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
|
|||
import com.tangem.tap.features.wallet.ui.WalletFragment
|
||||
import com.tangem.wallet.R
|
||||
|
||||
private class Navigation
|
||||
|
||||
fun FragmentActivity.openFragment(
|
||||
screen: AppScreen,
|
||||
addToBackstack: Boolean,
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
|
|||
val amountToCreateAccount = blockchain.amountToCreateAccount(wallet.getFirstToken())
|
||||
|
||||
if (exception is BlockchainSdkError.AccountNotFound && amountToCreateAccount != null) {
|
||||
Result.Failure(TapError.WalletManagerUpdate.NoAccountError(amountToCreateAccount.toString()))
|
||||
Result.Failure(TapError.WalletManager.NoAccountError(amountToCreateAccount.toString()))
|
||||
} else {
|
||||
val message = exception.localizedMessage ?: "An error has occurred. Try later"
|
||||
Result.Failure(TapError.WalletManagerUpdate.InternalError(message))
|
||||
Result.Failure(TapError.WalletManager.InternalError(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import android.content.Context
|
|||
import android.widget.Toast
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.tangem.tap.common.extensions.getColor
|
||||
import com.tangem.tap.domain.ArgError
|
||||
import com.tangem.tap.domain.MultiMessageError
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.assembleErrors
|
||||
import com.tangem.tap.notificationsHandler
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import java.lang.ref.WeakReference
|
||||
|
|
@ -29,7 +31,17 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
|
|||
fun showNotification(message: String) {
|
||||
baseLayout.get()?.let { layout ->
|
||||
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
|
||||
.also { snackbar -> snackbar.show() }
|
||||
.also { snackbar -> snackbar.show() }
|
||||
}
|
||||
}
|
||||
|
||||
fun showDebugNotification(message: String) {
|
||||
baseLayout.get()?.let { layout ->
|
||||
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
|
||||
.also { snackbar ->
|
||||
snackbar.setBackgroundTint(layout.getColor(R.color.warning_warning))
|
||||
snackbar.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +63,12 @@ class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
|
|||
val message = builder(errorList.map { getMessageString(context, it.first, it.second) })
|
||||
showNotification(message)
|
||||
}
|
||||
|
||||
fun showDebugErrorNotification(message: Int, args: List<Any>? = null) {
|
||||
baseLayout.get()?.let {
|
||||
showDebugNotification(getMessageString(it.context, message, args))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getMessageString(context: Context, message: Int, args: List<Any>?): String {
|
||||
|
|
@ -74,17 +92,37 @@ private fun handleNotificationAction(action: Action) {
|
|||
if (action is Debug && !BuildConfig.DEBUG) return
|
||||
|
||||
when (action) {
|
||||
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
|
||||
is NotificationAction -> {
|
||||
notificationsHandler?.showNotification(action.messageResource)
|
||||
}
|
||||
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
|
||||
is ErrorAction -> {
|
||||
when (action.error) {
|
||||
is MultiMessageError -> {
|
||||
val multiError = action.error as MultiMessageError
|
||||
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
|
||||
when (action) {
|
||||
is Debug -> {
|
||||
val args = (action.error as? ArgError)?.args ?: listOf()
|
||||
when (action) {
|
||||
is DebugNotification -> {
|
||||
notificationsHandler?.showNotification(action.error.messageResource, args)
|
||||
}
|
||||
is DebugToastNotification -> {
|
||||
notificationsHandler?.showToastNotification(action.error.messageResource, args)
|
||||
}
|
||||
is DebugErrorAction -> {
|
||||
notificationsHandler?.showDebugErrorNotification(action.error.messageResource, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val args = (action.error as? ArgError)?.args ?: listOf()
|
||||
notificationsHandler?.showNotification(action.error.messageResource, args)
|
||||
when (action.error) {
|
||||
is MultiMessageError -> {
|
||||
val multiError = action.error as MultiMessageError
|
||||
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
|
||||
}
|
||||
else -> {
|
||||
val args = (action.error as? ArgError)?.args ?: listOf()
|
||||
notificationsHandler?.showNotification(action.error.messageResource, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,8 @@ sealed class TapError(
|
|||
object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm)
|
||||
}
|
||||
|
||||
sealed class WalletManagerUpdate {
|
||||
sealed class WalletManager {
|
||||
object CreationError: CustomError("Can't create wallet manager")
|
||||
class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount)
|
||||
class InternalError(message: String): CustomError(message)
|
||||
object BlockchainIsUnreachable: TapError(R.string.wallet_balance_blockchain_unreachable)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import com.tangem.blockchain.blockchains.solana.RentProvider
|
|||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.tap.common.ThrottlerWithValues
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
@ -19,7 +19,6 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
|
|||
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
|
||||
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
|
||||
import com.tangem.tap.domain.tokens.CardCurrencies
|
||||
import com.tangem.tap.domain.tokens.BlockchainNetwork
|
||||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.features.wallet.models.PendingTransactionType
|
||||
|
|
@ -61,12 +60,12 @@ class TapWalletManager {
|
|||
}
|
||||
is Result.Failure -> {
|
||||
when (result.error) {
|
||||
is TapError.WalletManagerUpdate.NoAccountError -> {
|
||||
is TapError.WalletManager.NoAccountError -> {
|
||||
dispatchOnMain(
|
||||
WalletAction.LoadWallet.NoAccount(
|
||||
walletManager.wallet,
|
||||
blockchainNetwork,
|
||||
(result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage
|
||||
(result.error as TapError.WalletManager.NoAccountError).customMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class OnboardingManager(
|
|||
|
||||
suspend fun loadArtworkUrl(): String {
|
||||
val cardInfo = cardInfo
|
||||
?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
|
||||
?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
|
||||
this.cardInfo = cardInfo
|
||||
return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo)
|
||||
}
|
||||
|
|
@ -57,11 +57,11 @@ class OnboardingManager(
|
|||
is Result.Failure -> {
|
||||
val error = (result.error as? TapError) ?: TapError.UnknownError
|
||||
when (error) {
|
||||
is TapError.WalletManagerUpdate.NoAccountError -> OnboardingWalletBalance.error(error)
|
||||
// NoInternetConnection, WalletManagerUpdate.InternalError
|
||||
is TapError.WalletManager.NoAccountError -> OnboardingWalletBalance.error(error)
|
||||
// NoInternetConnection, WalletManager.InternalError
|
||||
else -> {
|
||||
Timber.e(error.localizedMessage)
|
||||
OnboardingWalletBalance.criticalError(TapError.WalletManagerUpdate.BlockchainIsUnreachableTryLater)
|
||||
OnboardingWalletBalance.criticalError(TapError.WalletManager.BlockchainIsUnreachableTryLater)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,7 +93,7 @@ data class OnboardingWalletBalance(
|
|||
fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction
|
||||
|
||||
val amountToCreateAccount: String?
|
||||
get() = if (error is TapError.WalletManagerUpdate.NoAccountError) error.customMessage else null
|
||||
get() = if (error is TapError.WalletManager.NoAccountError) error.customMessage else null
|
||||
|
||||
companion object {
|
||||
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ fun TokenContractAddressView(screenFieldData: ScreenFieldData) {
|
|||
OutlinedTextFieldWidget(
|
||||
textFieldData = tokenField.data,
|
||||
labelId = R.string.custom_token_contract_address_input_title,
|
||||
placeholder = "0x0000000000000000",
|
||||
placeholder = "0x0000000000000000000000000000000000000000",
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
isLoading = screenFieldData.viewState.isLoading,
|
||||
error = screenFieldData.error,
|
||||
|
|
@ -71,7 +71,7 @@ fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenStat
|
|||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) }
|
||||
SpacerH8()
|
||||
}
|
||||
|
|
@ -123,11 +123,11 @@ fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTo
|
|||
itemList = networkField.itemList,
|
||||
selectedItem = networkField.data,
|
||||
isEnabled = screenFieldData.viewState.isEnabled,
|
||||
textFieldConverter = { state.convertBlockchainName(it, notSelected) },
|
||||
textFieldConverter = { state.blockchainToName(it) ?: notSelected },
|
||||
dropdownItemView = { blockchain ->
|
||||
val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected)
|
||||
val blockchainName = state.convertBlockchainName(blockchain, notSelected)
|
||||
TitleSubtitle(derivationPathLabel, blockchainName)
|
||||
val derivationPathName = state.blockchainToName(blockchain, true) ?: notSelected
|
||||
val blockchainName = state.blockchainToName(blockchain) ?: notSelected
|
||||
TitleSubtitle(derivationPathName, blockchainName)
|
||||
}
|
||||
) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) }
|
||||
SpacerH8()
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ import com.tangem.domain.common.KeyWalletPublicKey
|
|||
import com.tangem.domain.common.ScanResponse
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.features.addCustomToken.CompleteData
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddedCurrencies
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
|
|
@ -28,19 +28,16 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
|
|||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
|
||||
import com.tangem.tap.domain.tokens.BlockchainNetwork
|
||||
import com.tangem.tap.features.wallet.redux.Currency
|
||||
import com.tangem.tap.features.wallet.redux.WalletAction
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
||||
class TokensMiddleware {
|
||||
|
||||
val tokensMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
val tokensMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
when (action) {
|
||||
|
|
@ -58,8 +55,10 @@ class TokensMiddleware {
|
|||
val isTestcard = scanResponse?.card?.isTestCard ?: false
|
||||
|
||||
scope.launch {
|
||||
val currencies = async { currenciesRepository.getSupportedTokens(isTestcard)
|
||||
.filter(action.supportedBlockchains?.toSet()) }
|
||||
val currencies = async {
|
||||
currenciesRepository.getSupportedTokens(isTestcard)
|
||||
.filter(action.supportedBlockchains?.toSet())
|
||||
}
|
||||
val delay = async { delay(600) }
|
||||
delay.await()
|
||||
store.dispatchOnMain(TokensAction.LoadCurrencies.Success(currencies.await()))
|
||||
|
|
@ -69,6 +68,7 @@ class TokensMiddleware {
|
|||
private fun handleSaveChanges(action: TokensAction.SaveChanges) {
|
||||
val scanResponse = store.state.globalState.scanResponse ?: return
|
||||
|
||||
//TODO: bad things happens.
|
||||
val currentTokens = store.state.tokensState.addedWallets.toTokens()
|
||||
val currentBlockchains = store.state.tokensState.addedWallets.toBlockchains(
|
||||
store.state.tokensState.derivationStyle
|
||||
|
|
@ -83,44 +83,42 @@ class TokensMiddleware {
|
|||
removeCurrenciesIfNeeded(blockchainsToRemove, tokensToRemove)
|
||||
|
||||
if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) {
|
||||
store.dispatchDebugErrorNotification("Nothing to save")
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
return
|
||||
}
|
||||
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
val currencyList = blockchainsToAdd.map {
|
||||
Currency.Blockchain(it, it.derivationPath(derivationStyle)?.rawPath)
|
||||
} + tokensToAdd.map {
|
||||
Currency.Token(it.token, it.blockchain, it.blockchain.derivationPath(derivationStyle)?.rawPath)
|
||||
}
|
||||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingBlockchains(scanResponse, blockchainsToAdd, tokensToAdd)
|
||||
deriveMissingBlockchains(scanResponse, currencyList) {
|
||||
submitAdd(it, currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
} else {
|
||||
submitAdd(blockchainsToAdd, tokensToAdd, scanResponse)
|
||||
store.dispatch(NavigationAction.PopBackTo())
|
||||
submitAdd(scanResponse, currencyList)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
|
||||
val tokensState = store.state.tokensState
|
||||
val addedTokensList = tokensState.addedTokens.map {
|
||||
DomainWrapped.TokenWithBlockchain(it.token.copy(), it.blockchain)
|
||||
}
|
||||
val addedBlockchains = tokensState.addedBlockchains.map { it }
|
||||
val addedCurrencies = AddedCurrencies(addedTokensList, addedBlockchains)
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
|
||||
|
||||
val callback = fun(data: CompleteData) {
|
||||
Timber.e("Yoooohhhoooo")
|
||||
}
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(callback))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
}
|
||||
|
||||
private fun deriveMissingBlockchains(
|
||||
scanResponse: ScanResponse,
|
||||
blockchains: List<Blockchain>,
|
||||
tokens: List<TokenWithBlockchain>
|
||||
currencyList: List<Currency>,
|
||||
onSuccess: (ScanResponse) -> Unit,
|
||||
) {
|
||||
val derivationDataList = listOfNotNull(
|
||||
getDerivations(EllipticCurve.Secp256k1, scanResponse, blockchains, tokens),
|
||||
getDerivations(EllipticCurve.Ed25519, scanResponse, blockchains, tokens)
|
||||
getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList),
|
||||
getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList)
|
||||
)
|
||||
val derivations = derivationDataList.map { it.derivations }.toMap()
|
||||
val derivations = derivationDataList.associate { it.derivations }
|
||||
if (derivations.isEmpty()) {
|
||||
onSuccess(scanResponse)
|
||||
return
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
val result = tangemSdkManager.derivePublicKeys(
|
||||
|
|
@ -145,10 +143,8 @@ class TokensMiddleware {
|
|||
derivedKeys = updatedDerivedKeys
|
||||
)
|
||||
store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
|
||||
submitAdd(blockchains, tokens, updatedScanResponse)
|
||||
|
||||
delay(DELAY_SDK_DIALOG_CLOSE)
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
onSuccess(updatedScanResponse)
|
||||
}
|
||||
is CompletionResult.Failure -> {
|
||||
store.dispatchErrorNotification(TapError.CustomError("Error adding tokens"))
|
||||
|
|
@ -160,20 +156,31 @@ class TokensMiddleware {
|
|||
private fun getDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
blockchains: List<Blockchain>,
|
||||
tokens: List<TokenWithBlockchain>
|
||||
currencyList: List<Currency>,
|
||||
): DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
|
||||
val derivationPathsCandidates = (blockchains + tokens.map { it.blockchain }).distinct()
|
||||
.mapNotNull { it.derivationPath(scanResponse.card.derivationStyle) }
|
||||
val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter {
|
||||
it.getSupportedCurves().contains(curve)
|
||||
}.mapNotNull {
|
||||
it.derivationPath(scanResponse.card.derivationStyle)
|
||||
}
|
||||
|
||||
val customTokensCandidates = currencyList.filter {
|
||||
it.blockchain.getSupportedCurves().contains(curve)
|
||||
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
|
||||
|
||||
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
|
||||
if (bothCandidates.isEmpty()) return null
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||
|
||||
val toDerive = derivationPathsCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
if (toDerive.isEmpty()) return null
|
||||
|
||||
return DerivationData(
|
||||
derivations = mapKeyOfWalletPublicKey to toDerive,
|
||||
alreadyDerivedKeys = alreadyDerivedKeys,
|
||||
|
|
@ -188,20 +195,42 @@ class TokensMiddleware {
|
|||
)
|
||||
|
||||
private fun submitAdd(
|
||||
blockchains: List<Blockchain>, tokens: List<TokenWithBlockchain>, scanResponse: ScanResponse,
|
||||
scanResponse: ScanResponse,
|
||||
currencyList: List<Currency>,
|
||||
) {
|
||||
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
|
||||
val derivationStyle = scanResponse.card.derivationStyle
|
||||
|
||||
(blockchains.mapNotNull {
|
||||
val walletManager = factory.makeWalletManagerForApp(
|
||||
scanResponse, it,
|
||||
scanResponse.card.derivationStyle?.let { DerivationParams.Default(it) }
|
||||
) ?: return@mapNotNull null
|
||||
WalletAction.MultiWallet.AddBlockchain(BlockchainNetwork.fromWalletManager(walletManager), walletManager)
|
||||
} + tokens.map {
|
||||
val blockchainNetwork = BlockchainNetwork(it.blockchain, scanResponse.card)
|
||||
WalletAction.MultiWallet.AddToken(it.token, blockchainNetwork)
|
||||
}).forEach { store.dispatchOnMain(it) }
|
||||
val addActions = currencyList.mapNotNull { currency ->
|
||||
when (currency) {
|
||||
is Currency.Blockchain -> {
|
||||
val derivationPath = currency.derivationPath?.let { DerivationPath(it) }
|
||||
|
||||
val derivationParams = derivationStyle?.let {
|
||||
when (derivationPath) {
|
||||
null -> DerivationParams.Default(derivationStyle)
|
||||
else -> DerivationParams.Custom(derivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
val walletManager = factory.makeWalletManagerForApp(
|
||||
scanResponse = scanResponse,
|
||||
blockchain = currency.blockchain,
|
||||
derivationParams = derivationParams
|
||||
) ?: return@mapNotNull null
|
||||
val blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
WalletAction.MultiWallet.AddBlockchain(blockchainNetwork, walletManager)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
val rawDerivationPath = currency.derivationPath
|
||||
?: currency.blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
|
||||
val blockchainNetwork = BlockchainNetwork(currency.blockchain, rawDerivationPath, emptyList())
|
||||
WalletAction.MultiWallet.AddToken(currency.token, blockchainNetwork)
|
||||
}
|
||||
}
|
||||
}
|
||||
addActions.forEach { store.dispatchOnMain(it) }
|
||||
}
|
||||
|
||||
private fun removeCurrenciesIfNeeded(blockchains: List<Blockchain>, tokens: List<Token>) {
|
||||
|
|
@ -221,4 +250,45 @@ class TokensMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
|
||||
return currency.derivationPath?.let {
|
||||
!scanResponse.hasDerivation(currency.blockchain, it)
|
||||
} ?: false
|
||||
}
|
||||
|
||||
private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) {
|
||||
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())
|
||||
}
|
||||
|
||||
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.wallets.map { walletStore ->
|
||||
walletStore.walletsData.map { walletData -> walletData.currency }
|
||||
}.flatten().map {
|
||||
when (it) {
|
||||
is Currency.Blockchain -> DomainWrapped.Currency.Blockchain(it.blockchain, it.derivationPath)
|
||||
is Currency.Token -> DomainWrapped.Currency.Token(it.token, it.blockchain, it.derivationPath)
|
||||
}
|
||||
}
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies))
|
||||
domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(onAddCustomToken))
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.blockchain.common.address.AddressType
|
|||
import com.tangem.blockchain.extensions.isAboveZero
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.tap.common.entities.Button
|
||||
import com.tangem.tap.common.extensions.toQrCode
|
||||
import com.tangem.tap.common.redux.StateDialog
|
||||
|
|
@ -17,6 +18,7 @@ import com.tangem.tap.domain.extensions.sellIsAllowed
|
|||
import com.tangem.tap.domain.extensions.toSendableAmounts
|
||||
import com.tangem.tap.domain.tokens.BlockchainNetwork
|
||||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
||||
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
|
||||
import com.tangem.tap.features.wallet.models.PendingTransaction
|
||||
import com.tangem.tap.features.wallet.models.toPendingTransactions
|
||||
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken
|
||||
|
|
@ -439,6 +441,28 @@ sealed interface Currency {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun fromCustomCurrency(customCurrency: CustomCurrency): Currency {
|
||||
return when (customCurrency) {
|
||||
is CustomCurrency.CustomBlockchain -> Blockchain(
|
||||
blockchain = customCurrency.network,
|
||||
derivationPath = customCurrency.derivationPath?.rawPath
|
||||
)
|
||||
is CustomCurrency.CustomToken -> Token(
|
||||
token = customCurrency.token,
|
||||
blockchain = customCurrency.network,
|
||||
derivationPath = customCurrency.derivationPath?.rawPath,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun fromTokenWithBlockchain(tokenWithBlockchain: TokenWithBlockchain): Token {
|
||||
return Currency.Token(
|
||||
token = tokenWithBlockchain.token,
|
||||
blockchain = tokenWithBlockchain.blockchain,
|
||||
derivationPath = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -235,7 +235,8 @@ class MultiWalletMiddleware {
|
|||
token, blockchainNetwork.blockchain, blockchainNetwork.derivationPath
|
||||
)
|
||||
}))
|
||||
walletManager.addTokens(tokens)
|
||||
if (tokens.isNotEmpty()) walletManager.addTokens(tokens)
|
||||
|
||||
currenciesRepository.saveUpdatedCurrency(
|
||||
cardId = scanResponse.card.cardId,
|
||||
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
|
||||
|
|
|
|||
|
|
@ -6,16 +6,14 @@ import com.tangem.network.api.tangemTech.Coins
|
|||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface DomainStateDialog
|
||||
sealed interface DomainDialog {
|
||||
|
||||
sealed class DomainDialog : DomainStateDialog {
|
||||
|
||||
data class DialogError(val error: DomainError) : DomainDialog()
|
||||
data class DialogError(val error: DomainError) : DomainDialog
|
||||
|
||||
data class SelectTokenDialog(
|
||||
val items: List<Coins.CheckAddressResponse.Token.Contract>,
|
||||
val networkIdConverter: (String) -> String,
|
||||
val onSelect: (Coins.CheckAddressResponse.Token.Contract) -> Unit,
|
||||
val onClose: VoidCallback = {}
|
||||
) : DomainDialog()
|
||||
) : DomainDialog
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -11,8 +10,30 @@ import com.tangem.blockchain.common.Token
|
|||
// to appropriate parts of module
|
||||
sealed interface DomainWrapped {
|
||||
|
||||
data class TokenWithBlockchain(
|
||||
val token: Token,
|
||||
val blockchain: Blockchain
|
||||
)
|
||||
// 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
|
||||
}
|
||||
|
||||
fun isCustomCurrency(derivationStyle: DerivationStyle?): Boolean {
|
||||
if (derivationPath == null || derivationStyle == null) return false
|
||||
return derivationPath != blockchain.derivationPath(derivationStyle)?.rawPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,13 @@ package com.tangem.domain.common
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.card.WalletData
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.backup.PrimaryCard
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
|
@ -48,6 +52,33 @@ data class ScanResponse(
|
|||
|
||||
fun twinsIsTwinned(): Boolean =
|
||||
card.isTangemTwins() && walletData != null && secondTwinPublicKey != null
|
||||
|
||||
fun hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean {
|
||||
return hasDerivation(blockchain, DerivationPath(rawDerivationPath))
|
||||
}
|
||||
|
||||
fun hasDerivation(blockchain: Blockchain, derivationPath: DerivationPath): Boolean {
|
||||
val isTestnet = card.isTestCard || blockchain.isTestnet()
|
||||
return when {
|
||||
Blockchain.secp256k1Blockchains(isTestnet).contains(blockchain) -> {
|
||||
hasDerivation(EllipticCurve.Secp256k1, derivationPath)
|
||||
}
|
||||
Blockchain.ed25519OnlyBlockchains(isTestnet).contains(blockchain) -> {
|
||||
hasDerivation(EllipticCurve.Ed25519, derivationPath)
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun hasDerivation(curve: EllipticCurve, derivationPath: DerivationPath): Boolean {
|
||||
val foundWallet = card.wallets.firstOrNull { it.curve == curve }
|
||||
?: return false
|
||||
|
||||
val extendedPublicKeysMap = derivedKeys[foundWallet.publicKey.toMapKey()] ?: return false
|
||||
|
||||
val extendedPublicKey = extendedPublicKeysMap[derivationPath]
|
||||
return extendedPublicKey != null
|
||||
}
|
||||
}
|
||||
|
||||
enum class ProductType {
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.domain.common.form.BaseFieldDataConverter
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class CompleteDataType {
|
||||
Blockchain, Token
|
||||
}
|
||||
|
||||
sealed class CompleteData() {
|
||||
|
||||
class CustomBlockchain(
|
||||
val network: Blockchain,
|
||||
val derivationPath: String?
|
||||
) : CompleteData() {
|
||||
|
||||
class Converter : BaseFieldDataConverter<CustomBlockchain>() {
|
||||
override fun getConvertedData(): CustomBlockchain {
|
||||
val network = collectedData[CustomTokenFieldId.Network] as Blockchain
|
||||
val derivationPath = collectedData[CustomTokenFieldId.DerivationPath] as? String
|
||||
return CustomBlockchain(network, derivationPath)
|
||||
}
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomToken(
|
||||
val token: Token,
|
||||
val network: Blockchain,
|
||||
val derivationPath: String?,
|
||||
) : CompleteData() {
|
||||
|
||||
class Converter(val tokenId: String?) : BaseFieldDataConverter<CustomToken>() {
|
||||
|
||||
override fun getConvertedData(): CustomToken {
|
||||
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,
|
||||
collectedData[CustomTokenFieldId.DerivationPath] as? String,
|
||||
)
|
||||
}
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = CustomTokenFieldId.values().toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.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]
|
||||
*/
|
||||
enum class CompleteDataType {
|
||||
Blockchain, Token
|
||||
}
|
||||
|
||||
sealed class CustomCurrency(
|
||||
val network: Blockchain,
|
||||
val derivationPath: DerivationPath?,
|
||||
) {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,13 +2,13 @@ package com.tangem.domain.features.addCustomToken.redux
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
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.AddCustomTokenError
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning
|
||||
import com.tangem.domain.features.addCustomToken.CompleteData
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
|
|
@ -16,9 +16,8 @@ import org.rekotlin.Action
|
|||
*/
|
||||
sealed class AddCustomTokenAction : Action {
|
||||
sealed class Init : AddCustomTokenAction() {
|
||||
data class SetAddedCurrencies(val addedCurrencies: AddedCurrencies) : AddCustomTokenAction()
|
||||
|
||||
data class SetOnAddTokenCallback(val callback: (CompleteData) -> Unit) : AddCustomTokenAction()
|
||||
data class SetAddedCurrencies(val addedCurrencies: List<DomainWrapped.Currency>) : AddCustomTokenAction()
|
||||
data class SetOnAddTokenCallback(val callback: (CustomCurrency) -> Unit) : AddCustomTokenAction()
|
||||
}
|
||||
|
||||
object OnCreate : AddCustomTokenAction() {
|
||||
|
|
@ -36,22 +35,16 @@ sealed class AddCustomTokenAction : Action {
|
|||
data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data<String>) : AddCustomTokenAction()
|
||||
object OnAddCustomTokenClicked : AddCustomTokenAction()
|
||||
|
||||
data class SetFoundTokenId(val id: String?) : AddCustomTokenAction()
|
||||
|
||||
// form fields
|
||||
data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction()
|
||||
object ClearTokenFields : AddCustomTokenAction()
|
||||
|
||||
data class FillTokenFields(
|
||||
val token: Coins.CheckAddressResponse.Token,
|
||||
val contract: Coins.CheckAddressResponse.Token.Contract,
|
||||
) : AddCustomTokenAction()
|
||||
|
||||
sealed class FieldError : AddCustomTokenAction() {
|
||||
data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : FieldError()
|
||||
data class Remove(val id: CustomTokenFieldId) : FieldError()
|
||||
}
|
||||
|
||||
data class SetTokenId(val id: String) : AddCustomTokenAction()
|
||||
|
||||
// warnings
|
||||
sealed class Warning : AddCustomTokenAction() {
|
||||
data class Add(val warnings: Set<AddCustomTokenWarning>) : Warning()
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ package com.tangem.domain.features.addCustomToken.redux
|
|||
import android.webkit.ValueCallback
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.services.Result
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.DomainException
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.TapWorkarounds.derivationStyle
|
||||
import com.tangem.domain.common.extensions.fromNetworkId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.common.form.*
|
||||
|
|
@ -19,7 +23,7 @@ import com.tangem.domain.redux.domainStore
|
|||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
|
|
@ -39,17 +43,6 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
return storeState.copy(addCustomTokensState = newHubState)
|
||||
}
|
||||
|
||||
private val contractAddressValidator: TokenContractAddressValidator
|
||||
get() = hubState.getValidator(ContractAddress)
|
||||
private val nameValidator: TokenNameValidator
|
||||
get() = hubState.getValidator(Name)
|
||||
private val symbolValidator: TokenSymbolValidator
|
||||
get() = hubState.getValidator(Symbol)
|
||||
private val decimalsValidator: TokenDecimalsValidator
|
||||
get() = hubState.getValidator(Decimals)
|
||||
val networkValidator: TokenNetworkValidator
|
||||
get() = hubState.getValidator(Network)
|
||||
|
||||
override suspend fun handleAction(
|
||||
action: Action,
|
||||
storeState: DomainState,
|
||||
|
|
@ -58,105 +51,114 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
if (action !is AddCustomTokenAction) return
|
||||
|
||||
when (action) {
|
||||
is Init.SetAddedCurrencies -> {}
|
||||
is Init.SetOnAddTokenCallback -> {}
|
||||
is OnCreate -> {
|
||||
// hubState.addedCurrencies.guard {
|
||||
// return throwUnAppropriateInitialization("addedTokens")
|
||||
// }
|
||||
hubState.appSavedCurrencies.guard {
|
||||
return throwUnAppropriateInitialization("addedTokens")
|
||||
}
|
||||
}
|
||||
is OnDestroy -> hubScope.cancel()
|
||||
is OnDestroy -> cancelAll()
|
||||
is OnTokenContractAddressChanged -> {
|
||||
val address = action.contractAddress.value
|
||||
when (val error = ContractAddress.validate(address)) {
|
||||
|
||||
when (val error = ContractAddress.validateValue(address)) {
|
||||
null -> {
|
||||
ContractAddress.removeError()
|
||||
dispatchOnMain(unlockTokenFieldsAction())
|
||||
unlockTokenFields()
|
||||
}
|
||||
AddCustomTokenError.FieldIsEmpty -> {
|
||||
ContractAddress.removeError()
|
||||
dispatchOnMain(lockTokenFieldsAction())
|
||||
return
|
||||
}
|
||||
AddCustomTokenError.InvalidContractAddress -> {
|
||||
ContractAddress.addError(error)
|
||||
dispatchOnMain(unlockTokenFieldsAction())
|
||||
unlockTokenFields()
|
||||
return
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
|
||||
if (!action.contractAddress.isUserInput) return
|
||||
|
||||
manageTokenChanges(requestInfoAboutContractAddress(address))
|
||||
manageFoundTokenChanges(requestInfoAboutToken(address))
|
||||
}
|
||||
is OnTokenNameChanged -> {
|
||||
updateAddButton()
|
||||
}
|
||||
is OnTokenSymbolChanged -> {
|
||||
updateAddButton()
|
||||
}
|
||||
is OnTokenDecimalsChanged -> {
|
||||
updateAddButton()
|
||||
}
|
||||
is OnTokenNetworkChanged -> {
|
||||
if (!action.blockchainNetwork.isUserInput) return
|
||||
|
||||
val contractAddress = ContractAddress.getFieldValue<String>()
|
||||
val error = ContractAddress.validate(contractAddress)
|
||||
if (error == null) {
|
||||
manageTokenChanges(requestInfoAboutContractAddress(contractAddress))
|
||||
val error = ContractAddress.validateValue(contractAddress)
|
||||
if (error == null && contractAddress.isNotEmpty()) {
|
||||
// token branch
|
||||
manageFoundTokenChanges(requestInfoAboutToken(contractAddress))
|
||||
} else {
|
||||
|
||||
// blockchain branch
|
||||
val isAlreadyAdded = isBlockchainPersistIntoAppSavedTokensList(
|
||||
selectedNetwork = action.blockchainNetwork.value
|
||||
)
|
||||
updateWarningAlreadyAdded(isAlreadyAdded)
|
||||
updateAddButton()
|
||||
}
|
||||
}
|
||||
is OnTokenNameChanged -> {
|
||||
Name.addOrRemoveError(Name.validate(action.tokenName.value))
|
||||
}
|
||||
is OnTokenSymbolChanged -> {
|
||||
Symbol.addOrRemoveError(Symbol.validate(action.tokenSymbol.value))
|
||||
}
|
||||
is OnTokenDecimalsChanged -> {
|
||||
Decimals.addOrRemoveError(Decimals.validate(action.tokenDecimals.value))
|
||||
}
|
||||
is ClearTokenFields -> {
|
||||
Name.setFieldValue(Field.Data("", false))
|
||||
Symbol.setFieldValue(Field.Data("", false))
|
||||
Decimals.setFieldValue(Field.Data("", false))
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
is FillTokenFields -> {
|
||||
val token = action.token
|
||||
val contract = action.contract
|
||||
val blockchain = Blockchain.fromNetworkId(contract.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(contract.decimalCount.toString(), false))
|
||||
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
is OnTokenDerivationPathChanged -> {
|
||||
val isAlreadyAdded = if (ContractAddress.isFilled()) {
|
||||
// token branch
|
||||
isTokenPersistIntoAppSavedTokensList(
|
||||
selectedDerivation = action.blockchainDerivationPath.value
|
||||
)
|
||||
} else {
|
||||
// blockchain branch
|
||||
isBlockchainPersistIntoAppSavedTokensList(
|
||||
selectedDerivation = action.blockchainDerivationPath.value
|
||||
)
|
||||
}
|
||||
updateWarningAlreadyAdded(isAlreadyAdded)
|
||||
updateAddButton()
|
||||
}
|
||||
is OnAddCustomTokenClicked -> {
|
||||
// if (hubState.allFieldsIsEmpty()) {
|
||||
dispatchOnMain(
|
||||
DomainGlobalAction.ShowDialog(DomainDialog.DialogError(
|
||||
AddCustomTokenError.InvalidDerivationPath
|
||||
)))
|
||||
return
|
||||
// }
|
||||
when {
|
||||
!hubState.customTokensFieldsIsEmpty() && !hubState.networkIsEmpty() -> {
|
||||
hubState.getCompleteData(CompleteDataType.Token)
|
||||
val state = hubState
|
||||
val completeData = when {
|
||||
state.tokensFieldsIsFilled() && state.networkIsSelected() -> {
|
||||
state.gatherUserToken()
|
||||
}
|
||||
!state.tokensFieldsIsFilled() && 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)
|
||||
}
|
||||
// !hubState.customTokensFieldsIsEmpty() && -> {
|
||||
// }
|
||||
}
|
||||
// if (true) {
|
||||
// dispatchOnMain(NavigationAction.PopBackTo())
|
||||
// hubState.onTokenAddCallback?.invoke()
|
||||
// }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestInfoAboutContractAddress(
|
||||
private suspend fun updateWarningAlreadyAdded(isInAppSavedList: Boolean) {
|
||||
if (isInAppSavedList) {
|
||||
AddCustomTokenWarning.TokenAlreadyAdded.add()
|
||||
} else {
|
||||
AddCustomTokenWarning.TokenAlreadyAdded.remove()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestInfoAboutToken(
|
||||
contractAddress: String,
|
||||
): List<Coins.CheckAddressResponse.Token> {
|
||||
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
|
||||
|
|
@ -179,85 +181,184 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
return result
|
||||
}
|
||||
|
||||
private suspend fun manageTokenChanges(foundTokens: List<Coins.CheckAddressResponse.Token>) {
|
||||
val toAddWarnings = mutableSetOf<AddCustomTokenWarning>()
|
||||
val toRemoveWarnings = mutableSetOf<AddCustomTokenWarning>()
|
||||
private suspend fun manageFoundTokenChanges(foundTokens: List<Coins.CheckAddressResponse.Token>) {
|
||||
if (foundTokens.isEmpty()) {
|
||||
// token not found - it's completely custom
|
||||
AddCustomTokenWarning.TokenAlreadyAdded.remove()
|
||||
AddCustomTokenWarning.PotentialScamToken.add()
|
||||
|
||||
dispatchOnMain(SetFoundTokenId(null))
|
||||
clearTokenFields()
|
||||
unlockTokenFields()
|
||||
updateAddButton()
|
||||
return
|
||||
}
|
||||
|
||||
// foundToken - contains all info about the token
|
||||
val foundToken = foundTokens[0]
|
||||
dispatchOnMain(SetFoundTokenId(foundToken.id))
|
||||
when {
|
||||
foundTokens.isEmpty() -> {
|
||||
toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken)
|
||||
toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
dispatchOnMain(ClearTokenFields)
|
||||
dispatchOnMain(unlockTokenFieldsAction())
|
||||
foundToken.contracts.isEmpty() -> {
|
||||
Timber.e("Unexpected state -> throw to FB")
|
||||
}
|
||||
foundToken.contracts.size == 1 -> {
|
||||
// token with single contract address
|
||||
val singleTokenContract = foundToken.contracts[0]
|
||||
fillTokenFields(foundToken, singleTokenContract)
|
||||
|
||||
val isInAppSavedTokens = isTokenPersistIntoAppSavedTokensList()
|
||||
if (isInAppSavedTokens) {
|
||||
lockTokenFields()
|
||||
lockAddButton()
|
||||
AddCustomTokenWarning.PotentialScamToken.replace(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
} else {
|
||||
// not in the saved tokens list
|
||||
if (singleTokenContract.active) {
|
||||
lockTokenFields()
|
||||
unlockAddButton()
|
||||
if (hubState.derivationPathIsSelected()) {
|
||||
AddCustomTokenWarning.PotentialScamToken.add()
|
||||
} else {
|
||||
AddCustomTokenWarning.TokenAlreadyAdded.remove()
|
||||
AddCustomTokenWarning.PotentialScamToken.remove()
|
||||
}
|
||||
} else {
|
||||
unlockAddButton()
|
||||
AddCustomTokenWarning.PotentialScamToken.add()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val token = foundTokens[0]
|
||||
val contracts = token.contracts
|
||||
when {
|
||||
contracts.isEmpty() -> {
|
||||
// TODO: refactoring:
|
||||
Timber.e("Unexpected state -> throw to FB")
|
||||
}
|
||||
contracts.size == 1 -> {
|
||||
val contract = contracts[0]
|
||||
val isPersistIntoTheAppAddedTokenList = isPersistIntoTheAppAddedTokenList(token, contract)
|
||||
AddCustomTokenWarning.PotentialScamToken.replace(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
|
||||
if (isPersistIntoTheAppAddedTokenList) {
|
||||
toAddWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken)
|
||||
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false)))
|
||||
dispatchOnMain(lockTokenFieldsAction())
|
||||
} else {
|
||||
toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true)))
|
||||
|
||||
val isStandardDerivation = true
|
||||
val tokenContract = token.contracts[0]
|
||||
if (tokenContract.active && isStandardDerivation) {
|
||||
toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken)
|
||||
dispatchOnMain(FillTokenFields(token, contract))
|
||||
dispatchOnMain(lockTokenFieldsAction())
|
||||
} else {
|
||||
toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken)
|
||||
dispatchOnMain(ClearTokenFields)
|
||||
dispatchOnMain(unlockTokenFieldsAction())
|
||||
}
|
||||
val dialog = DomainDialog.SelectTokenDialog(
|
||||
items = foundToken.contracts,
|
||||
networkIdConverter = { networkId ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
throw DomainException.SelectTokeNetworkException(networkId)
|
||||
}
|
||||
hubState.blockchainToName(blockchain) ?: ""
|
||||
},
|
||||
onSelect = { selectedContract ->
|
||||
hubScope.launch {
|
||||
// find how to connect to the upper coroutineContext and dispatch through them
|
||||
fillTokenFields(foundToken, selectedContract)
|
||||
lockTokenFields()
|
||||
unlockAddButton()
|
||||
}
|
||||
},
|
||||
)
|
||||
dispatchOnMain(DomainGlobalAction.ShowDialog(dialog))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun replaceWarnings(
|
||||
warningsAdd: MutableSet<AddCustomTokenWarning> = mutableSetOf(),
|
||||
warningsRemove: MutableSet<AddCustomTokenWarning> = mutableSetOf(),
|
||||
) {
|
||||
if (warningsAdd.isNotEmpty() || warningsRemove.isNotEmpty()) {
|
||||
dispatchOnMain(Warning.Replace(warningsRemove.toSet(), warningsAdd.toSet()))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateAddButton() {
|
||||
val state = hubState
|
||||
if (state.warnings.contains(AddCustomTokenWarning.TokenAlreadyAdded)) {
|
||||
lockAddButton()
|
||||
return
|
||||
}
|
||||
when {
|
||||
// token
|
||||
state.tokensOneFieldsIsFilled() -> {
|
||||
lockAddButton()
|
||||
}
|
||||
// token
|
||||
state.tokensFieldsIsFilled() && state.networkIsSelected() -> {
|
||||
unlockAddButton()
|
||||
}
|
||||
// blockchain
|
||||
else -> {
|
||||
if (state.networkIsSelected()) {
|
||||
val alreadyAdded = isBlockchainPersistIntoAppSavedTokensList()
|
||||
if (alreadyAdded) {
|
||||
lockAddButton()
|
||||
} else {
|
||||
unlockAddButton()
|
||||
}
|
||||
else -> {
|
||||
val dialog = DomainDialog.SelectTokenDialog(
|
||||
items = contracts,
|
||||
networkIdConverter = { networkId ->
|
||||
val blockchain = Blockchain.fromNetworkId(networkId)
|
||||
if (blockchain == null || blockchain == Blockchain.Unknown) {
|
||||
throw DomainException.SelectTokeNetworkException(networkId)
|
||||
}
|
||||
hubState.convertBlockchainName(blockchain, "")
|
||||
},
|
||||
onSelect = { selectedContract ->
|
||||
hubScope.launch {
|
||||
// find how to connect to the upper coroutineContext and dispatch through them
|
||||
dispatchOnMain(FillTokenFields(token, selectedContract))
|
||||
dispatchOnMain(lockTokenFieldsAction())
|
||||
}
|
||||
},
|
||||
)
|
||||
dispatchOnMain(DomainGlobalAction.ShowDialog(dialog))
|
||||
} else {
|
||||
lockAddButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun lockAddButton() {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false)))
|
||||
}
|
||||
|
||||
private suspend fun unlockAddButton() {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true)))
|
||||
}
|
||||
|
||||
/**
|
||||
* These are helper functions.
|
||||
*/
|
||||
private fun isTokenPersistIntoAppSavedTokensList(
|
||||
tokenId: String? = hubState.tokenId,
|
||||
tokenContractAddress: String = ContractAddress.getFieldValue(),
|
||||
tokenNetworkId: String = Network.getFieldValue<Blockchain>().toNetworkId(),
|
||||
selectedDerivation: Blockchain = DerivationPath.getFieldValue()
|
||||
): Boolean {
|
||||
val savedCurrencies = hubState.appSavedCurrencies ?: return false
|
||||
|
||||
val derivationPath = getDerivationPathFromSelectedBlockchain(selectedDerivation)
|
||||
savedCurrencies.forEach { wrappedCurrency ->
|
||||
when (wrappedCurrency) {
|
||||
is DomainWrapped.Currency.Blockchain -> {}
|
||||
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
|
||||
if (sameId && sameAddress && sameBlockchain && sameDerivationPath) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (toAddWarnings.isNotEmpty() || toRemoveWarnings.isNotEmpty()) {
|
||||
dispatchOnMain(Warning.Replace(toRemoveWarnings.toSet(), toAddWarnings.toSet()))
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun isPersistIntoTheAppAddedTokenList(
|
||||
token: Coins.CheckAddressResponse.Token,
|
||||
contract: Coins.CheckAddressResponse.Token.Contract
|
||||
): Boolean = false
|
||||
private fun isBlockchainPersistIntoAppSavedTokensList(
|
||||
selectedNetwork: Blockchain = Network.getFieldValue(),
|
||||
selectedDerivation: Blockchain = DerivationPath.getFieldValue()
|
||||
): Boolean {
|
||||
val state = hubState
|
||||
val savedCurrencies = state.appSavedCurrencies ?: return false
|
||||
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun getDerivationPathFromSelectedBlockchain(
|
||||
selectedDerivationBlockchain: Blockchain
|
||||
): com.tangem.common.hdWallet.DerivationPath? = AddCustomTokenState.getDerivationPath(
|
||||
mainNetwork = Network.getFieldValue(),
|
||||
derivationNetwork = selectedDerivationBlockchain,
|
||||
derivationStyle = hubState.cardDerivationStyle
|
||||
)
|
||||
|
||||
private suspend fun CustomTokenFieldId.addError(error: AddCustomTokenError) {
|
||||
dispatchOnMain(FieldError.Add(this, error))
|
||||
|
|
@ -267,13 +368,6 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
dispatchOnMain(FieldError.Remove(this))
|
||||
}
|
||||
|
||||
private suspend fun CustomTokenFieldId.addOrRemoveError(error: AddCustomTokenError?) {
|
||||
when (error) {
|
||||
null -> removeError()
|
||||
else -> addError(error)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> CustomTokenFieldId.getField(): T {
|
||||
val state = hubState
|
||||
val value = when (this) {
|
||||
|
|
@ -310,66 +404,126 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
}
|
||||
}
|
||||
|
||||
private fun CustomTokenFieldId.validate(value: Any): AddCustomTokenError? {
|
||||
private fun CustomTokenFieldId.validateValue(value: Any): AddCustomTokenError? {
|
||||
val state = hubState
|
||||
val contractAddressValidator: TokenContractAddressValidator = state.getValidator(ContractAddress)
|
||||
val nameValidator: TokenNameValidator = state.getValidator(Name)
|
||||
val symbolValidator: TokenSymbolValidator = state.getValidator(Symbol)
|
||||
val decimalsValidator: TokenDecimalsValidator = state.getValidator(Decimals)
|
||||
val networkValidator: TokenNetworkValidator = state.getValidator(Network)
|
||||
return when (this) {
|
||||
ContractAddress -> contractAddressValidator.validate(value as String)
|
||||
Network -> networkValidator.validate(value as Blockchain)
|
||||
Network, DerivationPath -> networkValidator.validate(value as Blockchain)
|
||||
Name -> nameValidator.validate(value as String)
|
||||
Symbol -> symbolValidator.validate(value as String)
|
||||
Decimals -> decimalsValidator.validate(value as String)
|
||||
DerivationPath -> networkValidator.validate(value as Blockchain)
|
||||
}
|
||||
}
|
||||
|
||||
private fun lockTokenFieldsAction(): Action {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The field is being validated.
|
||||
* If there is an error, then it adds it to the field.
|
||||
*/
|
||||
private suspend fun CustomTokenFieldId.validateAndUpdateError(value: Any): AddCustomTokenError? {
|
||||
val error = this.validateValue(value)
|
||||
when (error) {
|
||||
null -> this.removeError()
|
||||
else -> this.addError(error)
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
private suspend fun fillTokenFields(
|
||||
token: Coins.CheckAddressResponse.Token,
|
||||
contract: Coins.CheckAddressResponse.Token.Contract,
|
||||
) {
|
||||
val blockchain = Blockchain.fromNetworkId(contract.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(contract.decimalCount.toString(), false))
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
private suspend fun clearTokenFields() {
|
||||
Name.setFieldValue(Field.Data("", false))
|
||||
Symbol.setFieldValue(Field.Data("", false))
|
||||
Decimals.setFieldValue(Field.Data("", false))
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
|
||||
private suspend fun lockTokenFields() {
|
||||
val state = hubState
|
||||
return Screen.UpdateTokenFields(listOf(
|
||||
val action = Screen.UpdateTokenFields(listOf(
|
||||
Network to state.screenState.network.copy(isEnabled = false),
|
||||
Name to state.screenState.name.copy(isEnabled = false),
|
||||
Symbol to state.screenState.symbol.copy(isEnabled = false),
|
||||
Decimals to state.screenState.decimals.copy(isEnabled = false),
|
||||
))
|
||||
dispatchOnMain(action)
|
||||
}
|
||||
|
||||
private fun unlockTokenFieldsAction(): Action {
|
||||
private suspend fun unlockTokenFields() {
|
||||
val state = hubState
|
||||
return Screen.UpdateTokenFields(listOf(
|
||||
val action = Screen.UpdateTokenFields(listOf(
|
||||
Network to state.screenState.network.copy(isEnabled = true),
|
||||
Name to state.screenState.name.copy(isEnabled = true),
|
||||
Symbol to state.screenState.symbol.copy(isEnabled = true),
|
||||
Decimals to state.screenState.decimals.copy(isEnabled = true),
|
||||
))
|
||||
dispatchOnMain(action)
|
||||
}
|
||||
|
||||
private suspend fun toggleAddButtonAction(enable: Boolean) = when (enable) {
|
||||
true -> unlockAddButtonAction()
|
||||
else -> lockAddButtonAction()
|
||||
private suspend fun AddCustomTokenWarning.add() {
|
||||
dispatchOnMain(Warning.Add(setOf(this)))
|
||||
}
|
||||
|
||||
private suspend fun lockAddButtonAction() {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false)))
|
||||
private suspend fun AddCustomTokenWarning.remove() {
|
||||
dispatchOnMain(Warning.Remove(setOf(this)))
|
||||
}
|
||||
|
||||
private suspend fun unlockAddButtonAction() {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true)))
|
||||
private suspend fun AddCustomTokenWarning.replace(to: AddCustomTokenWarning) {
|
||||
dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
|
||||
}
|
||||
|
||||
// private suspend fun AddCustomTokenWarning.replace(replace: Boolean, to: AddCustomTokenWarning) {
|
||||
// if (replace) dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
|
||||
// }
|
||||
|
||||
|
||||
override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState {
|
||||
return when (action) {
|
||||
is Init.SetAddedCurrencies -> {
|
||||
state.copy(addedCurrencies = action.addedCurrencies)
|
||||
state.copy(appSavedCurrencies = action.addedCurrencies)
|
||||
}
|
||||
is Init.SetOnAddTokenCallback -> {
|
||||
state.copy(onTokenAddCallback = action.callback)
|
||||
}
|
||||
is OnCreate -> {
|
||||
// val card = requireNotNull(globalState.scanResponse?.card)
|
||||
val card = requireNotNull(globalState.scanResponse?.card)
|
||||
val tangemTechServiceManager = TangemTechServiceManager(TangemTechService())
|
||||
// tangemTechServiceManager.attachAuthKey(card.cardPublicKey.toHexString())
|
||||
tangemTechServiceManager.attachAuthKey(card.cardPublicKey.toHexString())
|
||||
|
||||
var derivationPathState = state.screenState.derivationPath
|
||||
derivationPathState = when (card.derivationStyle) {
|
||||
DerivationStyle.LEGACY -> derivationPathState.copy(isVisible = true)
|
||||
null, DerivationStyle.NEW -> derivationPathState.copy(isVisible = false)
|
||||
}
|
||||
state.copy(
|
||||
// derivationStyle = card.derivationStyle,
|
||||
derivationStyle = DerivationStyle.LEGACY,
|
||||
tangemTechServiceManager = tangemTechServiceManager
|
||||
cardDerivationStyle = card.derivationStyle,
|
||||
tangemTechServiceManager = tangemTechServiceManager,
|
||||
screenState = state.screenState.copy(derivationPath = derivationPathState)
|
||||
)
|
||||
}
|
||||
is OnDestroy -> state.reset()
|
||||
|
|
@ -414,7 +568,7 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
|
|||
val newMap = state.formErrors.toMutableMap().apply { remove(action.id) }
|
||||
state.copy(formErrors = newMap)
|
||||
}
|
||||
is SetTokenId -> {
|
||||
is SetFoundTokenId -> {
|
||||
state.copy(tokenId = action.id)
|
||||
}
|
||||
is Warning.Add -> {
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@ package com.tangem.domain.features.addCustomToken.redux
|
|||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.DerivationStyle
|
||||
import com.tangem.domain.DomainWrapped
|
||||
import com.tangem.domain.common.form.*
|
||||
import com.tangem.domain.features.addCustomToken.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class AddCustomTokenState(
|
||||
val addedCurrencies: AddedCurrencies? = null,
|
||||
val onTokenAddCallback: ((CompleteData) -> Unit)? = null,
|
||||
val derivationStyle: DerivationStyle? = null,
|
||||
val appSavedCurrencies: List<DomainWrapped.Currency>? = null,
|
||||
val onTokenAddCallback: ((CustomCurrency) -> Unit)? = null,
|
||||
val cardDerivationStyle: DerivationStyle? = null,
|
||||
val form: Form = Form(createFormFields()),
|
||||
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<out Any>> = createFormValidators(),
|
||||
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
|
||||
|
|
@ -28,30 +29,73 @@ data class AddCustomTokenState(
|
|||
|
||||
fun hasError(id: FieldId): Boolean = formErrors[id] != null
|
||||
|
||||
fun getCompleteData(type: CompleteDataType): CompleteData = when (type) {
|
||||
CompleteDataType.Token -> getToken()
|
||||
CompleteDataType.Blockchain -> getBlockchain()
|
||||
}
|
||||
|
||||
inline fun <reified T> visitDataConverter(converter: FieldDataConverter<T>): T {
|
||||
form.visitDataConverter(converter)
|
||||
return converter.getConvertedData()
|
||||
}
|
||||
|
||||
fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) {
|
||||
Blockchain.Unknown -> unknown
|
||||
else -> blockchain.fullName
|
||||
fun blockchainToName(blockchain: Blockchain, isDerivationPath: Boolean = false): String? {
|
||||
return when {
|
||||
isDerivationPath -> blockchain.derivationPath(cardDerivationStyle)?.rawPath
|
||||
else -> {
|
||||
when (blockchain) {
|
||||
Blockchain.Unknown -> null
|
||||
else -> blockchain.fullName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun convertDerivationPathLabel(blockchain: Blockchain, unknown: String): String {
|
||||
return blockchain.derivationPath(derivationStyle)?.rawPath ?: unknown
|
||||
// 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 tokensOneFieldsIsFilled(): 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.size == 1
|
||||
}
|
||||
|
||||
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 gatherUserToken(): CustomCurrency.CustomToken? = try {
|
||||
getToken()
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
fun gatherBlockchain(): CustomCurrency.CustomBlockchain? = try {
|
||||
getBlockchain()
|
||||
} catch (ex: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
fun reset(): AddCustomTokenState {
|
||||
return this.copy(
|
||||
addedCurrencies = null,
|
||||
appSavedCurrencies = null,
|
||||
onTokenAddCallback = null,
|
||||
derivationStyle = null,
|
||||
cardDerivationStyle = null,
|
||||
form = Form(createFormFields()),
|
||||
formErrors = emptyMap(),
|
||||
tokenId = null,
|
||||
|
|
@ -61,40 +105,33 @@ data class AddCustomTokenState(
|
|||
)
|
||||
}
|
||||
|
||||
fun networkIsEmpty(): Boolean {
|
||||
val network = getField<TokenBlockchainField>(Network)
|
||||
return network.data.value != Blockchain.Unknown
|
||||
}
|
||||
|
||||
fun customTokensFieldsIsEmpty(): Boolean {
|
||||
val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals)
|
||||
val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) }
|
||||
val validator = StringIsEmptyValidator()
|
||||
// val errors = mutableMapOf<>()
|
||||
fieldsToCheck.forEach { field ->
|
||||
val error = validator.validate(field.data.value?.toString())
|
||||
if (error != null) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun allFieldsIsEmpty(): Boolean {
|
||||
return networkIsEmpty() && customTokensFieldsIsEmpty()
|
||||
}
|
||||
|
||||
private fun getToken(): CompleteData.CustomToken {
|
||||
return CompleteData.CustomToken.Converter(tokenId)
|
||||
private fun getToken(): CustomCurrency.CustomToken {
|
||||
return CustomCurrency.CustomToken.Converter(tokenId, cardDerivationStyle)
|
||||
.apply { visitDataConverter(this) }
|
||||
.getConvertedData()
|
||||
}
|
||||
|
||||
private fun getBlockchain(): CompleteData.CustomBlockchain {
|
||||
return CompleteData.CustomBlockchain.Converter()
|
||||
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
|
||||
*/
|
||||
fun getDerivationPath(
|
||||
mainNetwork: Blockchain,
|
||||
derivationNetwork: Blockchain,
|
||||
derivationStyle: DerivationStyle?
|
||||
): com.tangem.common.hdWallet.DerivationPath? = when (derivationNetwork) {
|
||||
Blockchain.Unknown -> mainNetwork
|
||||
else -> derivationNetwork
|
||||
}.derivationPath(derivationStyle)
|
||||
|
||||
private fun createFormFields(): List<DataField<*>> {
|
||||
return listOf(
|
||||
TokenField(ContractAddress),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.DomainWrapped
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -27,9 +24,4 @@ sealed class ViewStates {
|
|||
data class AddButton(
|
||||
val isEnabled: Boolean = true
|
||||
) : ViewStates()
|
||||
}
|
||||
|
||||
data class AddedCurrencies(
|
||||
val addedTokens: List<DomainWrapped.TokenWithBlockchain>,
|
||||
val addedBlockchains: List<Blockchain>
|
||||
)
|
||||
}
|
||||
|
|
@ -97,6 +97,10 @@ internal abstract class BaseStoreHub<State>(
|
|||
}
|
||||
}
|
||||
|
||||
protected fun cancelAll() {
|
||||
actionsAndJobs.forEach { (_, job) -> job.cancel() }
|
||||
}
|
||||
|
||||
protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback<Action>)
|
||||
protected abstract fun reduceAction(action: Action, state: State): State
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.redux.global
|
||||
|
||||
import com.tangem.domain.DomainStateDialog
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -10,5 +10,5 @@ import org.rekotlin.Action
|
|||
//TODO: refactoring: is alias for the GlobalAction
|
||||
sealed class DomainGlobalAction : Action {
|
||||
data class SetScanResponse(val scanResponse: ScanResponse?) : DomainGlobalAction()
|
||||
data class ShowDialog(val stateDialog: DomainStateDialog?) : DomainGlobalAction()
|
||||
data class ShowDialog(val stateDialog: DomainDialog?) : DomainGlobalAction()
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.domain.redux.global
|
||||
|
||||
import com.tangem.domain.DomainStateDialog
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
|
||||
/**
|
||||
|
|
@ -9,5 +9,5 @@ import com.tangem.domain.common.ScanResponse
|
|||
//TODO: refactoring: is alias for the GlobalState
|
||||
data class DomainGlobalState(
|
||||
val scanResponse: ScanResponse? = null,
|
||||
val dialog: DomainStateDialog? = null,
|
||||
val dialog: DomainDialog? = null,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue