Updated on 2026-08-14

This commit is contained in:
Tangem 2022-04-12 16:17:06 +03:00
parent ee8c426ca9
commit a38c36ce2d
28 changed files with 940 additions and 464 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View 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()
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}
}
}

View file

@ -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<*>)
}

View file

@ -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()

View file

@ -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")
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}

View file

@ -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))

View file

@ -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()
}
}

View file

@ -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"
}
}

View file

@ -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()
)
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}

View 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

View 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
}
)

View 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) } }
}

View file

@ -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()
}

View file

@ -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
}
}

View file

@ -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,
)

View file

@ -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()
)

View file

@ -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
}

View file

@ -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)
)
}

View file

@ -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) } }
}