Updated on 2026-08-14

This commit is contained in:
Tangem 2022-04-06 19:03:55 +03:00
parent 608121ab50
commit 82fd854852
49 changed files with 980 additions and 117 deletions

1
domain/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

75
domain/build.gradle Normal file
View file

@ -0,0 +1,75 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 31
defaultConfig {
minSdk 21
targetSdk 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
debug {
debuggable true
minifyEnabled false
}
release {
debuggable false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// composeOptions {
// kotlinCompilerExtensionVersion '1.1.0'
// }
packagingOptions {
exclude 'lib/x86_64/darwin/libscrypt.dylib'
exclude 'lib/x86_64/freebsd/libscrypt.so'
exclude 'lib/x86_64/linux/libscrypt.so'
}
}
dependencies {
implementation implementation(project(path: ':network'))
// Tangem sdk's
implementation 'com.tangem:blockchain:develop-66'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-140'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-140'
// Kotlin
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
// State management
implementation "org.rekotlin:rekotlin:1.0.4"
// Network
//TODO: it must depends from network module
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
implementation 'com.squareup.moshi:moshi:1.12.0'
implementation "com.squareup.moshi:moshi-kotlin:1.12.0"
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
// Logs
implementation 'com.jakewharton.timber:timber:4.7.1'
// Tests
testImplementation 'junit:junit:4.13.2'
testImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

21
domain/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,22 @@
package com.tangem.domain.features
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.tangem.feature2.test", appContext.packageName)
}
}

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.domain.features">
</manifest>

View file

@ -0,0 +1,24 @@
package com.tangem.domain.common
/**
[REDACTED_AUTHOR]
*/
interface DomainError {
val code: Int
val message: String
val data: Any?
}
open class AnyError(
override val code: Int,
override val message: String,
override val data: Any? = null,
) : DomainError
interface ErrorConverter<T> {
fun convertError(error: DomainError): T
}
interface Validator<Data, Error> {
fun validate(data: Data? = null): Error?
}

View file

@ -0,0 +1,23 @@
package com.tangem.domain.common
import kotlinx.coroutines.CoroutineExceptionHandler
import timber.log.Timber
import java.io.PrintWriter
import java.io.StringWriter
/**
[REDACTED_AUTHOR]
*/
class FeatureCoroutineExceptionHandler {
// add an external logger (FbAnalytics) for handling errors
companion object {
fun create(from: String): CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
val sw = StringWriter()
throwable.printStackTrace(PrintWriter(sw))
val exceptionAsString: String = sw.toString()
Timber.e("CoroutineException: from: %s, exception: %s", from, exceptionAsString)
throw throwable
}
}
}

View file

@ -0,0 +1,41 @@
package com.tangem.domain.common
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 val debounceScope = CoroutineScope(Job() + Dispatchers.Main)
private val flow = MutableStateFlow(value)
init {
initFlow()
}
private fun initFlow() {
debounceScope.launch {
flow.filter { if (value == null) true else value != it }
.debounce(debounce)
.onEach {
Timber.d("onValueChanged: $it")
onValueChanged(it)
}
.collect()
}
}
fun emmit(emmitValue: T) {
debounceScope.launch { flow.emit(emmitValue) }
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.domain.common.extensions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
[REDACTED_AUTHOR]
*/
suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.Main, block)
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.IO, block)

View file

@ -0,0 +1,63 @@
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.features.addCustomToken.AddCustomTokenError
/**
[REDACTED_AUTHOR]
*/
abstract class CustomTokenValidator<T> : Validator<T, AddCustomTokenError>
class StringIsEmptyValidator : CustomTokenValidator<String>() {
override fun validate(data: String?): AddCustomTokenError? = when {
data == null || data.isEmpty() -> null
else -> AddCustomTokenError.FieldIsNotEmpty
}
}
class StringIsNotEmptyValidator : CustomTokenValidator<String>() {
override fun validate(data: String?): AddCustomTokenError? = when {
data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty
else -> null
}
}
class TokenContractAddressValidator : CustomTokenValidator<String>() {
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
}
}
}
class TokenNetworkValidator : CustomTokenValidator<Blockchain>() {
override fun validate(data: Blockchain?): AddCustomTokenError? = when (data) {
null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected
else -> null
}
}
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
}
}
}
}

View file

