Updated on 2026-08-14
This commit is contained in:
parent
608121ab50
commit
82fd854852
49 changed files with 980 additions and 117 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
typealias ValueCallback<T> = (T) -> Unit
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
1
domain/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
75
domain/build.gradle
Normal file
75
domain/build.gradle
Normal 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
21
domain/proguard-rules.pro
vendored
Normal 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
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
4
domain/src/main/AndroidManifest.xml
Normal file
4
domain/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.domain.features">
|
||||
|
||||
</manifest>
|
||||
24
domain/src/main/java/com/tangem/domain/common/DomainError.kt
Normal file
24
domain/src/main/java/com/tangem/domain/common/DomainError.kt
Normal 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?
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
package com.tangem.domain.common.extensions
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
domain/src/main/java/com/tangem/domain/common/form/Form.kt
Normal file
75
domain/src/main/java/com/tangem/domain/common/form/Form.kt
Normal 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<*>)
|
||||
}
|
||||
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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, "")
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
37
domain/src/main/java/com/tangem/domain/store/DomainStore.kt
Normal file
37
domain/src/main/java/com/tangem/domain/store/DomainStore.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
87
domain/src/main/java/com/tangem/domain/store/StoreHub.kt
Normal file
87
domain/src/main/java/com/tangem/domain/store/StoreHub.kt
Normal 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) } }
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
include ':app'
|
||||
include ':domain'
|
||||
include ':network'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue