Updated on 2026-08-14
This commit is contained in:
parent
ee8c426ca9
commit
a38c36ce2d
28 changed files with 940 additions and 464 deletions
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.common.redux
|
||||
|
||||
import com.tangem.domain.restore.DomainState
|
||||
import com.tangem.domain.restore.domainStore
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.tap.common.redux.global.GlobalMiddleware
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.common.redux.navigation.NavigationState
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.features.global.redux.DomainGlobalAction
|
||||
import com.tangem.domain.restore.domainStore
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package com.tangem.domain.common
|
||||
package com.tangem.domain
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* @property code describes what feature is the error coming from
|
||||
* @property message the error description
|
||||
* @property data any data that can help in the part where this error is being handled
|
||||
*/
|
||||
interface DomainError {
|
||||
val code: Int
|
||||
|
|
@ -9,7 +12,7 @@ interface DomainError {
|
|||
val data: Any?
|
||||
}
|
||||
|
||||
open class AnyError(
|
||||
open class AnError(
|
||||
override val code: Int,
|
||||
override val message: String,
|
||||
override val data: Any? = null,
|
||||
|
|
@ -21,4 +24,6 @@ interface ErrorConverter<T> {
|
|||
|
||||
interface Validator<Data, Error> {
|
||||
fun validate(data: Data? = null): Error?
|
||||
}
|
||||
}
|
||||
|
||||
const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100
|
||||
19
domain/src/main/java/com/tangem/domain/DomainStateDialog.kt
Normal file
19
domain/src/main/java/com/tangem/domain/DomainStateDialog.kt
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.domain
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface DomainStateDialog
|
||||
|
||||
sealed class DomainDialog : DomainStateDialog {
|
||||
|
||||
data class SelectTokenDialog(
|
||||
val items: List<Coins.CheckAddressResponse.Token.Contract>,
|
||||
val itemNameConverter: (Coins.CheckAddressResponse.Token.Contract) -> String,
|
||||
val onSelect: (Coins.CheckAddressResponse.Token.Contract) -> Unit,
|
||||
val onClose: VoidCallback = {}
|
||||
) : DomainDialog()
|
||||
}
|
||||
|
|
@ -41,12 +41,6 @@ object TapWorkarounds {
|
|||
|
||||
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId]
|
||||
|
||||
val Card.derivationType: DerivationType
|
||||
get() = when {
|
||||
tangemWalletBatchesWithStandardDerivationType.contains(batchId) -> DerivationType.Standard
|
||||
else -> DerivationType.Metamask
|
||||
}
|
||||
|
||||
val Card.isStart2Coin: Boolean
|
||||
get() = isStart2CoinIssuer(issuer.name)
|
||||
|
||||
|
|
@ -71,8 +65,4 @@ object TapWorkarounds {
|
|||
fun isStart2CoinIssuer(cardIssuer: String?): Boolean {
|
||||
return cardIssuer?.toLowerCase(Locale.US) == START_2_COIN_ISSUER
|
||||
}
|
||||
}
|
||||
|
||||
enum class DerivationType {
|
||||
Metamask, Standard
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
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> {
|
||||
protected 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(),
|
||||
protected val jsonConverter: MoshiJsonConverter
|
||||
) : BaseFieldDataConverter<String>() {
|
||||
|
||||
override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ")
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = fieldsToConvert
|
||||
}
|
||||
|
|
@ -2,9 +2,7 @@ package com.tangem.domain.common.form
|
|||
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.hdWallet.HDWalletError
|
||||
import com.tangem.domain.common.Validator
|
||||
import com.tangem.domain.Validator
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenError
|
||||
|
||||
/**
|
||||
|
|
@ -27,18 +25,16 @@ class StringIsNotEmptyValidator : CustomTokenValidator<String>() {
|
|||
}
|
||||
|
||||
class TokenContractAddressValidator : CustomTokenValidator<String>() {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
if (data == null || data.isEmpty()) return null
|
||||
|
||||
override fun validate(data: String?): AddCustomTokenError? = when {
|
||||
// data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty
|
||||
else -> EthAddressValidator().validate(data)
|
||||
}
|
||||
|
||||
private class EthAddressValidator : CustomTokenValidator<String>() {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
val isValid = EthereumAddressService().validate(data ?: "")
|
||||
return if (isValid) null else AddCustomTokenError.InvalidContractAddress
|
||||
return if (EthereumAddressService().validate(data)) {
|
||||
null
|
||||
} else {
|
||||
AddCustomTokenError.InvalidContractAddress
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TokenNetworkValidator : CustomTokenValidator<Blockchain>() {
|
||||
|
|
@ -48,16 +44,13 @@ class TokenNetworkValidator : CustomTokenValidator<Blockchain>() {
|
|||
}
|
||||
}
|
||||
|
||||
class DerivationPathValidator : CustomTokenValidator<String>() {
|
||||
override fun validate(data: String?): AddCustomTokenError? = when {
|
||||
data == null || data.isEmpty() -> null
|
||||
else -> {
|
||||
try {
|
||||
DerivationPath(data)
|
||||
null
|
||||
} catch (ex: HDWalletError) {
|
||||
AddCustomTokenError.InvalidDerivationPath
|
||||
}
|
||||
class TokenDecimalsValidator : CustomTokenValidator<String>() {
|
||||
override fun validate(data: String?): AddCustomTokenError? {
|
||||
val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty
|
||||
|
||||
return when {
|
||||
decimal > 30 -> AddCustomTokenError.InvalidDecimalsCount
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.domain.common.form
|
||||
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -13,63 +11,38 @@ class Form(
|
|||
fun getData(id: FieldId): Pair<FieldId, *>? = getField(id)?.getData()
|
||||
|
||||
// convert this form data whatever you want
|
||||
fun getData(converter: FieldDataConverter<*>) {
|
||||
fun visitDataConverter(converter: FieldDataConverter<*>) {
|
||||
fieldList.forEach { it.visitDataConverter(converter) }
|
||||
}
|
||||
}
|
||||
|
||||
interface FieldId
|
||||
|
||||
interface Field<Data> {
|
||||
interface Field<T> {
|
||||
val id: FieldId
|
||||
var value: Data
|
||||
val isEnabled: Boolean
|
||||
val isVisible: Boolean
|
||||
var data: Data<T>
|
||||
|
||||
data class Data<Data>(
|
||||
val value: Data,
|
||||
val isUserInput: Boolean = true
|
||||
)
|
||||
}
|
||||
|
||||
abstract class BaseDataField<Data>(
|
||||
override val id: FieldId,
|
||||
override var value: Data
|
||||
) : DataField<Data> {
|
||||
typealias FieldData = Pair<FieldId, Field.Data<*>>
|
||||
|
||||
override fun getData(): Pair<FieldId, Data> = id to value
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
interface FieldDataConverter<Result> : DataConverterVisitor<Pair<FieldId, Any?>, Result>
|
||||
|
||||
abstract class BaseFieldDataConverter<Data>() : FieldDataConverter<Data> {
|
||||
protected val collectIds: List<FieldId> = getIdToCollect()
|
||||
|
||||
protected val collectedData: MutableMap<FieldId, Any?> = mutableMapOf()
|
||||
|
||||
override fun visit(data: Pair<FieldId, Any?>?) {
|
||||
val id = data?.first ?: return
|
||||
|
||||
if (collectIds.contains(id)) {
|
||||
collectedData[id] = data.second
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getIdToCollect(): List<FieldId>
|
||||
}
|
||||
|
||||
abstract class FieldToJsonConverter(
|
||||
protected val jsonConverter: MoshiJsonConverter
|
||||
) : BaseFieldDataConverter<String>() {
|
||||
|
||||
override fun getConvertedData(): String = jsonConverter.toJson(collectedData)
|
||||
}
|
||||
|
||||
interface DataConverterVisitor<Visitor, Result> {
|
||||
fun visit(data: Visitor?)
|
||||
fun getConvertedData(): Result
|
||||
}
|
||||
|
||||
interface DataField<Data> : Field<Data> {
|
||||
fun getData(): Pair<FieldId, Data>
|
||||
fun visitDataConverter(dataConverter: FieldDataConverter<*>)
|
||||
}
|
||||
|
|
@ -1,21 +1,20 @@
|
|||
package com.tangem.domain.common
|
||||
package com.tangem.domain.common.util
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class ValueDebouncer<T>(
|
||||
var value: T?,
|
||||
private val debounce: Long = 400,
|
||||
private val onValueChanged: (T?) -> Unit
|
||||
) {
|
||||
|
||||
private var value: T? = null
|
||||
private val debounceScope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
private val flow = MutableStateFlow(value)
|
||||
|
||||
|
|
@ -28,7 +27,7 @@ class ValueDebouncer<T>(
|
|||
flow.filter { if (value == null) true else value != it }
|
||||
.debounce(debounce)
|
||||
.onEach {
|
||||
Timber.d("onValueChanged: $it")
|
||||
value = it
|
||||
onValueChanged(it)
|
||||
}
|
||||
.collect()
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.common.json.MoshiJsonConverter
|
||||
import com.tangem.domain.common.form.FieldToJsonConverter
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.state.StatePrinter
|
||||
import org.rekotlin.Action
|
||||
|
||||
class AddCustomTokenStatePrinter : StatePrinter<AddCustomTokenAction, AddCustomTokenState> {
|
||||
private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE
|
||||
private var builder: StringBuilder = StringBuilder()
|
||||
|
||||
override fun print(action: Action, domainState: DomainState): String? {
|
||||
val action = (action as? AddCustomTokenAction) ?: return null
|
||||
val state = domainState.addCustomTokensState
|
||||
|
||||
val fieldConverter = FieldToJsonConverter(listOf(
|
||||
CustomTokenFieldId.ContractAddress,
|
||||
CustomTokenFieldId.Network,
|
||||
CustomTokenFieldId.Name,
|
||||
CustomTokenFieldId.Symbol,
|
||||
CustomTokenFieldId.Decimals,
|
||||
CustomTokenFieldId.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
|
||||
}
|
||||
|
||||
override fun getStateObject(domainState: DomainState): AddCustomTokenState = domainState.addCustomTokensState
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.form.BaseFieldDataConverter
|
||||
import com.tangem.domain.common.form.FieldDataConverter
|
||||
import com.tangem.domain.common.form.FieldId
|
||||
import com.tangem.domain.features.addCustomToken.redux.CompleteDataType
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class CompleteData() {
|
||||
|
||||
companion object {
|
||||
fun createDataConverter(completeDataType: CompleteDataType): FieldDataConverter<out CompleteData> =
|
||||
when (completeDataType) {
|
||||
CompleteDataType.Blockchain -> CustomBlockchain.Converter()
|
||||
CompleteDataType.Token -> CustomToken.Converter()
|
||||
}
|
||||
}
|
||||
|
||||
class CustomBlockchain(
|
||||
val selectedNetwork: Blockchain,
|
||||
val derivationPath: String?
|
||||
) : CompleteData() {
|
||||
|
||||
class Converter : BaseFieldDataConverter<CustomBlockchain>() {
|
||||
override fun getConvertedData(): CustomBlockchain = CustomBlockchain(
|
||||
collectedData[CustomTokenFieldId.Network] as Blockchain,
|
||||
collectedData[CustomTokenFieldId.DerivationPath] as? String,
|
||||
)
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomToken(
|
||||
val contractAddress: String,
|
||||
val selectedNetwork: Blockchain,
|
||||
val name: String,
|
||||
val tokenSymbol: String,
|
||||
val decimals: Int,
|
||||
val derivationPath: String?,
|
||||
) : CompleteData() {
|
||||
|
||||
class Converter : BaseFieldDataConverter<CustomToken>() {
|
||||
override fun getConvertedData(): CustomToken = CustomToken(
|
||||
collectedData[CustomTokenFieldId.ContractAddress] as String,
|
||||
collectedData[CustomTokenFieldId.Network] as Blockchain,
|
||||
collectedData[CustomTokenFieldId.Name] as String,
|
||||
collectedData[CustomTokenFieldId.Symbol] as String,
|
||||
collectedData[CustomTokenFieldId.Decimals] as Int,
|
||||
collectedData[CustomTokenFieldId.DerivationPath] as? String,
|
||||
)
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = CustomTokenFieldId.values().toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,21 @@
|
|||
package com.tangem.domain.features.addCustomToken
|
||||
|
||||
import com.tangem.domain.common.AnyError
|
||||
import com.tangem.domain.AnError
|
||||
import com.tangem.domain.ERROR_CODE_ADD_CUSTOM_TOKEN
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class AddCustomTokenWarning : AnyError(0, "Add custom token - warning") {
|
||||
object PotentialScamToken : AddCustomTokenWarning()
|
||||
object TokenAlreadyAdded : AddCustomTokenWarning()
|
||||
}
|
||||
|
||||
sealed class AddCustomTokenError : AnyError(1, "Add custom token - error") {
|
||||
object NetworkIsNotSelected : AddCustomTokenError()
|
||||
object InvalidContractAddress : AddCustomTokenError()
|
||||
sealed class AddCustomTokenError : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - error") {
|
||||
object FieldIsEmpty : AddCustomTokenError()
|
||||
object FieldIsNotEmpty : AddCustomTokenError()
|
||||
object InvalidContractAddress : AddCustomTokenError()
|
||||
object NetworkIsNotSelected : AddCustomTokenError()
|
||||
object InvalidDecimalsCount : AddCustomTokenError()
|
||||
object InvalidDerivationPath : AddCustomTokenError()
|
||||
}
|
||||
|
||||
sealed class AddCustomTokenWarning : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - warning") {
|
||||
object PotentialScamToken : AddCustomTokenWarning()
|
||||
object TokenAlreadyAdded : AddCustomTokenWarning()
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ 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
|
||||
|
||||
/**
|
||||
|
|
@ -16,21 +17,16 @@ enum class CustomTokenFieldId : FieldId {
|
|||
DerivationPath,
|
||||
}
|
||||
|
||||
data class TokenField(
|
||||
override val id: FieldId,
|
||||
) : BaseDataField<String>(id, Field.Data(""))
|
||||
|
||||
data class TokenNetworkField(
|
||||
override val id: FieldId,
|
||||
val itemList: List<Blockchain>,
|
||||
override val isEnabled: Boolean = true,
|
||||
override val isVisible: Boolean = true,
|
||||
) : BaseDataField<Blockchain>(id, Blockchain.Unknown)
|
||||
|
||||
data class TokenField(
|
||||
override val id: FieldId,
|
||||
override val isEnabled: Boolean = true,
|
||||
override val isVisible: Boolean = true,
|
||||
) : BaseDataField<String>(id, "")
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown))
|
||||
|
||||
data class TokenDerivationPathField(
|
||||
override val id: FieldId,
|
||||
override val isEnabled: Boolean = true,
|
||||
override val isVisible: Boolean = true,
|
||||
) : BaseDataField<String>(id, "")
|
||||
val itemList: List<Blockchain>,
|
||||
) : BaseDataField<Blockchain>(id, Field.Data(Blockchain.Unknown))
|
||||
|
|
@ -1,34 +1,33 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
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.CustomTokenFieldId
|
||||
import com.tangem.network.api.tangemTech.CoinsCheckAddressResponse
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class AddCustomTokenAction : Action {
|
||||
// initializing actions
|
||||
data class SetTangemTechAuthHeader(val cardPublicKeyHex: String) : AddCustomTokenAction()
|
||||
|
||||
|
||||
// from user, ui
|
||||
object OnBackPressed : AddCustomTokenAction()
|
||||
data class OnTokenContractAddressChanged(val value: String) : AddCustomTokenAction()
|
||||
data class OnTokenNetworkChanged(val value: Blockchain) : AddCustomTokenAction()
|
||||
data class OnTokenDerivationPathChanged(val value: String) : AddCustomTokenAction()
|
||||
data class OnTokenFieldChanged(val id: FieldId, val value: String) : AddCustomTokenAction()
|
||||
object OnCreate : AddCustomTokenAction()
|
||||
object OnDestroy : AddCustomTokenAction()
|
||||
data class OnTokenFieldChanged(val id: FieldId, val value: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnTokenContractAddressChanged(val value: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnTokenNetworkChanged(val value: Field.Data<Blockchain>) : AddCustomTokenAction()
|
||||
data class OnTokenDerivationPathChanged(val value: Field.Data<Blockchain>) : AddCustomTokenAction()
|
||||
data class OnTokenDecimalsChanged(val value: Field.Data<String>) : AddCustomTokenAction()
|
||||
data class OnCustomTokenSelected(val any: Any = Unit) : AddCustomTokenAction()
|
||||
|
||||
|
||||
// from redux
|
||||
object UpdateForm : AddCustomTokenAction()
|
||||
data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction()
|
||||
data class FillTokenFields(
|
||||
val token: CoinsCheckAddressResponse.Token,
|
||||
val contract: CoinsCheckAddressResponse.Token.Contract,
|
||||
val token: Coins.CheckAddressResponse.Token,
|
||||
val contract: Coins.CheckAddressResponse.Token.Contract,
|
||||
) : AddCustomTokenAction()
|
||||
|
||||
sealed class Error : AddCustomTokenAction() {
|
||||
|
|
@ -37,7 +36,14 @@ sealed class AddCustomTokenAction : Action {
|
|||
}
|
||||
|
||||
sealed class Warning : AddCustomTokenAction() {
|
||||
data class Add(val warning: AddCustomTokenWarning) : Warning()
|
||||
data class Remove(val warning: AddCustomTokenWarning) : Warning()
|
||||
data class Add(val warnings: Set<AddCustomTokenWarning>) : Warning()
|
||||
data class Remove(val warnings: Set<AddCustomTokenWarning>) : Warning()
|
||||
data class Replace(val remove: Set<AddCustomTokenWarning>, val add: Set<AddCustomTokenWarning>) : 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,95 +2,184 @@ package com.tangem.domain.features.addCustomToken.redux
|
|||
|
||||
import android.webkit.ValueCallback
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.domain.DomainDialog
|
||||
import com.tangem.domain.common.form.*
|
||||
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.store.BaseStoreHub
|
||||
import com.tangem.domain.store.DomainState
|
||||
import com.tangem.domain.store.dispatchOnMain
|
||||
import com.tangem.domain.redux.BaseStoreHub
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import com.tangem.domain.redux.dispatchOnMain
|
||||
import com.tangem.domain.redux.domainStore
|
||||
import com.tangem.domain.redux.global.DomainGlobalAction
|
||||
import com.tangem.network.api.tangemTech.Coins
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object AddCustomTokenHub : BaseStoreHub<AddCustomTokensState>("AddCustomTokenHub") {
|
||||
internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomTokenHub") {
|
||||
|
||||
override val initialState: AddCustomTokensState = AddCustomTokensState()
|
||||
override fun getHubState(storeState: DomainState): AddCustomTokenState {
|
||||
return storeState.addCustomTokensState
|
||||
}
|
||||
|
||||
override fun handle(state: () -> DomainState?, action: Action, dispatch: DispatchFunction) = when (action) {
|
||||
is OnBackPressed -> hubScope.cancel()
|
||||
else -> super.handle(state, action, dispatch)
|
||||
override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState {
|
||||
return storeState.copy(addCustomTokensState = newHubState)
|
||||
}
|
||||
|
||||
override suspend fun handleAction(
|
||||
state: DomainState,
|
||||
action: Action,
|
||||
dispatch: DispatchFunction,
|
||||
storeState: DomainState,
|
||||
cancel: ValueCallback<Action>
|
||||
) {
|
||||
if (action !is AddCustomTokenAction) return
|
||||
val state = state.addCustomTokensState
|
||||
// val card = storeState.globalState.scanResponse?.card
|
||||
// ?: throw IllegalStateException("ScanResponse must be set before showing the AddCustomToken screen")
|
||||
val hubState = storeState.addCustomTokensState
|
||||
|
||||
when (action) {
|
||||
is OnCreate -> {
|
||||
// hubState.addCustomTokenManager.attachAuthKey(card.cardPublicKey.toHexString())
|
||||
}
|
||||
is OnDestroy -> hubScope.cancel()
|
||||
is OnTokenContractAddressChanged -> {
|
||||
val contractAddress = action.value
|
||||
val validator: TokenContractAddressValidator = getValidator(ContractAddress, state)
|
||||
val error = validator.validate(contractAddress)
|
||||
val validator: TokenContractAddressValidator = getValidator(ContractAddress, hubState)
|
||||
val error = validator.validate(contractAddress.value)
|
||||
addOrRemoveError(ContractAddress, error)
|
||||
if (error != null) return
|
||||
|
||||
val manager = state.addCustomTokenManager
|
||||
val selectedNetwork: Blockchain? = getField<TokenNetworkField>(Network, state).value.let {
|
||||
if (it == Blockchain.Unknown) null else it
|
||||
if (error != null || contractAddress.value.isEmpty()) {
|
||||
dispatchOnMain(actionsUnlockTokenFields())
|
||||
return
|
||||
}
|
||||
val foundTokens = manager.findContractAddress(contractAddress, selectedNetwork?.id)
|
||||
|
||||
val foundTokens = requestInfoAboutContractAddress(contractAddress.value, hubState)
|
||||
val warningsToAdd = mutableSetOf<AddCustomTokenWarning>()
|
||||
val warningsToRemove = mutableSetOf<AddCustomTokenWarning>()
|
||||
when {
|
||||
foundTokens.isEmpty() -> {}
|
||||
foundTokens.size == 1 -> {
|
||||
// fill and disable other fields by token info
|
||||
val token = foundTokens[0]
|
||||
dispatchOnMain(FillTokenFields(token, token.contracts[0]))
|
||||
foundTokens.isEmpty() -> {
|
||||
warningsToAdd.add(AddCustomTokenWarning.PotentialScamToken)
|
||||
}
|
||||
else -> {
|
||||
// show tokens list for selection
|
||||
|
||||
val token = foundTokens[0]
|
||||
checkToken(null, token, warningsToAdd, warningsToRemove)
|
||||
}
|
||||
|
||||
}
|
||||
if (warningsToAdd.isNotEmpty() || warningsToRemove.isNotEmpty()) {
|
||||
dispatchOnMain(Warning.Replace(warningsToRemove.toSet(), warningsToAdd.toSet()))
|
||||
}
|
||||
}
|
||||
is OnTokenNetworkChanged -> {
|
||||
val validator: TokenNetworkValidator = getValidator(Network, state)
|
||||
addOrRemoveError(Network, validator.validate(action.value))
|
||||
}
|
||||
is OnTokenFieldChanged -> {
|
||||
val validator: StringIsNotEmptyValidator = getValidator(action.id, state)
|
||||
addOrRemoveError(action.id as CustomTokenFieldId, validator.validate(action.value))
|
||||
val validator: TokenNetworkValidator = getValidator(Network, hubState)
|
||||
addOrRemoveError(Network, validator.validate(action.value.value))
|
||||
}
|
||||
is OnTokenDerivationPathChanged -> {
|
||||
val validator: DerivationPathValidator = getValidator(DerivationPath, state)
|
||||
addOrRemoveError(DerivationPath, validator.validate(action.value))
|
||||
// val validator: TokenDerivationPathValidator = getValidator(DerivationPath, hubState)
|
||||
// addOrRemoveError(DerivationPath, validator.validate(action.value.value))
|
||||
}
|
||||
is OnTokenDecimalsChanged -> {
|
||||
val validator: TokenDecimalsValidator = getValidator(Decimals, hubState)
|
||||
addOrRemoveError(Decimals, validator.validate(action.value.value))
|
||||
}
|
||||
is OnTokenFieldChanged -> {
|
||||
val validator: StringIsNotEmptyValidator = getValidator(action.id, hubState)
|
||||
addOrRemoveError(action.id as CustomTokenFieldId, validator.validate(action.value.value))
|
||||
}
|
||||
is OnCustomTokenSelected -> {
|
||||
// dispatchOnMain()
|
||||
}
|
||||
is FillTokenFields -> {
|
||||
val networkField = getField<TokenNetworkField>(Network, state)
|
||||
val nameField = getField<TokenField>(Name, state)
|
||||
val symbolField = getField<TokenField>(Symbol, state)
|
||||
val decimalsField = getField<TokenField>(Decimals, state)
|
||||
val networkField = getField<TokenNetworkField>(Network, hubState)
|
||||
val nameField = getField<TokenField>(Name, hubState)
|
||||
val symbolField = getField<TokenField>(Symbol, hubState)
|
||||
val decimalsField = getField<TokenField>(Decimals, hubState)
|
||||
|
||||
val token = action.token
|
||||
val contract = action.contract
|
||||
networkField.value = Blockchain.fromId(contract.networkId)
|
||||
nameField.value = token.name
|
||||
symbolField.value = token.symbol
|
||||
decimalsField.value = contract.decimalCount.toString()
|
||||
val blockchain = Blockchain.fromNetworkId(contract.networkId)
|
||||
networkField.data = Field.Data(blockchain, false)
|
||||
nameField.data = Field.Data(token.name, false)
|
||||
symbolField.data = Field.Data(token.symbol, false)
|
||||
decimalsField.data = Field.Data(contract.decimalCount.toString(), false)
|
||||
|
||||
dispatchOnMain(UpdateForm)
|
||||
dispatchOnMain(UpdateForm(hubState))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestInfoAboutContractAddress(
|
||||
contractAddress: String,
|
||||
hubState: AddCustomTokenState
|
||||
): List<Coins.CheckAddressResponse.Token> {
|
||||
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true))))
|
||||
val tokenManager = hubState.addCustomTokenManager
|
||||
val field = getField<TokenNetworkField>(Network, hubState)
|
||||
val selectedNetworkId: String? = field.data.value.let {
|
||||
if (it == Blockchain.Unknown) null else it
|
||||
}?.toNetworkId()
|
||||
val foundTokens = tokenManager.checkAddress(contractAddress, selectedNetworkId)
|
||||
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = false))))
|
||||
return foundTokens
|
||||
}
|
||||
|
||||
private suspend fun checkToken(
|
||||
card: Card?,
|
||||
token: Coins.CheckAddressResponse.Token,
|
||||
warningsToAdd: MutableSet<AddCustomTokenWarning>,
|
||||
warningsToRemove: MutableSet<AddCustomTokenWarning>,
|
||||
) {
|
||||
val contracts = token.contracts
|
||||
when {
|
||||
contracts.isEmpty() -> {
|
||||
}
|
||||
contracts.size == 1 -> {
|
||||
val contract = contracts[0]
|
||||
val isPersistIntoTheAppAddedTokenList = isPersistIntoTheAppAddedTokenList(token, contract)
|
||||
|
||||
if (isPersistIntoTheAppAddedTokenList) {
|
||||
warningsToAdd.add(AddCustomTokenWarning.TokenAlreadyAdded)
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false)))
|
||||
dispatchOnMain(actionsLockTokenFields())
|
||||
} else {
|
||||
dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true)))
|
||||
// val isStandardDerivation = card.derivationType == DerivationType.Standard
|
||||
val isStandardDerivation = true
|
||||
val isStandardToken = token.active && isStandardDerivation
|
||||
if (isStandardToken) {
|
||||
dispatchOnMain(FillTokenFields(token, contract))
|
||||
dispatchOnMain(actionsLockTokenFields())
|
||||
} else {
|
||||
warningsToAdd.add(AddCustomTokenWarning.PotentialScamToken)
|
||||
dispatchOnMain(actionsUnlockTokenFields())
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val dialog = DomainDialog.SelectTokenDialog(
|
||||
items = contracts,
|
||||
itemNameConverter = { it.address },
|
||||
onSelect = { selectedContract ->
|
||||
hubScope.launch {
|
||||
// find how to connect to the upper coroutineContext and dispatch through them
|
||||
dispatchOnMain(FillTokenFields(token, selectedContract))
|
||||
dispatchOnMain(FillTokenFields(token, selectedContract))
|
||||
}
|
||||
},
|
||||
)
|
||||
dispatchOnMain(DomainGlobalAction.ShowDialog(dialog))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isPersistIntoTheAppAddedTokenList(
|
||||
token: Coins.CheckAddressResponse.Token,
|
||||
contract: Coins.CheckAddressResponse.Token.Contract
|
||||
): Boolean = false
|
||||
|
||||
private suspend fun addOrRemoveError(id: CustomTokenFieldId, error: AddCustomTokenError?) {
|
||||
if (error == null) {
|
||||
dispatchOnMain(Error.Remove(id))
|
||||
|
|
@ -99,38 +188,62 @@ internal object AddCustomTokenHub : BaseStoreHub<AddCustomTokensState>("AddCusto
|
|||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> getField(id: FieldId, state: AddCustomTokensState): T {
|
||||
private fun actionsLockTokenFields(): Action {
|
||||
val state = domainStore.state.addCustomTokensState
|
||||
return 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),
|
||||
))
|
||||
}
|
||||
|
||||
private fun actionsUnlockTokenFields(): Action {
|
||||
val state = domainStore.state.addCustomTokensState
|
||||
return 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),
|
||||
))
|
||||
}
|
||||
|
||||
private inline fun <reified T> getField(id: FieldId, state: AddCustomTokenState): T {
|
||||
return state.form.getField(id) as T
|
||||
}
|
||||
|
||||
private inline fun <reified T> getValidator(id: FieldId, state: AddCustomTokensState): T {
|
||||
private inline fun <reified T> getValidator(id: FieldId, state: AddCustomTokenState): T {
|
||||
return state.getValidator(id) as T
|
||||
}
|
||||
|
||||
override fun reduceAction(action: Action, state: AddCustomTokensState): AddCustomTokensState {
|
||||
override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState {
|
||||
return when (action) {
|
||||
is SetTangemTechAuthHeader -> {
|
||||
state.apply { addCustomTokenManager.attachAuthKey(action.cardPublicKeyHex) }
|
||||
}
|
||||
is UpdateForm -> updateFormState(state)
|
||||
is OnTokenNetworkChanged -> {
|
||||
val field: TokenNetworkField = getField(Network, state)
|
||||
field.value = action.value
|
||||
updateFormState(state)
|
||||
is UpdateForm -> {
|
||||
updateFormState(action.state)
|
||||
}
|
||||
is OnTokenContractAddressChanged -> {
|
||||
val field: TokenField = getField(ContractAddress, state)
|
||||
field.value = action.value
|
||||
field.data = action.value
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenNetworkChanged -> {
|
||||
val field: TokenNetworkField = getField(Network, state)
|
||||
field.data = action.value
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenDerivationPathChanged -> {
|
||||
val field: TokenDerivationPathField = getField(Network, state)
|
||||
field.value = action.value
|
||||
val field: TokenDerivationPathField = getField(DerivationPath, state)
|
||||
field.data = action.value
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenDecimalsChanged -> {
|
||||
val field: TokenField = getField(Decimals, state)
|
||||
field.data = action.value
|
||||
updateFormState(state)
|
||||
}
|
||||
is OnTokenFieldChanged -> {
|
||||
val field: TokenField = getField(action.id, state)
|
||||
field.value = action.value
|
||||
field.data = action.value
|
||||
updateFormState(state)
|
||||
}
|
||||
is Error.Add -> {
|
||||
|
|
@ -142,18 +255,143 @@ internal object AddCustomTokenHub : BaseStoreHub<AddCustomTokensState>("AddCusto
|
|||
state.copy(formErrors = newMap)
|
||||
}
|
||||
is Warning.Add -> {
|
||||
val newList = state.warnings.toMutableList().apply { add(action.warning) }
|
||||
state.copy(warnings = newList)
|
||||
val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) }
|
||||
state.copy(warnings = newList.toSet())
|
||||
}
|
||||
is Warning.Remove -> {
|
||||
val newList = state.warnings.toMutableList().apply { remove(action.warning) }
|
||||
state.copy(warnings = newList)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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: AddCustomTokensState): AddCustomTokensState {
|
||||
private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState {
|
||||
return state.copy(form = Form(state.form.fieldList))
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: refactoring: replace by Blockchain.Companion.fromNetworkId
|
||||
fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain {
|
||||
return when (networkId) {
|
||||
"avalanche" -> Blockchain.Avalanche
|
||||
"binancecoin" -> Blockchain.Binance
|
||||
"binance-smart-chain" -> Blockchain.BSC
|
||||
"ethereum" -> Blockchain.Ethereum
|
||||
"polygon-pos" -> Blockchain.Polygon
|
||||
"solana" -> Blockchain.Solana
|
||||
"fantom" -> Blockchain.Fantom
|
||||
"bitcoin" -> Blockchain.Bitcoin
|
||||
"bitcoin-cash" -> Blockchain.BitcoinCash
|
||||
"cardano" -> Blockchain.CardanoShelley
|
||||
"dogecoin" -> Blockchain.Dogecoin
|
||||
"ducatus" -> Blockchain.Ducatus
|
||||
"litecoin" -> Blockchain.Litecoin
|
||||
"rsk" -> Blockchain.RSK
|
||||
"stellar" -> Blockchain.Stellar
|
||||
"tezos" -> Blockchain.Tezos
|
||||
"ripple" -> Blockchain.XRP
|
||||
else -> Blockchain.Unknown
|
||||
}
|
||||
}
|
||||
|
||||
fun Blockchain.toNetworkId(): String? {
|
||||
return when (this) {
|
||||
Blockchain.Unknown -> null
|
||||
Blockchain.Avalanche -> "avalanche"
|
||||
Blockchain.AvalancheTestnet -> "avalanche"
|
||||
Blockchain.Binance -> "binancecoin"
|
||||
Blockchain.BinanceTestnet -> "binancecoin"
|
||||
Blockchain.BSC -> "binance-smart-chain"
|
||||
Blockchain.BSCTestnet -> "binance-smart-chain"
|
||||
Blockchain.Bitcoin -> "bitcoin"
|
||||
Blockchain.BitcoinTestnet -> "bitcoin"
|
||||
Blockchain.BitcoinCash -> "bitcoin-cash"
|
||||
Blockchain.BitcoinCashTestnet -> "bitcoin-cash"
|
||||
Blockchain.Cardano -> "cardano"
|
||||
Blockchain.CardanoShelley -> "cardano"
|
||||
Blockchain.Dogecoin -> "dogecoin"
|
||||
Blockchain.Ducatus -> "ducatus"
|
||||
Blockchain.Ethereum -> "ethereum"
|
||||
Blockchain.EthereumTestnet -> "ethereum"
|
||||
Blockchain.Fantom -> "fantom"
|
||||
Blockchain.FantomTestnet -> "fantom"
|
||||
Blockchain.Litecoin -> "litecoin"
|
||||
Blockchain.Polygon -> "matic-network"
|
||||
Blockchain.PolygonTestnet -> "matic-networks"
|
||||
Blockchain.RSK -> "rootstock"
|
||||
Blockchain.Stellar -> "stellar"
|
||||
Blockchain.StellarTestnet -> "stellar"
|
||||
Blockchain.Solana -> "solana"
|
||||
Blockchain.SolanaTestnet -> "solana"
|
||||
Blockchain.Tezos -> "tezos"
|
||||
Blockchain.XRP -> "ripple"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.form.*
|
||||
import com.tangem.domain.features.addCustomToken.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class AddCustomTokenState(
|
||||
val form: Form = Form(createFormFields()),
|
||||
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<*>> = createFormValidators(),
|
||||
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
|
||||
val warnings: Set<AddCustomTokenWarning> = emptySet(),
|
||||
val screenState: ScreenState = createInitialScreenState(),
|
||||
val addCustomTokenManager: AddCustomTokenManager = AddCustomTokenManager(TangemTechService())
|
||||
) : StateType {
|
||||
|
||||
val completeDataType: CompleteDataType
|
||||
get() = calculateDataType()
|
||||
|
||||
inline fun <reified T> visitDataConverter(converter: FieldDataConverter<T>): T {
|
||||
form.visitDataConverter(converter)
|
||||
return converter.getConvertedData()
|
||||
}
|
||||
|
||||
fun getValidator(id: FieldId): CustomTokenValidator<*> = formValidators[id]!!
|
||||
|
||||
fun hasError(id: FieldId): Boolean = formErrors[id] != null
|
||||
|
||||
fun getError(id: FieldId): AddCustomTokenError? {
|
||||
return formErrors[id]
|
||||
}
|
||||
|
||||
private fun calculateDataType(): CompleteDataType {
|
||||
val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals)
|
||||
val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) }
|
||||
|
||||
val isEmptyValidator = StringIsEmptyValidator()
|
||||
fieldsToCheck.map { data -> data.toString() }.forEach {
|
||||
// if one of the fields has error -> then it
|
||||
val error = isEmptyValidator.validate(it)
|
||||
if (error != null) return CompleteDataType.Token
|
||||
}
|
||||
|
||||
return CompleteDataType.Blockchain
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) {
|
||||
Blockchain.Unknown -> unknown
|
||||
Blockchain.Cardano -> "Cardano"
|
||||
Blockchain.CardanoShelley -> "Cardano Shelley"
|
||||
else -> blockchain.fullName
|
||||
}
|
||||
|
||||
fun convertDerivationPathName(blockchain: Blockchain, unknown: String): String = when (blockchain) {
|
||||
Blockchain.Unknown -> unknown
|
||||
Blockchain.BSC -> "BNB Smart Chain"
|
||||
Blockchain.Fantom -> "Fantom Opera"
|
||||
else -> blockchain.fullName
|
||||
}
|
||||
|
||||
private fun createFormFields(): List<DataField<*>> {
|
||||
return listOf(
|
||||
TokenField(ContractAddress),
|
||||
TokenNetworkField(Network, getSupportedNetworks()),
|
||||
TokenField(Name),
|
||||
TokenField(Symbol),
|
||||
TokenField(Decimals),
|
||||
TokenDerivationPathField(DerivationPath, getSupportedDerivations()),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFormValidators(): Map<CustomTokenFieldId, CustomTokenValidator<*>> {
|
||||
return mapOf(
|
||||
ContractAddress to TokenContractAddressValidator(),
|
||||
Network to TokenNetworkValidator(),
|
||||
Name to StringIsNotEmptyValidator(),
|
||||
Symbol to StringIsNotEmptyValidator(),
|
||||
Decimals to TokenDecimalsValidator(),
|
||||
// DerivationPath to TokenDerivationPathValidator(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportedNetworks(): List<Blockchain> {
|
||||
return listOf(
|
||||
Blockchain.Ethereum,
|
||||
Blockchain.BSC,
|
||||
Blockchain.Binance,
|
||||
Blockchain.Polygon,
|
||||
Blockchain.Avalanche,
|
||||
// Blockchain.Solana, // not supported until tokens added to the Blockchain SDK
|
||||
Blockchain.Fantom,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportedDerivations(): List<Blockchain> {
|
||||
val evmBlockchains = Blockchain.values().filter {
|
||||
!it.isTestnet() && it.getChainId() != null
|
||||
}
|
||||
return evmBlockchains
|
||||
}
|
||||
|
||||
private fun createInitialScreenState(): ScreenState {
|
||||
return ScreenState(
|
||||
contractAddressField = ViewStates.TokenField(),
|
||||
network = ViewStates.TokenField(),
|
||||
name = ViewStates.TokenField(),
|
||||
symbol = ViewStates.TokenField(),
|
||||
decimals = ViewStates.TokenField(),
|
||||
derivationPath = ViewStates.TokenField(),
|
||||
addButton = ViewStates.AddButton()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.common.form.*
|
||||
import com.tangem.domain.features.addCustomToken.*
|
||||
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
|
||||
import com.tangem.network.api.tangemTech.TangemTechService
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class AddCustomTokensState(
|
||||
val form: Form = Form(createFormFields()),
|
||||
val formValidators: Map<CustomTokenFieldId, CustomTokenValidator<*>> = createFormValidators(),
|
||||
val formErrors: Map<CustomTokenFieldId, AddCustomTokenError> = emptyMap(),
|
||||
val warnings: List<AddCustomTokenWarning> = emptyList(),
|
||||
val addCustomTokenManager: AddCustomTokenManager = AddCustomTokenManager(TangemTechService())
|
||||
) : StateType {
|
||||
|
||||
val completeDataType: CompleteDataType
|
||||
get() = calculateDataType()
|
||||
|
||||
fun getData(
|
||||
converter: FieldDataConverter<out CompleteData> = CompleteData.createDataConverter(completeDataType)
|
||||
): CompleteData {
|
||||
form.getData(converter)
|
||||
return converter.getConvertedData()
|
||||
}
|
||||
|
||||
fun getLockedFieldsForKnownToken(): List<CustomTokenFieldId> {
|
||||
return listOf(
|
||||
Name, Symbol, Decimals
|
||||
)
|
||||
}
|
||||
|
||||
fun getValidator(id: FieldId): CustomTokenValidator<*> = formValidators[id]!!
|
||||
|
||||
fun hasError(id: FieldId): Boolean = formErrors[id] != null
|
||||
|
||||
fun getError(id: FieldId): AddCustomTokenError? {
|
||||
return formErrors[id]
|
||||
}
|
||||
|
||||
private fun calculateDataType(): CompleteDataType {
|
||||
val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals)
|
||||
val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) }
|
||||
|
||||
val isEmptyValidator = StringIsEmptyValidator()
|
||||
fieldsToCheck.map { data -> data.toString() }.forEach {
|
||||
// if one of the fields has error -> then it
|
||||
val error = isEmptyValidator.validate(it)
|
||||
if (error != null) return CompleteDataType.Token
|
||||
}
|
||||
|
||||
return CompleteDataType.Blockchain
|
||||
}
|
||||
|
||||
|
||||
companion object Utils {
|
||||
private fun createFormFields(): List<DataField<*>> {
|
||||
return listOf(
|
||||
TokenField(ContractAddress),
|
||||
TokenNetworkField(Network, getSupportedBlockchains()),
|
||||
TokenField(Name),
|
||||
TokenField(Symbol),
|
||||
TokenField(Decimals),
|
||||
TokenDerivationPathField(DerivationPath),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createFormValidators(): Map<CustomTokenFieldId, CustomTokenValidator<*>> {
|
||||
return mapOf(
|
||||
ContractAddress to TokenContractAddressValidator(),
|
||||
Network to TokenNetworkValidator(),
|
||||
Name to StringIsNotEmptyValidator(),
|
||||
Symbol to StringIsNotEmptyValidator(),
|
||||
Decimals to StringIsNotEmptyValidator(),
|
||||
DerivationPath to DerivationPathValidator(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getSupportedBlockchains(): List<Blockchain> {
|
||||
return Blockchain.values().filter { !it.isTestnet() }.toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class CompleteDataType {
|
||||
Blockchain, Token
|
||||
}
|
||||
|
||||
sealed class CompleteData() {
|
||||
|
||||
companion object {
|
||||
fun createDataConverter(completeDataType: CompleteDataType): FieldDataConverter<out CompleteData> =
|
||||
when (completeDataType) {
|
||||
CompleteDataType.Blockchain -> CustomBlockchain.Converter()
|
||||
CompleteDataType.Token -> CustomToken.Converter()
|
||||
}
|
||||
}
|
||||
|
||||
class CustomBlockchain(
|
||||
val selectedNetwork: Blockchain,
|
||||
val derivationPath: String?
|
||||
) : CompleteData() {
|
||||
|
||||
class Converter : BaseFieldDataConverter<CustomBlockchain>() {
|
||||
override fun getConvertedData(): CustomBlockchain = CustomBlockchain(
|
||||
collectedData[Network] as Blockchain,
|
||||
collectedData[DerivationPath] as? String,
|
||||
)
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = listOf(Network, DerivationPath)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomToken(
|
||||
val contractAddress: String,
|
||||
val selectedNetwork: Blockchain,
|
||||
val name: String,
|
||||
val tokenSymbol: String,
|
||||
val decimals: Int,
|
||||
val derivationPath: String?,
|
||||
) : CompleteData() {
|
||||
|
||||
class Converter : BaseFieldDataConverter<CustomToken>() {
|
||||
override fun getConvertedData(): CustomToken = CustomToken(
|
||||
collectedData[ContractAddress] as String,
|
||||
collectedData[Network] as Blockchain,
|
||||
collectedData[Name] as String,
|
||||
collectedData[Symbol] as String,
|
||||
collectedData[Decimals] as Int,
|
||||
collectedData[DerivationPath] as? String,
|
||||
)
|
||||
|
||||
override fun getIdToCollect(): List<FieldId> = CustomTokenFieldId.values().toList()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.domain.features.addCustomToken.redux
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class CompleteDataType {
|
||||
Blockchain, Token
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
13
domain/src/main/java/com/tangem/domain/redux/DomainState.kt
Normal file
13
domain/src/main/java/com/tangem/domain/redux/DomainState.kt
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
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
|
||||
33
domain/src/main/java/com/tangem/domain/redux/DomainStore.kt
Normal file
33
domain/src/main/java/com/tangem/domain/redux/DomainStore.kt
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub
|
||||
import com.tangem.domain.redux.global.DomainGlobalHub
|
||||
import com.tangem.domain.redux.state.observeReducedStates
|
||||
import org.rekotlin.Store
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
private class DomainStore // for simple search
|
||||
|
||||
private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(
|
||||
DomainGlobalHub(),
|
||||
AddCustomTokenHub(),
|
||||
)
|
||||
|
||||
val domainStore = Store(
|
||||
state = DomainState(),
|
||||
middleware = RE_STORE_HUBS.map { it.getMiddleware() },
|
||||
reducer = { action, state ->
|
||||
requireNotNull(state)
|
||||
|
||||
// we can examine the store state after each change by reducer
|
||||
val reducedSates = RE_STORE_HUBS.mapNotNull {
|
||||
val reducedState = it.reduce(action, state)
|
||||
if (reducedState == state) null else Pair(action, reducedState)
|
||||
}
|
||||
observeReducedStates(reducedSates)
|
||||
|
||||
if (reducedSates.isEmpty()) state else reducedSates.last().second
|
||||
}
|
||||
)
|
||||
107
domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt
Normal file
107
domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
package com.tangem.domain.redux
|
||||
|
||||
import android.webkit.ValueCallback
|
||||
import com.tangem.domain.common.FeatureCoroutineExceptionHandler
|
||||
import com.tangem.domain.common.extensions.withIOContext
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import kotlinx.coroutines.*
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* ReStoreHub's should not store the <StoreState> or the <State>, because this can lead to destabilization of
|
||||
* a state behavior.
|
||||
*/
|
||||
// all ReStoreHub's must be marked as internal
|
||||
internal interface ReStoreHub<StoreState, State> : HubMiddleware<StoreState>, HubReducer<StoreState>
|
||||
|
||||
internal interface HubMiddleware<StoreState> {
|
||||
fun getMiddleware(): Middleware<StoreState>
|
||||
}
|
||||
|
||||
internal interface HubReducer<StoreState> {
|
||||
fun reduce(action: Action, storeState: StoreState): StoreState
|
||||
}
|
||||
|
||||
/**
|
||||
* ReStoreHub is the entry point for actions. It processes it through middleware and reducer.
|
||||
* Actions handled by ReStoreHub go into coroutine scope, which can be canceled while the action is being processed.
|
||||
* All action went from the middleware must be dispatched through ReStoreHub.dispatchOnMain(Actions) to prevent
|
||||
* concurrent modification in the Store
|
||||
* Only the changed hub State will change its state in the DomainState
|
||||
* @param name - name of the Hub
|
||||
* @param dispatcher - main coroutine dispatcher for actions
|
||||
*/
|
||||
internal abstract class BaseStoreHub<State>(
|
||||
private val name: String,
|
||||
private val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
||||
) : ReStoreHub<DomainState, State> {
|
||||
|
||||
val hubScope = CoroutineScope(
|
||||
Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name)
|
||||
)
|
||||
|
||||
private val actionsAndJobs = mutableMapOf<Action, Job>()
|
||||
|
||||
override fun getMiddleware(): Middleware<DomainState> {
|
||||
return { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handle(state, action, dispatch)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches new coroutine and stores the action with it's coroutine job. (Coroutine can be cancelled
|
||||
* through invoking the cancelActionJob() function inside a middleware).
|
||||
* Removes the action when job is completed.
|
||||
*/
|
||||
protected open fun handle(storeStateHolder: () -> DomainState?, action: Action, dispatch: DispatchFunction) {
|
||||
val storeState = storeStateHolder()
|
||||
?: throw UnsupportedOperationException("StoreState for the $name can't be NULL")
|
||||
|
||||
hubScope.launch {
|
||||
actionsAndJobs[action] = this.coroutineContext.job
|
||||
actionsAndJobs[action]?.invokeOnCompletion { actionsAndJobs.remove(action) }
|
||||
|
||||
handleAction(action, storeState) {
|
||||
actionsAndJobs.remove(it)?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce the action and check if - if the action hasn't updated the hubState, then it doesn't need to update
|
||||
* storeState
|
||||
*/
|
||||
override fun reduce(action: Action, storeState: DomainState): DomainState {
|
||||
val oldState = getHubState(storeState)
|
||||
val newState = reduceAction(action, oldState)
|
||||
return if (oldState === newState) {
|
||||
storeState
|
||||
} else {
|
||||
updateStoreState(storeState, newState)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback<Action>)
|
||||
protected abstract fun reduceAction(action: Action, state: State): State
|
||||
|
||||
protected abstract fun getHubState(storeState: DomainState): State
|
||||
protected abstract fun updateStoreState(storeState: DomainState, newHubState: State): DomainState
|
||||
|
||||
}
|
||||
|
||||
internal suspend inline fun ReStoreHub<*, *>.dispatchOnMain(vararg actions: Action) {
|
||||
withMainContext { actions.forEach { domainStore.dispatch(it) } }
|
||||
}
|
||||
|
||||
internal suspend inline fun ReStoreHub<*, *>.dispatchOnIO(vararg actions: Action) {
|
||||
withIOContext { actions.forEach { domainStore.dispatch(it) } }
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.features.global.redux
|
||||
package com.tangem.domain.redux.global
|
||||
|
||||
import com.tangem.domain.DomainStateDialog
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
import org.rekotlin.Action
|
||||
|
||||
|
|
@ -9,4 +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()
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.domain.features.global.redux
|
||||
package com.tangem.domain.redux.global
|
||||
|
||||
import android.webkit.ValueCallback
|
||||
import com.tangem.domain.restore.BaseStoreHub
|
||||
import com.tangem.domain.restore.DomainState
|
||||
import com.tangem.domain.redux.BaseStoreHub
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -16,19 +15,18 @@ internal class DomainGlobalHub : BaseStoreHub<DomainGlobalState>("DomainGlobalHu
|
|||
return storeState.globalState
|
||||
}
|
||||
|
||||
override fun updateStoreState(storeState: DomainState, newState: DomainGlobalState): DomainState {
|
||||
return storeState.copy(globalState = newState)
|
||||
override fun updateStoreState(storeState: DomainState, newHubState: DomainGlobalState): DomainState {
|
||||
return storeState.copy(globalState = newHubState)
|
||||
}
|
||||
|
||||
override suspend fun handleAction(
|
||||
state: DomainState,
|
||||
action: Action,
|
||||
dispatch: DispatchFunction,
|
||||
storeState: DomainState,
|
||||
cancel: ValueCallback<Action>
|
||||
) {
|
||||
if (action !is DomainGlobalAction) return
|
||||
|
||||
val state = state.globalState
|
||||
val state = storeState.globalState
|
||||
|
||||
when (action) {
|
||||
}
|
||||
|
|
@ -38,6 +36,9 @@ internal class DomainGlobalHub : BaseStoreHub<DomainGlobalState>("DomainGlobalHu
|
|||
is DomainGlobalAction.SetScanResponse -> {
|
||||
state.copy(scanResponse = action.scanResponse)
|
||||
}
|
||||
is DomainGlobalAction.ShowDialog -> {
|
||||
state.copy(dialog = action.stateDialog)
|
||||
}
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.domain.features.global.redux
|
||||
package com.tangem.domain.redux.global
|
||||
|
||||
import com.tangem.domain.DomainStateDialog
|
||||
import com.tangem.domain.common.ScanResponse
|
||||
|
||||
/**
|
||||
|
|
@ -8,4 +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,
|
||||
)
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.domain.redux.state
|
||||
|
||||
import com.tangem.domain.features.addCustomToken.AddCustomTokenStatePrinter
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import org.rekotlin.Action
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
* Use it only in debug mode!
|
||||
*/
|
||||
internal fun observeReducedStates(reducedSates: List<Pair<Action, DomainState>>) {
|
||||
// we can add any logic to watch for changes of actions, states, etc.
|
||||
val isDebugMode = true
|
||||
if (!isDebugMode) return
|
||||
|
||||
logStates(reducedSates)
|
||||
}
|
||||
|
||||
private fun logStates(reducedSates: List<Pair<Action, DomainState>>) {
|
||||
reducedSates.forEach {
|
||||
val printer = statePrinters.firstNotNullOfOrNull { entry ->
|
||||
if (entry.key.isAssignableFrom(it.first::class.java)) entry.value else null
|
||||
} ?: return@forEach
|
||||
|
||||
val messageToPrint = printer.print(it.first, it.second) ?: return@forEach
|
||||
|
||||
Timber.d(messageToPrint)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: refactoring: mutate to factory
|
||||
private val statePrinters = mutableMapOf(
|
||||
AddCustomTokenAction::class.java to AddCustomTokenStatePrinter()
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.domain.redux.state
|
||||
|
||||
import com.tangem.domain.redux.DomainState
|
||||
import org.rekotlin.Action
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface StatePrinter<A, S> {
|
||||
fun print(action: Action, domainState: DomainState): String?
|
||||
fun getStateObject(domainState: DomainState): S
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package com.tangem.domain.store
|
||||
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub
|
||||
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokensState
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import org.rekotlin.StateType
|
||||
import org.rekotlin.Store
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
private class DomainStore // for simple search
|
||||
|
||||
val domainStore = Store(
|
||||
state = DomainState(),
|
||||
middleware = domainMiddlewares(),
|
||||
reducer = { action, state -> domainReduce(action, state) }
|
||||
)
|
||||
|
||||
data class DomainState(
|
||||
val addCustomTokensState: AddCustomTokensState = AddCustomTokenHub.initialState
|
||||
) : StateType
|
||||
|
||||
private fun domainMiddlewares(): List<Middleware<DomainState>> {
|
||||
return listOf(
|
||||
AddCustomTokenHub.middleware
|
||||
)
|
||||
}
|
||||
|
||||
private fun domainReduce(action: Action, state: DomainState?): DomainState {
|
||||
requireNotNull(state)
|
||||
|
||||
return DomainState(
|
||||
addCustomTokensState = AddCustomTokenHub.reduceAction(action, state.addCustomTokensState)
|
||||
)
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
package com.tangem.domain.store
|
||||
|
||||
import android.webkit.ValueCallback
|
||||
import com.tangem.domain.common.FeatureCoroutineExceptionHandler
|
||||
import com.tangem.domain.common.extensions.withIOContext
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import kotlinx.coroutines.*
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.DispatchFunction
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface StoreHub<StoreState, State> {
|
||||
val initialState: State
|
||||
val middleware: Middleware<StoreState>
|
||||
fun reduceAction(action: Action, state: State): State
|
||||
}
|
||||
|
||||
/**
|
||||
* Hub contains the entry points for actions. It processes it through middleware and reducer.
|
||||
* All action went from the middleware must be dispatched through StoreHub.dispatchOnMain(Actions)
|
||||
* and StoreHub.dispatchOnIO(Actions)
|
||||
|
||||
* Hub is the provider of an initial state of a State.
|
||||
*
|
||||
* @param name - name of the Hub
|
||||
*/
|
||||
abstract class BaseStoreHub<State>(
|
||||
private val name: String,
|
||||
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : StoreHub<DomainState, State> {
|
||||
|
||||
protected val actionsAndJobs = mutableMapOf<Action, Job>()
|
||||
protected val hubScope = CoroutineScope(
|
||||
Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name)
|
||||
)
|
||||
|
||||
/**
|
||||
* Main entry point for the all actions
|
||||
*/
|
||||
override val middleware: Middleware<DomainState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handle(state, action, dispatch)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches new coroutine and stores the action with it's coroutine job. (Coroutine can be cancelled
|
||||
* through invoking the cancelActionJob() function inside a middleware).
|
||||
* Removes the action when job is completed.
|
||||
*/
|
||||
protected open fun handle(state: () -> DomainState?, action: Action, dispatch: DispatchFunction) {
|
||||
val domainState = state() ?: throw UnsupportedOperationException("State for the $name can't be NULL")
|
||||
|
||||
hubScope.launch {
|
||||
actionsAndJobs[action] = this.coroutineContext.job
|
||||
actionsAndJobs[action]?.invokeOnCompletion { actionsAndJobs.remove(action) }
|
||||
|
||||
handleAction(
|
||||
state = domainState,
|
||||
action = action,
|
||||
dispatch = dispatch,
|
||||
cancel = { actionsAndJobs.remove(it)?.cancel() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract suspend fun handleAction(
|
||||
state: DomainState,
|
||||
action: Action,
|
||||
dispatch: DispatchFunction,
|
||||
cancel: ValueCallback<Action>,
|
||||
)
|
||||
}
|
||||
|
||||
internal suspend inline fun StoreHub<*, *>.dispatchOnMain(vararg actions: Action) {
|
||||
withMainContext { actions.forEach { domainStore.dispatch(it) } }
|
||||
}
|
||||
|
||||
internal suspend inline fun StoreHub<*, *>.dispatchOnIO(vararg actions: Action) {
|
||||
withIOContext { actions.forEach { domainStore.dispatch(it) } }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue