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

View file

@ -3,12 +3,12 @@ package com.tangem.tap
import android.content.Intent
import android.content.pm.ActivityInfo
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemSdk
import com.tangem.domain.common.FeatureCoroutineExceptionHandler
import com.tangem.operations.backup.BackupService
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.common.DialogManager
@ -27,12 +27,9 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.io.PrintWriter
import java.io.StringWriter
import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
@ -42,23 +39,13 @@ lateinit var backupService: BackupService
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
get() = Job() + Dispatchers.IO + initCoroutineExceptionHandler()
get() = Job() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope")
val scope = CoroutineScope(coroutineContext)
private val mainCoroutineContext: CoroutineContext
get() = Job() + Dispatchers.Main
get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
val mainScope = CoroutineScope(mainCoroutineContext)
private fun initCoroutineExceptionHandler(): CoroutineExceptionHandler {
return CoroutineExceptionHandler { _, throwable ->
val sw = StringWriter()
throwable.printStackTrace(PrintWriter(sw))
val exceptionAsString: String = sw.toString()
Log.e("Coroutine", exceptionAsString)
throw throwable
}
}
class MainActivity : AppCompatActivity(), SnackbarHandler {
private var snackbar: Snackbar? = null

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.network.common.MoshiConverter
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.images.PicassoHelper
import com.tangem.tap.common.redux.AppState
@ -24,7 +25,6 @@ import com.tangem.tap.features.feedback.AdditionalEmailInfo
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.feedback.TangemLogCollector
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.createMoshi
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.wallet.BuildConfig
import org.rekotlin.Store
@ -72,7 +72,7 @@ class TapApplication : Application() {
}
private fun loadConfigs() {
val moshi = createMoshi()
val moshi = MoshiConverter.defaultMoshi()
val localLoader = FeaturesLocalLoader(this, moshi)
val remoteLoader = FeaturesRemoteLoader(moshi)
val configManager = ConfigManager(localLoader, remoteLoader)

View file

@ -0,0 +1,6 @@
package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
typealias ValueCallback<T> = (T) -> Unit

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.extensions
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.domain.store.DomainState
import com.tangem.domain.store.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
@ -49,6 +51,9 @@ data class AppState(
val shopState: ShopState = ShopState(),
) : StateType {
val featuresState: DomainState
get() = domainStore.state
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(

View file

@ -3,10 +3,10 @@ package com.tangem.tap.common.redux.global
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.tap.*
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager

View file

@ -3,11 +3,11 @@ package com.tangem.tap.domain
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.ThrottlerWithValues
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository

View file

@ -9,17 +9,17 @@ import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.card.FirmwareVersion
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.extensions.appendIf
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.extensions.setCustomIconUrl
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.network.createMoshi
import timber.log.Timber
class CurrenciesRepository(val context: Application) {
private val moshi = createMoshi()
private val moshi = MoshiConverter.defaultMoshi()
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java)
)

View file

@ -10,11 +10,11 @@ import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.network.common.MoshiConverter
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.network.createMoshi
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(
@ -107,7 +107,7 @@ class TwinCardsManager(
}
private fun getAdapter(): JsonAdapter<List<Issuer>> {
return createMoshi().adapter(
return MoshiConverter.defaultMoshi().adapter(
Types.newParameterizedType(List::class.java, Issuer::class.java)
)
}

View file

@ -5,14 +5,14 @@ import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.tangem.tap.network.createMoshi
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.session.WCSession
class WalletConnectRepository(val context: Application) {
private val moshi = createMoshi()
private val moshi = MoshiConverter.defaultMoshi()
private val walletConnectAdapter: JsonAdapter<List<SessionDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, SessionDao::class.java)
)

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.demo
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.home.compose
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
@ -12,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.SpacerS8
import com.tangem.wallet.R
@Composable
@ -45,7 +48,7 @@ fun HomeButtons(
maxLines = 1
)
}
Spacer(modifier = Modifier.size(8.dp))
SpacerS8()
Button(
modifier = Modifier
.weight(1f)

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
@ -108,7 +109,7 @@ fun StoriesScreen(
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Spacer(modifier = Modifier.size(24.dp))
SpacerS24()
StoriesProgressBar(
steps = steps,
currentStep = currentStep.value,

View file

@ -22,6 +22,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.compose.SpacerS16
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.common.extensions.compose.argb
import com.tangem.wallet.R
@ -53,11 +55,11 @@ fun StoriesGeneralContent(
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(16.dp))
SpacerS16()
SubtitleText(subtitleText, subtitleTextId)
Spacer(modifier = Modifier.size(25.dp))
SpacerS24()
if (imageSource != null) {
Image(

View file

@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.SpacerV4
@Composable
fun StoriesProgressBar(
@ -67,7 +68,7 @@ fun StoriesProgressBar(
) {}
}
if (index != steps) {
Spacer(modifier = Modifier.width(4.dp))
SpacerV4()
}
}
}

View file

@ -1,12 +1,12 @@
package com.tangem.tap.features.home.redux
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.GetCardSourceParams
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.post
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
@ -50,7 +50,10 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
postUi(700) { store.dispatch(HomeAction.ReadCard) }
}
}
is HomeAction.ReadCard -> handleReadCard()
is HomeAction.ReadCard -> {
handleReadCard()
// store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomTokens))
}
is HomeAction.GoToShop -> {
when (action.regionProvider.getRegion()?.toLowerCase()) {
"ru" -> store.dispatchOpenUrl(BUY_WALLET_URL)

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.common.CompletionResult
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog

View file

@ -2,10 +2,10 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen

View file

@ -9,6 +9,7 @@ import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam

View file

@ -11,6 +11,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.Analytics

View file

@ -1,62 +0,0 @@
package com.tangem.tap.network
import com.squareup.moshi.FromJson
import com.squareup.moshi.Moshi
import com.squareup.moshi.ToJson
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.wallet.BuildConfig
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.math.BigDecimal
import java.util.concurrent.TimeUnit
fun createRetrofitInstance(
baseUrl: String,
interceptors: List<Interceptor> = emptyList(),
): Retrofit {
val okHttpBuilder = OkHttpClient.Builder()
interceptors.forEach { okHttpBuilder.addInterceptor(it) }
addTimeOuts(okHttpBuilder)
if (BuildConfig.DEBUG) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor())
return Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(createMoshiConverterFactory())
.client(okHttpBuilder.build())
.build()
}
private fun addTimeOuts(okHttpBuilder: OkHttpClient.Builder) {
okHttpBuilder.callTimeout(1, TimeUnit.SECONDS)
okHttpBuilder.connectTimeout(20, TimeUnit.SECONDS)
okHttpBuilder.readTimeout(20, TimeUnit.SECONDS)
okHttpBuilder.writeTimeout(20, TimeUnit.SECONDS)
}
fun createMoshiConverterFactory(): Converter.Factory = MoshiConverterFactory.create(createMoshi())
fun createMoshi(): Moshi = Moshi.Builder()
.add(BigDecimalAdapter)
.add(KotlinJsonAdapterFactory())
.add(TangemSdkAdapter.DerivationPathAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.build()
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
return logging
}
private object BigDecimalAdapter {
@FromJson
fun fromJson(string: String) = BigDecimal(string)
@ToJson
fun toJson(value: BigDecimal) = value.toString()
}

View file

@ -1,8 +1,7 @@
package com.tangem.tap.network.coinmarketcap
import com.tangem.tap.network.createRetrofitInstance
import com.tangem.network.common.createRetrofitInstance
import okhttp3.Interceptor
import okhttp3.Response
import retrofit2.http.GET
import retrofit2.http.Query
@ -25,18 +24,14 @@ interface CoinMarketCapApi {
fun create(apiKey: String): CoinMarketCapApi {
return createRetrofitInstance(
baseUrl,
listOf(createCoinMarketRequestInterceptor(apiKey)),
interceptors = listOf(createCoinMarketRequestInterceptor(apiKey)),
).create(CoinMarketCapApi::class.java)
}
}
}
private fun createCoinMarketRequestInterceptor(apiKey: String): Interceptor {
return object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
requestBuilder.addHeader("X-CMC_PRO_API_KEY", apiKey)
return chain.proceed(requestBuilder.build())
}
}
private fun createCoinMarketRequestInterceptor(apiKey: String): Interceptor = Interceptor { chain ->
val requestBuilder = chain.request().newBuilder()
requestBuilder.addHeader("X-CMC_PRO_API_KEY", apiKey)
chain.proceed(requestBuilder.build())
}

View file

@ -5,9 +5,9 @@ import android.util.Base64
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.network.common.createRetrofitInstance
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.network.createRetrofitInstance
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder

View file

@ -4,9 +4,9 @@ import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.network.common.createRetrofitInstance
import com.tangem.tap.common.extensions.urlEncode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.network.createRetrofitInstance
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
@ -23,8 +23,10 @@ class OnramperService(
) : ExchangeService, ExchangeUrlBuilder {
private val api: OnramperApi by lazy {
createRetrofitInstance(OnramperApi.BASE_URL, listOf(AddKeyToHeaderInterceptor(apiKey)))
.create(OnramperApi::class.java)
createRetrofitInstance(
baseUrl = OnramperApi.BASE_URL,
interceptors = listOf(AddKeyToHeaderInterceptor(apiKey))
).create(OnramperApi::class.java)
}
private var status: OnramperStatus? = null

View file

@ -3,7 +3,7 @@ package com.tangem.tap.network.payid
import com.squareup.moshi.JsonClass
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.tap.network.createRetrofitInstance
import com.tangem.network.common.createRetrofitInstance
import retrofit2.Retrofit
class PayIdService {

View file

@ -2,7 +2,7 @@ package com.tangem.tap.network.payid
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.tap.network.createRetrofitInstance
import com.tangem.network.common.createRetrofitInstance
/**
[REDACTED_AUTHOR]

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

@ -1,4 +1,4 @@
package com.tangem.tap.common.extensions
package com.tangem.domain.common.extensions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers

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

View file

@ -1,2 +1,3 @@
include ':app'
include ':domain'
include ':network'