@ -0,0 +1,75 @@
package com.tangem.domain.common.form
import com.tangem.common.json.MoshiJsonConverter
/**
[REDACTED_AUTHOR]
*/
class Form(
val fieldList: List<DataField<*>>,
) {
fun getField(id: FieldId): DataField<*>? = fieldList.firstOrNull { it.id == id }
fun getData(id: FieldId): Pair<FieldId, *>? = getField(id)?.getData()
// convert this form data whatever you want
fun getData(converter: FieldDataConverter<*>) {
fieldList.forEach { it.visitDataConverter(converter) }
}
}
interface FieldId
interface Field<Data> {
val id: FieldId
var value: Data
val isEnabled: Boolean
val isVisible: Boolean
}
abstract class BaseDataField<Data>(
override val id: FieldId,
override var value: Data
) : DataField<Data> {
override fun getData(): Pair<FieldId, Data> = id to value
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

@ -0,0 +1,31 @@
package com.tangem.domain.features.addCustomToken
import com.tangem.common.services.Result
import com.tangem.network.api.tangemTech.CoinsCheckAddressResponse
import com.tangem.network.api.tangemTech.TangemAuthInterceptor
import com.tangem.network.api.tangemTech.TangemTechService
/**
[REDACTED_AUTHOR]
*/
class AddCustomTokenManager(
private val tangemTechService: TangemTechService
) {
suspend fun findContractAddress(
contractAddress: String,
networkId: String? = null
): List<CoinsCheckAddressResponse.Token> {
val result = tangemTechService.coinsCheckAddress(contractAddress, networkId)
return when (result) {
is Result.Success -> {
result.data.tokens
}
is Result.Failure -> emptyList()
}
}
fun attachAuthKey(authKey: String) {
tangemTechService.addHeaderInterceptors(listOf(TangemAuthInterceptor(authKey)))
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.features.addCustomToken
import com.tangem.domain.common.AnyError
/**
[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()
object FieldIsEmpty : AddCustomTokenError()
object FieldIsNotEmpty : AddCustomTokenError()
object InvalidDerivationPath : AddCustomTokenError()
}

View file

@ -0,0 +1,36 @@
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.FieldId
/**
[REDACTED_AUTHOR]
*/
enum class CustomTokenFieldId : FieldId {
ContractAddress,
Network,
Name,
Symbol,
Decimals,
DerivationPath,
}
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, "")
data class TokenDerivationPathField(
override val id: FieldId,
override val isEnabled: Boolean = true,
override val isVisible: Boolean = true,
) : BaseDataField<String>(id, "")

View file

@ -0,0 +1,43 @@
package com.tangem.domain.features.addCustomToken.redux
import com.tangem.blockchain.common.Blockchain
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 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()
// from redux
object UpdateForm : AddCustomTokenAction()
data class FillTokenFields(
val token: CoinsCheckAddressResponse.Token,
val contract: CoinsCheckAddressResponse.Token.Contract,
) : AddCustomTokenAction()
sealed class Error : AddCustomTokenAction() {
data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : Error()
data class Remove(val id: CustomTokenFieldId) : Error()
}
sealed class Warning : AddCustomTokenAction() {
data class Add(val warning: AddCustomTokenWarning) : Warning()
data class Remove(val warning: AddCustomTokenWarning) : Warning()
}
}

View file

@ -0,0 +1,159 @@
package com.tangem.domain.features.addCustomToken.redux
import android.webkit.ValueCallback
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.domain.features.addCustomToken.redux.AddCustomTokenAction.*
import com.tangem.domain.store.BaseStoreHub
import com.tangem.domain.store.DomainState
import com.tangem.domain.store.dispatchOnMain
import kotlinx.coroutines.cancel
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
/**
[REDACTED_AUTHOR]
*/
internal object AddCustomTokenHub : BaseStoreHub<AddCustomTokensState>("AddCustomTokenHub") {
override val initialState: AddCustomTokensState = AddCustomTokensState()
override fun handle(state: () -> DomainState?, action: Action, dispatch: DispatchFunction) = when (action) {
is OnBackPressed -> hubScope.cancel()
else -> super.handle(state, action, dispatch)
}
override suspend fun handleAction(
state: DomainState,
action: Action,
dispatch: DispatchFunction,
cancel: ValueCallback<Action>
) {
if (action !is AddCustomTokenAction) return
val state = state.addCustomTokensState
when (action) {
is OnTokenContractAddressChanged -> {
val contractAddress = action.value
val validator: TokenContractAddressValidator = getValidator(ContractAddress, state)
val error = validator.validate(contractAddress)
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
}
val foundTokens = manager.findContractAddress(contractAddress, selectedNetwork?.id)
when {
foundTokens.isEmpty() -> {}
foundTokens.size == 1 -> {
// fill and disable other fields by token info
val token = foundTokens[0]
dispatchOnMain(FillTokenFields(token, token.contracts[0]))
}
else -> {
// show tokens list for selection
}
}
}
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))
}
is OnTokenDerivationPathChanged -> {
val validator: DerivationPathValidator = getValidator(DerivationPath, state)
addOrRemoveError(DerivationPath, validator.validate(action.value))
}
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 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()
dispatchOnMain(UpdateForm)
}
}
}
private suspend fun addOrRemoveError(id: CustomTokenFieldId, error: AddCustomTokenError?) {
if (error == null) {
dispatchOnMain(Error.Remove(id))
} else {
dispatchOnMain(Error.Add(id, error))
}
}
private inline fun <reified T> getField(id: FieldId, state: AddCustomTokensState): T {
return state.form.getField(id) as T
}
private inline fun <reified T> getValidator(id: FieldId, state: AddCustomTokensState): T {
return state.getValidator(id) as T
}
override fun reduceAction(action: Action, state: AddCustomTokensState): AddCustomTokensState {
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 OnTokenContractAddressChanged -> {
val field: TokenField = getField(ContractAddress, state)
field.value = action.value
updateFormState(state)
}
is OnTokenDerivationPathChanged -> {
val field: TokenDerivationPathField = getField(Network, state)
field.value = action.value
updateFormState(state)
}
is OnTokenFieldChanged -> {
val field: TokenField = getField(action.id, state)
field.value = action.value
updateFormState(state)
}
is Error.Add -> {
val newMap = state.formErrors.toMutableMap().apply { this[action.id] = action.error }
state.copy(formErrors = newMap)
}
is Error.Remove -> {
val newMap = state.formErrors.toMutableMap().apply { remove(action.id) }
state.copy(formErrors = newMap)
}
is Warning.Add -> {
val newList = state.warnings.toMutableList().apply { add(action.warning) }
state.copy(warnings = newList)
}
is Warning.Remove -> {
val newList = state.warnings.toMutableList().apply { remove(action.warning) }
state.copy(warnings = newList)
}
else -> state
}
}
private fun updateFormState(state: AddCustomTokensState): AddCustomTokensState {
return state.copy(form = Form(state.form.fieldList))
}
}

View file

@ -0,0 +1,137 @@
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,37 @@
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

@ -0,0 +1,87 @@
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) } }
}

View file

@ -0,0 +1,16 @@
package com.tangem.domain.features
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}