diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index ed0df5d1f8..a663e66e7a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 7dbd7c7b3d..e4254151fc 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Any.kt b/app/src/main/java/com/tangem/tap/common/extensions/Any.kt new file mode 100644 index 0000000000..3dd4357bba --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Any.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.common.extensions + +/** +[REDACTED_AUTHOR] + */ +typealias ValueCallback = (T) -> Unit \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index f4b2863aac..f694e96ba7 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 719386cad9..02f3f9d19e 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -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> { return listOf( diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 7675e42b03..aa2218101b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 488ab3940e..697ec63bd0 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt index bb1d5c74bc..3d5b7d78b3 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt @@ -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> = moshi.adapter( Types.newParameterizedType(List::class.java, Blockchain::class.java) ) diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index a8b65e58d9..048ceb5ec4 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -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> { - return createMoshi().adapter( + return MoshiConverter.defaultMoshi().adapter( Types.newParameterizedType(List::class.java, Issuer::class.java) ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt index 67dab3645d..8180023cb9 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt @@ -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> = moshi.adapter( Types.newParameterizedType(List::class.java, SessionDao::class.java) ) diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt index 7537d54bdd..43fb11106d 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddlewares.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt index 040c72557f..04fb1f2647 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt b/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt index 15728ae713..085d35b42d 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt index 8d2577fc01..ce59c4cc1b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt index b8dd83e070..7822c5cee0 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt @@ -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() } } } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index aa5f0f8c16..2845cf4073 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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 = { 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) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index a611b435d6..8f58387acf 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 29ca1fbb3b..597cb67ebb 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index f32decfddd..065865d074 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 4004c0f96b..89b9652293 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index eaba6384c7..f3425918c9 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index 094379cc79..f04e1f018f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/network/Retrofit.kt b/app/src/main/java/com/tangem/tap/network/Retrofit.kt deleted file mode 100644 index a2188e6d64..0000000000 --- a/app/src/main/java/com/tangem/tap/network/Retrofit.kt +++ /dev/null @@ -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 = 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() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt b/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt index 5d78846280..0de58a0b32 100644 --- a/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt +++ b/app/src/main/java/com/tangem/tap/network/coinmarketcap/CoinMarketCapApi.kt @@ -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()) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index f3ca64c4db..54fd9c558e 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt index a2534b966d..15b7a73a5b 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt b/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt index 6eab298c6f..b82c9d60df 100644 --- a/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt +++ b/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt b/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt index 13d79d4591..f3b327591b 100644 --- a/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt +++ b/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt @@ -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] diff --git a/domain/.gitignore b/domain/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle new file mode 100644 index 0000000000..8d302d8eda --- /dev/null +++ b/domain/build.gradle @@ -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' +} \ No newline at end of file diff --git a/domain/proguard-rules.pro b/domain/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/domain/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt b/domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..70e3d0ada2 --- /dev/null +++ b/domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/domain/src/main/AndroidManifest.xml b/domain/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..ea7f515204 --- /dev/null +++ b/domain/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/DomainError.kt b/domain/src/main/java/com/tangem/domain/common/DomainError.kt new file mode 100644 index 0000000000..67bc6d289c --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/DomainError.kt @@ -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 { + fun convertError(error: DomainError): T +} + +interface Validator { + fun validate(data: Data? = null): Error? +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/FeatureCoroutineExceptionHandler.kt b/domain/src/main/java/com/tangem/domain/common/FeatureCoroutineExceptionHandler.kt new file mode 100644 index 0000000000..e6dde98009 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/FeatureCoroutineExceptionHandler.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/ValueDebouncer.kt b/domain/src/main/java/com/tangem/domain/common/ValueDebouncer.kt new file mode 100644 index 0000000000..c51c711131 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/ValueDebouncer.kt @@ -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( + 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) } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Coroutine.kt b/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt similarity index 89% rename from app/src/main/java/com/tangem/tap/common/extensions/Coroutine.kt rename to domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt index 57e88e4be2..0f7f23dd82 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Coroutine.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.extensions +package com.tangem.domain.common.extensions import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt b/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt new file mode 100644 index 0000000000..94127dfdc2 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt @@ -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 : Validator + +class StringIsEmptyValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = when { + data == null || data.isEmpty() -> null + else -> AddCustomTokenError.FieldIsNotEmpty + } +} + +class StringIsNotEmptyValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = when { + data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty + else -> null + } +} + +class TokenContractAddressValidator : CustomTokenValidator() { + + override fun validate(data: String?): AddCustomTokenError? = when { +// data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty + else -> EthAddressValidator().validate(data) + } + + private class EthAddressValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? { + val isValid = EthereumAddressService().validate(data ?: "") + return if (isValid) null else AddCustomTokenError.InvalidContractAddress + } + } +} + +class TokenNetworkValidator : CustomTokenValidator() { + override fun validate(data: Blockchain?): AddCustomTokenError? = when (data) { + null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected + else -> null + } +} + +class DerivationPathValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = when { + data == null || data.isEmpty() -> null + else -> { + try { + DerivationPath(data) + null + } catch (ex: HDWalletError) { + AddCustomTokenError.InvalidDerivationPath + } + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/form/Form.kt b/domain/src/main/java/com/tangem/domain/common/form/Form.kt new file mode 100644 index 0000000000..cd0a0ea261 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/form/Form.kt @@ -0,0 +1,75 @@ +package com.tangem.domain.common.form + +import com.tangem.common.json.MoshiJsonConverter + +/** +[REDACTED_AUTHOR] + */ +class Form( + val fieldList: List>, +) { + fun getField(id: FieldId): DataField<*>? = fieldList.firstOrNull { it.id == id } + + fun getData(id: FieldId): Pair? = getField(id)?.getData() + + // convert this form data whatever you want + fun getData(converter: FieldDataConverter<*>) { + fieldList.forEach { it.visitDataConverter(converter) } + } +} + +interface FieldId + +interface Field { + val id: FieldId + var value: Data + val isEnabled: Boolean + val isVisible: Boolean +} + +abstract class BaseDataField( + override val id: FieldId, + override var value: Data +) : DataField { + + override fun getData(): Pair = id to value + + override fun visitDataConverter(dataConverter: FieldDataConverter<*>) { + dataConverter.visit(getData()) + } +} + +interface FieldDataConverter : DataConverterVisitor, Result> + +abstract class BaseFieldDataConverter() : FieldDataConverter { + protected val collectIds: List = getIdToCollect() + + protected val collectedData: MutableMap = mutableMapOf() + + override fun visit(data: Pair?) { + val id = data?.first ?: return + + if (collectIds.contains(id)) { + collectedData[id] = data.second + } + } + + abstract fun getIdToCollect(): List +} + +abstract class FieldToJsonConverter( + protected val jsonConverter: MoshiJsonConverter +) : BaseFieldDataConverter() { + + override fun getConvertedData(): String = jsonConverter.toJson(collectedData) +} + +interface DataConverterVisitor { + fun visit(data: Visitor?) + fun getConvertedData(): Result +} + +interface DataField : Field { + fun getData(): Pair + fun visitDataConverter(dataConverter: FieldDataConverter<*>) +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt new file mode 100644 index 0000000000..7317484b19 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt @@ -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 { + 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))) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt new file mode 100644 index 0000000000..3503402fa8 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt @@ -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() +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt new file mode 100644 index 0000000000..bc03838408 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt @@ -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, + override val isEnabled: Boolean = true, + override val isVisible: Boolean = true, +) : BaseDataField(id, Blockchain.Unknown) + +data class TokenField( + override val id: FieldId, + override val isEnabled: Boolean = true, + override val isVisible: Boolean = true, +) : BaseDataField(id, "") + +data class TokenDerivationPathField( + override val id: FieldId, + override val isEnabled: Boolean = true, + override val isVisible: Boolean = true, +) : BaseDataField(id, "") diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt new file mode 100644 index 0000000000..7e5e9e6dc6 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt @@ -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() + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt new file mode 100644 index 0000000000..ea05c79fc7 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt @@ -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("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 + ) { + 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(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(Network, state) + val nameField = getField(Name, state) + val symbolField = getField(Symbol, state) + val decimalsField = getField(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 getField(id: FieldId, state: AddCustomTokensState): T { + return state.form.getField(id) as T + } + + private inline fun 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)) + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokensState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokensState.kt new file mode 100644 index 0000000000..45976a52d0 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokensState.kt @@ -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> = createFormValidators(), + val formErrors: Map = emptyMap(), + val warnings: List = emptyList(), + val addCustomTokenManager: AddCustomTokenManager = AddCustomTokenManager(TangemTechService()) +) : StateType { + + val completeDataType: CompleteDataType + get() = calculateDataType() + + fun getData( + converter: FieldDataConverter = CompleteData.createDataConverter(completeDataType) + ): CompleteData { + form.getData(converter) + return converter.getConvertedData() + } + + fun getLockedFieldsForKnownToken(): List { + 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> { + return listOf( + TokenField(ContractAddress), + TokenNetworkField(Network, getSupportedBlockchains()), + TokenField(Name), + TokenField(Symbol), + TokenField(Decimals), + TokenDerivationPathField(DerivationPath), + ) + } + + private fun createFormValidators(): Map> { + return mapOf( + ContractAddress to TokenContractAddressValidator(), + Network to TokenNetworkValidator(), + Name to StringIsNotEmptyValidator(), + Symbol to StringIsNotEmptyValidator(), + Decimals to StringIsNotEmptyValidator(), + DerivationPath to DerivationPathValidator(), + ) + } + + private fun getSupportedBlockchains(): List { + return Blockchain.values().filter { !it.isTestnet() }.toList() + } + } +} + +enum class CompleteDataType { + Blockchain, Token +} + +sealed class CompleteData() { + + companion object { + fun createDataConverter(completeDataType: CompleteDataType): FieldDataConverter = + when (completeDataType) { + CompleteDataType.Blockchain -> CustomBlockchain.Converter() + CompleteDataType.Token -> CustomToken.Converter() + } + } + + class CustomBlockchain( + val selectedNetwork: Blockchain, + val derivationPath: String? + ) : CompleteData() { + + class Converter : BaseFieldDataConverter() { + override fun getConvertedData(): CustomBlockchain = CustomBlockchain( + collectedData[Network] as Blockchain, + collectedData[DerivationPath] as? String, + ) + + override fun getIdToCollect(): List = 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() { + 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 = CustomTokenFieldId.values().toList() + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/store/DomainStore.kt b/domain/src/main/java/com/tangem/domain/store/DomainStore.kt new file mode 100644 index 0000000000..57e8c8401d --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/store/DomainStore.kt @@ -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> { + return listOf( + AddCustomTokenHub.middleware + ) +} + +private fun domainReduce(action: Action, state: DomainState?): DomainState { + requireNotNull(state) + + return DomainState( + addCustomTokensState = AddCustomTokenHub.reduceAction(action, state.addCustomTokensState) + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/store/StoreHub.kt b/domain/src/main/java/com/tangem/domain/store/StoreHub.kt new file mode 100644 index 0000000000..7322b42e91 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/store/StoreHub.kt @@ -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 { + val initialState: State + val middleware: Middleware + 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( + private val name: String, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO +) : StoreHub { + + protected val actionsAndJobs = mutableMapOf() + protected val hubScope = CoroutineScope( + Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name) + ) + + /** + * Main entry point for the all actions + */ + override val middleware: Middleware = { 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, + ) +} + +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) } } +} \ No newline at end of file diff --git a/domain/src/test/java/com/tangem/domain/features/ExampleUnitTest.kt b/domain/src/test/java/com/tangem/domain/features/ExampleUnitTest.kt new file mode 100644 index 0000000000..99368f65e4 --- /dev/null +++ b/domain/src/test/java/com/tangem/domain/features/ExampleUnitTest.kt @@ -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) + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index eba6dcf2b9..a6c2d48bed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ include ':app' +include ':domain' include ':network'