Updated on 2026-08-14

This commit is contained in:
Tangem 2022-05-04 13:48:54 +03:00
commit 15cb3d9411
16 changed files with 244 additions and 174 deletions

View file

@ -3,11 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
ext.configVar = [
configEnvironment: "CONFIG_ENVIRONMENT",
testActionEnabled: "TEST_ACTION_ENABLED",
]
apply from: '../dependencies.gradle'
android {
compileSdkVersion 31
@ -19,7 +15,8 @@ android {
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
buildFeatures {
compose true
@ -31,8 +28,9 @@ android {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'String', environmentConfig.environment, '\"prod\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
debug {
debuggable true
@ -42,16 +40,18 @@ android {
firebaseCrashlytics {
mappingFileUploadEnabled false
}
buildConfigField 'String', configVar.configEnvironment, '\"dev\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'true'
buildConfigField 'String', environmentConfig.environment, '\"dev\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'true'
}
debug_beta {
initWith release
debuggable false
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', configVar.configEnvironment, '\"prod\"'
buildConfigField 'Boolean', configVar.testActionEnabled, 'false'
buildConfigField 'String', environmentConfig.environment, '\"prod\"'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
}

View file

@ -7,6 +7,7 @@ import com.google.firebase.remoteconfig.ktx.remoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import com.tangem.Log
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.domain.DomainLayer
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.images.PicassoHelper
@ -46,6 +47,7 @@ class TapApplication : Application() {
override fun onCreate() {
super.onCreate()
DomainLayer.init()
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
Firebase.remoteConfig.setConfigSettingsAsync(remoteConfigSettings {

View file

@ -9,8 +9,8 @@ interface Loader<T> {
fun load(onComplete: (T) -> Unit)
companion object {
const val featuresName = "features_${BuildConfig.CONFIG_ENVIRONMENT}"
const val configValuesName = "tangem-app-config/config_${BuildConfig.CONFIG_ENVIRONMENT}"
const val warnings = "warnings_${BuildConfig.CONFIG_ENVIRONMENT}"
const val featuresName = "features_${BuildConfig.ENVIRONMENT}"
const val configValuesName = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}"
const val warnings = "warnings_${BuildConfig.ENVIRONMENT}"
}
}

View file

@ -2,3 +2,9 @@ ext.versions = [
kotlin : '1.6.10',
build_gradle: '7.1.3',
]
ext.environmentConfig = [
environment: "ENVIRONMENT",
testActionEnabled: "TEST_ACTION_ENABLED",
logEnabled : "LOG_ENABLED",
]

View file

@ -3,6 +3,8 @@ plugins {
id 'org.jetbrains.kotlin.android'
}
apply from: '../dependencies.gradle'
android {
compileSdk 31
@ -12,6 +14,8 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
buildTypes {
@ -19,13 +23,19 @@ android {
debuggable false
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'false'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'false'
}
debug {
debuggable true
minifyEnabled false
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'true'
}
debug_beta {
initWith release
buildConfigField 'Boolean', environmentConfig.testActionEnabled, 'true'
buildConfigField 'Boolean', environmentConfig.logEnabled, 'true'
}
}
kotlinOptions {

View file

@ -6,7 +6,9 @@ import com.tangem.common.module.ModuleException
/**
[REDACTED_AUTHOR]
*/
sealed class AddCustomTokenException(override val message: String) : Throwable(message), ModuleException {
sealed class DomainException(override val message: String) : Throwable(message), ModuleException
sealed class AddCustomTokenException(message: String) : DomainException(message) {
data class SelectTokeNetworkException(val networkId: String) : AddCustomTokenException(
"Unknown network [$networkId] should not be included in the network selection dialog."

View file

@ -0,0 +1,26 @@
package com.tangem.domain
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.state.ActionStateLoggerImpl
/**
[REDACTED_AUTHOR]
*/
object DomainLayer {
internal val actionStateLogger = ActionStateLoggerImpl()
var onInitComplete: ((DomainException?) -> Unit)? = null
fun init(data: Map<String, Any?>? = null) {
initActionStateLogger()
onInitComplete?.invoke(null)
}
private fun initActionStateLogger() {
val factory = actionStateLogger.actionStateConvertersFactory
factory.addConverter(AddCustomTokenAction::class.java, AddCustomTokenState.Converter())
}
}

View file

@ -1,61 +0,0 @@
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.StringActionConverter
import org.rekotlin.Action
class AddCustomTokenActionStateConverter : StringActionConverter<DomainState> {
private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE
private var builder: StringBuilder = StringBuilder()
override fun convert(action: Action, stateHolder: DomainState): String? {
val action = (action as? AddCustomTokenAction) ?: return null
val state = stateHolder.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
}
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

@ -18,11 +18,9 @@ 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.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.*
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.domain.redux.global.DomainGlobalState
import com.tangem.network.api.tangemTech.CoinsResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -38,6 +36,8 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
private val hubState: AddCustomTokenState
get() = domainStore.state.addCustomTokensState
override fun getReducer(): ReStoreReducer<AddCustomTokenState> = AddCustomTokenReducer(globalState)
override fun getHubState(storeState: DomainState): AddCustomTokenState = hubState
override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState {
@ -515,10 +515,17 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
}
// private suspend fun Warning.replace(replace: Boolean, to: Warning) {
// if (replace) dispatchOnMain(Warning.Replace(setOf(this), setOf(to)))
// }
@Throws
private fun throwUnAppropriateInitialization(objName: String) {
throw AddCustomTokenException.UnAppropriateInitializationException(
"AddCustomTokenHub", "$objName must be not NULL"
)
}
}
private class AddCustomTokenReducer(
private val globalState: DomainGlobalState,
) : ReStoreReducer<AddCustomTokenState> {
override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState {
return when (action) {
@ -679,10 +686,4 @@ internal class AddCustomTokenHub : BaseStoreHub<AddCustomTokenState>("AddCustomT
return state.copy(form = Form(state.form.fieldList))
}
@Throws
private fun throwUnAppropriateInitialization(objName: String) {
throw AddCustomTokenException.UnAppropriateInitializationException(
"AddCustomTokenHub", "$objName must be not NULL"
)
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.domain.features.addCustomToken.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainWrapped
import com.tangem.domain.common.extensions.SolanaAvailable
@ -10,7 +11,10 @@ import com.tangem.domain.common.extensions.SolanaTokensAvailable
import com.tangem.domain.common.form.*
import com.tangem.domain.features.addCustomToken.*
import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.*
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.state.StringActionStateConverter
import org.rekotlin.Action
import org.rekotlin.StateType
data class AddCustomTokenState(
@ -239,6 +243,58 @@ data class AddCustomTokenState(
)
}
}
class Converter : StringActionStateConverter<DomainState> {
private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE
private var builder: StringBuilder = StringBuilder()
override fun convert(action: Action, stateHolder: DomainState): String? {
val action = (action as? AddCustomTokenAction) ?: return null
val state = stateHolder.addCustomTokensState
val fieldConverter = FieldToJsonConverter(listOf(
ContractAddress,
Network,
Name,
Symbol,
Decimals,
DerivationPath,
), jsonConverter)
state.visitDataConverter(fieldConverter)
val errors = state.formErrors.map {
"${it.key}: ${it.value::class.java.simpleName}"
}
val warnings = state.warnings.map { it::class.java.simpleName }
printAction(action, state)
printStateValue("fields", fieldConverter.getConvertedData())
printStateValue("fieldErrors", toJson(errors))
printStateValue("warnings", toJson(warnings))
printStateValue("screenState", toJson(state.screenState))
printMessage("------------------------------------------------------")
val printed = builder.toString()
builder = StringBuilder()
return printed
}
private fun printStateValue(name: String, value: String) {
printMessage("$name: $value")
}
private fun printAction(action: AddCustomTokenAction, state: AddCustomTokenState) {
printMessage("action: $action, state: ${state::class.java.simpleName}")
}
private fun toJson(value: Any): String {
return jsonConverter.prettyPrint(value)
}
private fun printMessage(message: String) {
builder.append("$message\n")
}
}
}
enum class CustomTokenType {

View file

@ -1,8 +1,9 @@
package com.tangem.domain.redux
import com.tangem.domain.DomainLayer
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub
import com.tangem.domain.redux.global.DomainGlobalHub
import com.tangem.domain.redux.state.observeReducedStates
import org.rekotlin.Action
import org.rekotlin.Store
/**
@ -18,16 +19,27 @@ private val RE_STORE_HUBS: List<ReStoreHub<DomainState, *>> = listOf(
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
}
reducer = { action, state -> reduce(action, state) }
)
private fun reduce(action: Action, domainState: DomainState?): DomainState {
requireNotNull(domainState)
// we can examine the store state after each change by reducer
var assembleReducedDomainState: DomainState = domainState
val reducedStatesByAction = mutableListOf<Pair<Action, DomainState>>()
RE_STORE_HUBS.forEach {
val reducedState = it.reduce(action, assembleReducedDomainState)
assembleReducedDomainState = if (reducedState != assembleReducedDomainState) {
reducedStatesByAction.add(action to assembleReducedDomainState)
reducedState
} else {
assembleReducedDomainState
}
}
DomainLayer.actionStateLogger.log(reducedStatesByAction)
return assembleReducedDomainState
}

View file

@ -17,14 +17,13 @@ import java.util.concurrent.Executors
* a state behavior.
* All ReStoreHub's must be marked as internal
*/
internal interface ReStoreHub<StoreState, State> : HubMiddleware<StoreState>, HubReducer<StoreState>
internal interface HubMiddleware<StoreState> {
internal interface ReStoreHub<StoreState, State> {
fun getMiddleware(): Middleware<StoreState>
fun reduce(action: Action, domainState: StoreState): StoreState
}
internal interface HubReducer<StoreState> {
fun reduce(action: Action, storeState: StoreState): StoreState
internal interface ReStoreReducer<State> {
fun reduceAction(action: Action, state: State): State
}
/**
@ -87,13 +86,13 @@ internal abstract class BaseStoreHub<State>(
* Reduce the action and check it. 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
override fun reduce(action: Action, domainState: DomainState): DomainState {
val hubOldState = getHubState(domainState)
val hubNewState = getReducer().reduceAction(action, hubOldState)
return if (hubOldState === hubNewState) {
domainState
} else {
updateStoreState(storeState, newState)
updateStoreState(domainState, hubNewState)
}
}
@ -102,24 +101,13 @@ internal abstract class BaseStoreHub<State>(
}
protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback<Action>)
@Deprecated(
replaceWith = ReplaceWith("ReStoreReducer<T>"),
message = "must return a Reducer instance. The reducer must be a separate component - this closes " +
"access to the states that can be obtained from ReStoreHub"
)
abstract fun reduceAction(action: Action, state: State): State
// protected abstract fun getReducer(): ReStoreReducer<State>
protected abstract fun getReducer(): ReStoreReducer<State>
protected abstract fun getHubState(storeState: DomainState): State
protected abstract fun updateStoreState(storeState: DomainState, newHubState: State): DomainState
}
internal interface ReStoreReducer<State> {
fun reduceAction(action: Action, state: State): State
}
internal suspend inline fun ReStoreHub<*, *>.dispatchOnMain(vararg actions: Action) {
withMainContext { actions.forEach { domainStore.dispatch(it) } }
}

View file

@ -4,6 +4,7 @@ import android.webkit.ValueCallback
import com.tangem.common.extensions.toHexString
import com.tangem.domain.redux.BaseStoreHub
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.ReStoreReducer
import com.tangem.network.common.CardPublicKeyHttpInterceptor
import org.rekotlin.Action
@ -34,17 +35,24 @@ internal class DomainGlobalHub : BaseStoreHub<DomainGlobalState>("DomainGlobalHu
}
}
override fun reduceAction(action: Action, state: DomainGlobalState): DomainGlobalState = when (action) {
is DomainGlobalAction.SaveScanNoteResponse -> {
val cardPublicKeyHex = action.scanResponse.card.cardPublicKey.toHexString()
state.networkServices.tangemTechService.addHeaderInterceptors(
listOf(CardPublicKeyHttpInterceptor(cardPublicKeyHex))
)
state.copy(scanResponse = action.scanResponse)
override fun getReducer(): ReStoreReducer<DomainGlobalState> = DomainGlobalReducer()
}
private class DomainGlobalReducer : ReStoreReducer<DomainGlobalState> {
override fun reduceAction(action: Action, state: DomainGlobalState): DomainGlobalState {
return when (action) {
is DomainGlobalAction.SaveScanNoteResponse -> {
val cardPublicKeyHex = action.scanResponse.card.cardPublicKey.toHexString()
state.networkServices.tangemTechService.addHeaderInterceptors(
listOf(CardPublicKeyHttpInterceptor(cardPublicKeyHex))
)
state.copy(scanResponse = action.scanResponse)
}
is DomainGlobalAction.ShowDialog -> {
state.copy(dialog = action.stateDialog)
}
else -> state
}
is DomainGlobalAction.ShowDialog -> {
state.copy(dialog = action.stateDialog)
}
else -> state
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.domain.redux.state
import com.tangem.domain.redux.DomainState
import org.rekotlin.Action
/**
@ -9,6 +10,26 @@ interface StringStateConverter<StateHolder> {
fun convert(stateHolder: StateHolder): String
}
interface StringActionConverter<StateHolder> {
interface StringActionStateConverter<StateHolder> {
fun convert(action: Action, stateHolder: StateHolder): String?
}
class ActionStateConvertersFactory {
private val stateConverters = mutableMapOf<Class<out Action>, StringActionStateConverter<DomainState>>()
fun addConverter(classOfAction: Class<out Action>, converter: StringActionStateConverter<DomainState>) {
stateConverters[classOfAction] = converter
}
fun getConverter(action: Action): StringActionStateConverter<DomainState>? {
val converter = stateConverters.firstNotNullOfOrNull { (classOfAction, converter) ->
if (classOfAction.isAssignableFrom(action::class.java)) {
converter
} else {
null
}
} ?: return null
return converter
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.domain.redux.state
import com.tangem.domain.features.BuildConfig
import com.tangem.domain.redux.DomainState
import org.rekotlin.Action
import timber.log.Timber
/**
[REDACTED_AUTHOR]
* Use it only in debug mode!
*/
internal interface ActionStateLogger {
fun log(reducedSates: List<Pair<Action, DomainState>>)
}
internal class ActionStateLoggerImpl : ActionStateLogger {
val actionStateConvertersFactory = ActionStateConvertersFactory()
override fun log(reducedSates: List<Pair<Action, DomainState>>) {
if (!BuildConfig.LOG_ENABLED) return
logStates(reducedSates)
}
private fun logStates(reducedSates: List<Pair<Action, DomainState>>) {
reducedSates.forEach { (action, domainState) ->
val messageToPrint = actionStateConvertersFactory.getConverter(action)
?.convert(action, domainState)
?: return@forEach
Timber.d(messageToPrint)
}
}
}

View file

@ -1,36 +0,0 @@
package com.tangem.domain.redux.state
import com.tangem.domain.features.addCustomToken.AddCustomTokenActionStateConverter
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 converter = stateConverters.firstNotNullOfOrNull { entry ->
if (entry.key.isAssignableFrom(it.first::class.java)) entry.value else null
} ?: return@forEach
val messageToPrint = converter.convert(it.first, it.second) ?: return@forEach
Timber.d(messageToPrint)
}
}
// TODO: refactoring: mutate to factory
private val stateConverters = mutableMapOf(
AddCustomTokenAction::class.java to AddCustomTokenActionStateConverter()
)