From 608121ab509230db05fd446dd7e3ccb7b4fd709d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Apr 2022 18:52:29 +0300 Subject: [PATCH 01/28] Updated on 2026-08-14 --- network/.gitignore | 1 + network/build.gradle | 23 +++++++ .../network/api/tangemTech/Responses.kt | 50 ++++++++++++++ .../network/api/tangemTech/TangemTechApi.kt | 30 +++++++++ .../api/tangemTech/TangemTechService.kt | 66 +++++++++++++++++++ .../com/tangem/network/common/Interceptors.kt | 26 ++++++++ .../tangem/network/common/MoshiConverter.kt | 38 +++++++++++ .../com/tangem/network/common/Retrofit.kt | 41 ++++++++++++ settings.gradle | 3 +- 9 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 network/.gitignore create mode 100644 network/build.gradle create mode 100644 network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt create mode 100644 network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt create mode 100644 network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt create mode 100644 network/src/main/java/com/tangem/network/common/Interceptors.kt create mode 100644 network/src/main/java/com/tangem/network/common/MoshiConverter.kt create mode 100644 network/src/main/java/com/tangem/network/common/Retrofit.kt diff --git a/network/.gitignore b/network/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/network/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/network/build.gradle b/network/build.gradle new file mode 100644 index 0000000000..df2d688c42 --- /dev/null +++ b/network/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java-library' + id 'org.jetbrains.kotlin.jvm' +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + // Tangem sdk's + implementation 'com.tangem.tangem-sdk-kotlin:core:develop-140' + + // Network + implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3")) + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.okhttp3:logging-interceptor") + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation 'com.squareup.retrofit2:converter-moshi:2.6.0' + implementation 'com.squareup.moshi:moshi:1.13.0' + implementation "com.squareup.moshi:moshi-kotlin:1.13.0" +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt new file mode 100644 index 0000000000..6396f105be --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt @@ -0,0 +1,50 @@ +package com.tangem.network.api.tangemTech + +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +data class CoinsPricesResponse( + val prices: List +) + +data class CoinPrice( + val name: String, + val price: BigDecimal, +) + +data class CoinsCheckAddressResponse( + val imageHost: String, + val tokens: List, + val total: Int, +) { + data class Token( + val id: String, + val name: String, + val symbol: String, + val active: Boolean, + val contracts: List + ) { + data class Contract( + val networkId: String, + val address: String, + val decimalCount: BigDecimal, + val active: Boolean + ) + } +} + +data class CoinsCurrenciesResponse( + val currencies: List, +) { + data class Currency( + val id: String, + val code: String, + val name: String, + val rateBTC: String, + val unit: String, + val type: String, + ) +} + diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt new file mode 100644 index 0000000000..fbb92c73f1 --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt @@ -0,0 +1,30 @@ +package com.tangem.network.api.tangemTech + +import com.tangem.common.services.Result +import retrofit2.http.GET +import retrofit2.http.Query + +/** +[REDACTED_AUTHOR] + */ +interface TangemTechApi { + + @GET("coins/prices") + suspend fun coinsPrices( + @Query("currency") currency: String, + @Query("ids") ids: List, + ): Result + + @GET("coins/check-address") + suspend fun coinsCheckAddress( + @Query("contractAddress") contractAddress: String, + @Query("networkId") networkId: String? = null, + ): Result + + @GET("coins/currencies") + suspend fun coinsCurrencies(): Result + + @GET("coins/tokens") + suspend fun coinsTokens(): Result + +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt new file mode 100644 index 0000000000..b8e341d428 --- /dev/null +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt @@ -0,0 +1,66 @@ +package com.tangem.network.api.tangemTech + +import com.tangem.common.services.Result +import com.tangem.network.common.AddHeaderInterceptor +import com.tangem.network.common.CacheHttpInterceptor +import com.tangem.network.common.createRetrofitInstance + +/** +[REDACTED_AUTHOR] + */ +class TangemTechService { + + private val headerInterceptors = mutableListOf( + CacheHttpInterceptor(cacheMaxAge) + ) + + private var api: TangemTechApi = createApi() + + suspend fun coinsPrices( + currency: String, + ids: List + ): Result { + return api.coinsPrices(currency, ids) + } + + suspend fun coinsCheckAddress( + contractAddress: String, + networkId: String? = null + ): Result { + return api.coinsCheckAddress(contractAddress, networkId) + } + + suspend fun coinsCurrencies(): Result { + return api.coinsCurrencies() + } + + suspend fun coinsTokens(): Result { + return api.coinsTokens() + } + + fun addHeaderInterceptors(interceptors: List) { + headerInterceptors.removeAll(interceptors) + headerInterceptors.addAll(interceptors) + api = createApi() + } + + private fun createApi(): TangemTechApi { + val retrofit = createRetrofitInstance( + baseUrl = baseUrl, + interceptors = headerInterceptors.toList() + ) + + return retrofit.create(TangemTechApi::class.java) + } + + companion object { + const val baseUrl = "https://api.tangem-tech.com/" + const val cacheMaxAge = 600 + } +} + +class TangemAuthInterceptor( + private val cardPublicKeyHex: String +) : AddHeaderInterceptor( + mapOf("card_public_key" to cardPublicKeyHex) +) \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/Interceptors.kt b/network/src/main/java/com/tangem/network/common/Interceptors.kt new file mode 100644 index 0000000000..dab4451281 --- /dev/null +++ b/network/src/main/java/com/tangem/network/common/Interceptors.kt @@ -0,0 +1,26 @@ +package com.tangem.network.common + +import okhttp3.Interceptor +import okhttp3.Response + +/** +[REDACTED_AUTHOR] + */ +open class AddHeaderInterceptor( + private val headers: Map +) : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request().newBuilder().apply { + headers.forEach { + addHeader(it.key, it.value) + } + }.build() + + return chain.proceed(request) + } +} + +class CacheHttpInterceptor( + maxAgeSeconds: Int +) : AddHeaderInterceptor(mapOf("Cache-Control" to "max-age=$maxAgeSeconds")) \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/MoshiConverter.kt b/network/src/main/java/com/tangem/network/common/MoshiConverter.kt new file mode 100644 index 0000000000..1f967032e3 --- /dev/null +++ b/network/src/main/java/com/tangem/network/common/MoshiConverter.kt @@ -0,0 +1,38 @@ +package com.tangem.network.common + +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.MoshiJsonConverter +import com.tangem.common.json.TangemSdkAdapter +import retrofit2.Converter +import retrofit2.converter.moshi.MoshiConverterFactory +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +class MoshiConverter { + + companion object { + fun createFactory(moshi: Moshi = defaultMoshi()): Converter.Factory = MoshiConverterFactory.create(moshi) + + fun defaultMoshi(): Moshi = Moshi.Builder() + .add(BigDecimalAdapter) + .add(KotlinJsonAdapterFactory()) + .add(TangemSdkAdapter.DerivationPathAdapter()) + .add(TangemSdkAdapter.DerivationNodeAdapter()) + .build() + + fun sdkMoshi(): Moshi = MoshiJsonConverter.INSTANCE.moshi + } +} + +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/network/src/main/java/com/tangem/network/common/Retrofit.kt b/network/src/main/java/com/tangem/network/common/Retrofit.kt new file mode 100644 index 0000000000..d6e9614290 --- /dev/null +++ b/network/src/main/java/com/tangem/network/common/Retrofit.kt @@ -0,0 +1,41 @@ +package com.tangem.network.common + +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Converter +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit + +// TODO: refactoring: make it better through factory +fun createRetrofitInstance( + baseUrl: String, + okHttpBuilder: OkHttpClient.Builder = OkHttpClient.Builder(), + interceptors: List = emptyList(), + converterFactory: Converter.Factory = MoshiConverter.createFactory(), + logEnabled: Boolean = false +): Retrofit { + interceptors.forEach { okHttpBuilder.addInterceptor(it) } + addTimeOuts(okHttpBuilder) + + if (logEnabled) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor()) + + return Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(converterFactory) + .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) +} + +private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor { + return HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 9d495b34f8..eba6dcf2b9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,2 @@ -include ':app' \ No newline at end of file +include ':app' +include ':network' From 82fd8548520a81069f54f7779f472ae402f6e2f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Apr 2022 19:03:55 +0300 Subject: [PATCH 02/28] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 19 +-- .../java/com/tangem/tap/TapApplication.kt | 4 +- .../com/tangem/tap/common/extensions/Any.kt | 6 + .../com/tangem/tap/common/extensions/Store.kt | 1 + .../com/tangem/tap/common/redux/AppState.kt | 5 + .../common/redux/global/GlobalMidlleware.kt | 2 +- .../com/tangem/tap/domain/TapWalletManager.kt | 2 +- .../tap/domain/tokens/CurrenciesRepository.kt | 4 +- .../tap/domain/twins/TwinCardsManager.kt | 4 +- .../walletconnect/WalletConnectRepository.kt | 4 +- .../tap/features/demo/DemoMiddlewares.kt | 2 +- .../tap/features/home/compose/HomeButtons.kt | 7 +- .../tap/features/home/compose/Stories.kt | 3 +- .../home/compose/StoriesGeneralContent.kt | 6 +- .../home/compose/StoriesProgressBar.kt | 3 +- .../tap/features/home/redux/HomeMiddleware.kt | 7 +- .../note/redux/OnboardingNoteMiddleware.kt | 1 + .../redux/OnboardingOtherCardsMiddleware.kt | 2 +- .../twins/redux/TwinCardsMiddleware.kt | 1 + .../redux/OnboardingWalletMiddleware.kt | 2 +- .../send/redux/middlewares/SendMiddleware.kt | 1 + .../redux/middlewares/WalletMiddleware.kt | 1 + .../java/com/tangem/tap/network/Retrofit.kt | 62 ------- .../network/coinmarketcap/CoinMarketCapApi.kt | 17 +- .../moonpay/MoonPayService.kt | 2 +- .../onramper/OnramperService.kt | 8 +- .../tangem/tap/network/payid/PayIdService.kt | 2 +- .../tap/network/payid/PayIdVerifyService.kt | 2 +- domain/.gitignore | 1 + domain/build.gradle | 75 +++++++++ domain/proguard-rules.pro | 21 +++ .../features/ExampleInstrumentedTest.kt | 22 +++ domain/src/main/AndroidManifest.xml | 4 + .../com/tangem/domain/common/DomainError.kt | 24 +++ .../FeatureCoroutineExceptionHandler.kt | 23 +++ .../tangem/domain/common/ValueDebouncer.kt | 41 +++++ .../domain}/common/extensions/Coroutine.kt | 2 +- .../domain/common/form/FieldsValidators.kt | 63 +++++++ .../com/tangem/domain/common/form/Form.kt | 75 +++++++++ .../addCustomToken/AddCustomTokenManager.kt | 31 ++++ .../domain/features/addCustomToken/Errors.kt | 19 +++ .../features/addCustomToken/FormFields.kt | 36 ++++ .../redux/AddCustomTokenAction.kt | 43 +++++ .../addCustomToken/redux/AddCustomTokenHub.kt | 159 ++++++++++++++++++ .../redux/AddCustomTokensState.kt | 137 +++++++++++++++ .../com/tangem/domain/store/DomainStore.kt | 37 ++++ .../java/com/tangem/domain/store/StoreHub.kt | 87 ++++++++++ .../tangem/domain/features/ExampleUnitTest.kt | 16 ++ settings.gradle | 1 + 49 files changed, 980 insertions(+), 117 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/extensions/Any.kt delete mode 100644 app/src/main/java/com/tangem/tap/network/Retrofit.kt create mode 100644 domain/.gitignore create mode 100644 domain/build.gradle create mode 100644 domain/proguard-rules.pro create mode 100644 domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt create mode 100644 domain/src/main/AndroidManifest.xml create mode 100644 domain/src/main/java/com/tangem/domain/common/DomainError.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/FeatureCoroutineExceptionHandler.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/ValueDebouncer.kt rename {app/src/main/java/com/tangem/tap => domain/src/main/java/com/tangem/domain}/common/extensions/Coroutine.kt (89%) create mode 100644 domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/form/Form.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokensState.kt create mode 100644 domain/src/main/java/com/tangem/domain/store/DomainStore.kt create mode 100644 domain/src/main/java/com/tangem/domain/store/StoreHub.kt create mode 100644 domain/src/test/java/com/tangem/domain/features/ExampleUnitTest.kt 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' From 103e4249d7a9b87e5cd8db555bdcfb4bc40f1ff1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Apr 2022 19:05:16 +0300 Subject: [PATCH 03/28] Updated on 2026-08-14 --- app/build.gradle | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index cbbf668865..13580010f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,6 +68,9 @@ repositories { dependencies { implementation fileTree(include: ['*.aar'], dir: 'libs') + implementation implementation(project(path: ':domain')) + // TODO: refactoring: only for backwards compatibility with non-relocated services to the network module + implementation implementation(project(path: ':network')) implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.4.1' @@ -120,6 +123,7 @@ dependencies { implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' // Network and Json + //TODO: refactoring: remove it when all network services moved to the 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' @@ -143,10 +147,10 @@ dependencies { //Compose implementation 'androidx.activity:activity-compose:1.4.0' - implementation 'androidx.compose.material:material:1.1.0' - implementation 'androidx.compose.animation:animation:1.1.0' - implementation 'androidx.compose.ui:ui-tooling:1.1.0' - implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0" + implementation "androidx.compose.material:material:1.1.1" + implementation 'androidx.compose.animation:animation:1.1.1' + implementation 'androidx.compose.ui:ui-tooling:1.1.1' + implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.1" implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6' From 1290fcbc3f45a616496389dda3292675f3665f08 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Apr 2022 19:06:35 +0300 Subject: [PATCH 04/28] Updated on 2026-08-14 --- .../com/tangem/tap/common/compose/Spacer.kt | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/Spacer.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt new file mode 100644 index 0000000000..c1d9a7de9a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt @@ -0,0 +1,91 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** +[REDACTED_AUTHOR] + */ +// ***************************** Horizontal +@Composable +fun SpacerH(height: Dp) { + Spacer(modifier = Modifier.height(height)) +} + +@Composable +fun SpacerH4() { + SpacerH(4.dp) +} + +@Composable +fun SpacerH8() { + SpacerH(8.dp) +} + +@Composable +fun SpacerH16() { + SpacerH(16.dp) +} + +@Composable +fun SpacerH24() { + SpacerH(24.dp) +} + +// ***************************** Vertical +@Composable +fun SpacerV(width: Dp) { + Spacer(modifier = Modifier.width(width)) +} + +@Composable +fun SpacerV4() { + SpacerV(4.dp) +} + +@Composable +fun SpacerV8() { + SpacerV(8.dp) +} + +@Composable +fun SpacerV16() { + SpacerV(16.dp) +} + +@Composable +fun SpacerV24() { + SpacerV(24.dp) +} + +// ***************************** Size +@Composable +fun SpacerS(size: Dp) { + Spacer(modifier = Modifier.size(size)) +} + +@Composable +fun SpacerS4() { + SpacerS(4.dp) +} + +@Composable +fun SpacerS8() { + SpacerS(8.dp) +} + +@Composable +fun SpacerS16() { + SpacerS(16.dp) +} + +@Composable +fun SpacerS24() { + SpacerS(24.dp) +} \ No newline at end of file From fcf3237c595956aac041dcd74e79590506ac8961 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Apr 2022 19:08:34 +0300 Subject: [PATCH 05/28] Updated on 2026-08-14 --- .../com/tangem/tap/common/compose/Button.kt | 69 ++++++++ .../common/compose/ComposableTextDebouncer.kt | 26 +++ .../tap/common/compose/OutlinedSpinner.kt | 78 +++++++++ .../common/compose/OutlinedTextFieldWidget.kt | 153 ++++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/Button.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt new file mode 100644 index 0000000000..dc5d278520 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -0,0 +1,69 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.height +import androidx.compose.material.Button +import androidx.compose.material.Scaffold +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** +[REDACTED_AUTHOR] + */ +private class Button {} + +@Composable +fun Button( + text: String = "", + textId: Int? = null, + modifier: Modifier = Modifier, + leftContent: @Composable RowScope.() -> Unit = {}, + rightContent: @Composable RowScope.() -> Unit = {}, + onClick: () -> Unit, +) { + Button( + modifier = modifier.height(42.dp), + onClick = onClick, + ) { + leftContent() + ButtonText(text = textId?.let { stringResource(id = it) } ?: text) + rightContent() + } +} + +@Composable +fun ButtonText( + text: String, + modifier: Modifier = Modifier +) { + Text( + text, + modifier = modifier, + maxLines = 1, + style = TextStyle( + fontSize = 16.sp, + lineHeight = 20.sp, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center, + ) + ) +} + +@Preview +@Composable +fun ButtonTest() { + Scaffold { + Button( + "Some button", + onClick = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt new file mode 100644 index 0000000000..7420dce292 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.common.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import com.tangem.domain.common.ValueDebouncer + +/** +[REDACTED_AUTHOR] + * This is an empty compose view. It just remember the ValueDebouncer inside of itself. + */ +@Composable +fun ComposableTextDebouncer( + text: String, + debounce: Long = 400, + onTextChanged: (String) -> Unit +): ValueDebouncer { + val rTextDebounce = remember { + mutableStateOf(ValueDebouncer(text, debounce) { changedValue -> + changedValue?.let { onTextChanged(it) } + }) + } + rTextDebounce.value.value = text + + return rTextDebounce.value +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt new file mode 100644 index 0000000000..7be01ec1d0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt @@ -0,0 +1,78 @@ +package com.tangem.tap.common.compose + +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.VoidCallback +import com.tangem.tap.common.extensions.ValueCallback + +/** +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun OutlinedSpinner( + title: String, + itemList: List, + selectedItem: T, + onItemSelected: ValueCallback, + modifier: Modifier = Modifier, + itemNameConverter: (T) -> String = { it.toString() }, + onClose: VoidCallback = {} +) { + val rSelectedItem = remember { mutableStateOf(selectedItem) } + val rIsExpanded = remember { mutableStateOf(false) } + + val onItemSelectedInternal: (T) -> Unit = { + rSelectedItem.value = it + rIsExpanded.value = false + onItemSelected(it) + } + val onDismissRequest = { + rIsExpanded.value = false + onClose() + } + + ExposedDropdownMenuBox( + expanded = rIsExpanded.value, + onExpandedChange = { rIsExpanded.value = !rIsExpanded.value }, + ) { + OutlinedTextField( + modifier = modifier, + readOnly = true, + value = itemNameConverter(rSelectedItem.value), + onValueChange = {}, + label = { Text(title) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, + ) + ExposedDropdownMenu( + expanded = rIsExpanded.value, + onDismissRequest = onDismissRequest, + ) { + itemList.forEach { item -> + DropdownMenuItem( + onClick = { onItemSelectedInternal(item) } + ) { + Text(itemNameConverter(item)) + } + } + } + } +} + +@Preview +@Composable +fun TestSpinnerPreview(){ + Scaffold() { + OutlinedSpinner( + title = "Blockchain name", + itemList = listOf(Blockchain.values()), + selectedItem = Blockchain.Avalanche, + onItemSelected = {}, + ) + } +} diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt new file mode 100644 index 0000000000..8ed5cbb2d9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -0,0 +1,153 @@ +package com.tangem.tap.common.compose + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.domain.common.DomainError +import com.tangem.domain.common.ErrorConverter + +/** +[REDACTED_AUTHOR] + */ +private class OutlinedTextFieldWidget + +@Composable +fun OutlinedTextFieldWidget( + text: String, + modifier: Modifier = Modifier, + labelId: Int? = null, + label: String = "", + placeholderId: Int? = null, + placeholder: String = "", + isEnabled: Boolean = true, + error: DomainError? = null, + errorConverter: ErrorConverter? = null, + debounceTextChanges: Long = 400, + onTextChanged: (String) -> Unit, +) { + val placeholder = placeholderId?.let { stringResource(id = it) } ?: placeholder + val label = labelId?.let { stringResource(id = it) } ?: label + + val rTextValue = remember { mutableStateOf(text) } + val textDebouncer = ComposableTextDebouncer(text, debounceTextChanges, onTextChanged) + + Column( + modifier = modifier.animateContentSize(), + ) { + OutlinedTextField( + value = rTextValue.value, + onValueChange = { + rTextValue.value = it + textDebouncer.emmit(it) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(label) }, + placeholder = { Text(placeholder) }, + singleLine = true, + enabled = isEnabled, + isError = error != null, + ) + errorConverter?.let { TextFieldErrorWidget(error, it) } + } +} + +@Composable +fun TextFieldErrorWidget( + error: DomainError? = null, + errorConverter: ErrorConverter, +) { + AnimatedVisibility( + visible = error != null, + enter = fadeIn() + slideInVertically(), + exit = slideOutVertically() + fadeOut(), + ) { + ErrorView( + errorConverter.convertError(error!!), + style = TextStyle( + fontSize = 14.sp + ) + ) + } +} + +@Composable +fun ErrorView( + text: String, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current +) { + Text( + text, + color = MaterialTheme.colors.error, + modifier = modifier, + style = style + ) +} + + +@Preview +@Composable +fun OutlinedTextFieldWithErrorTest() { + val converter = remember { + object : ErrorConverter { + override fun convertError(error: DomainError): String { + return "Hello, i'am the error: ${error::class.java.simpleName}" + } + + } + } + + class SimpleError( + override val code: Int = 1, + override val message: String = "Error message", + override val data: Any? = null, + ) : DomainError + + val modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + Scaffold( + ) { + Column( + ) { + OutlinedTextFieldWidget( + modifier = modifier, + text = "", + label = "First label", + placeholder = "1 placeholder", + error = null, + errorConverter = converter, + onTextChanged = {}, + ) + OutlinedTextFieldWidget( + modifier = modifier, + text = "First", + label = "First label", + placeholder = "1 placeholder", + error = null, + errorConverter = converter, + onTextChanged = {}, + ) + OutlinedTextFieldWidget( + modifier = modifier, + text = "First", + label = "First label", + placeholder = "1 placeholder", + error = SimpleError(), + errorConverter = converter, + onTextChanged = {}, + ) + } + } +} \ No newline at end of file From 7f9575b9c9d9d75140fd0946d575f7fa52f22805 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Apr 2022 13:02:03 +0300 Subject: [PATCH 06/28] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/common/compose/Log.kt | 12 ++++++++++++ .../tap/common/compose/OutlinedTextFieldWidget.kt | 3 +++ 2 files changed, 15 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/Log.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/Log.kt b/app/src/main/java/com/tangem/tap/common/compose/Log.kt new file mode 100644 index 0000000000..c55fda6f4b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Log.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.common.compose + +import androidx.compose.runtime.Composable +import timber.log.Timber + +/** + * Simple logger for all recompositions + */ +@Composable +fun LogSideEffect(message: String) { + Timber.w("SideEffect: $message") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index 8ed5cbb2d9..44f5ab1634 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -34,6 +35,7 @@ fun OutlinedTextFieldWidget( error: DomainError? = null, errorConverter: ErrorConverter? = null, debounceTextChanges: Long = 400, + visualTransformation: VisualTransformation = VisualTransformation.None, onTextChanged: (String) -> Unit, ) { val placeholder = placeholderId?.let { stringResource(id = it) } ?: placeholder @@ -57,6 +59,7 @@ fun OutlinedTextFieldWidget( singleLine = true, enabled = isEnabled, isError = error != null, + visualTransformation = visualTransformation, ) errorConverter?.let { TextFieldErrorWidget(error, it) } } From 1a2d0bfc01eee3497a2f78f42b1ab71ece5b5576 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Apr 2022 13:02:57 +0300 Subject: [PATCH 07/28] Updated on 2026-08-14 --- .../main/java/com/tangem/network/api/tangemTech/Responses.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt index 6396f105be..f1a5a921a9 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt @@ -29,7 +29,7 @@ data class CoinsCheckAddressResponse( data class Contract( val networkId: String, val address: String, - val decimalCount: BigDecimal, + val decimalCount: BigDecimal?, val active: Boolean ) } From 5f0b16e1ee95334b4a0759cb4fd65eec768075b4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Apr 2022 13:32:11 +0300 Subject: [PATCH 08/28] Updated on 2026-08-14 --- .../com/tangem/tap/common/extensions/Store.kt | 2 +- .../com/tangem/tap/domain/TangemSdkManager.kt | 2 +- .../com/tangem/tap/domain/TapWalletManager.kt | 7 +- .../com/tangem/tap/domain/TapWorkarounds.kt | 78 ------------------- .../com/tangem/tap/domain/extensions/Card.kt | 31 +++++--- .../domain/extensions/WalletManagerFactory.kt | 9 ++- .../tasks/product/CreateProductWalletTask.kt | 10 +-- .../domain/tasks/product/ScanProductTask.kt | 55 +------------ .../twins/CreateSecondTwinWalletTask.kt | 1 + .../tap/domain/twins/FinalizeTwinTask.kt | 2 +- .../tap/domain/twins/TwinCardsManager.kt | 2 +- .../tap/domain/twins/TwinCardsWidget.kt | 1 + .../tangem/tap/features/demo/DemoHelper.kt | 2 +- .../tap/features/demo/DemoMiddlewares.kt | 2 +- .../tangem/tap/features/demo/Extentions.kt | 2 +- .../features/details/redux/DetailsAction.kt | 4 +- .../features/details/redux/DetailsReducer.kt | 10 +-- .../features/details/redux/DetailsState.kt | 2 +- .../walletconnect/WalletConnectAction.kt | 2 +- .../walletconnect/WalletConnectMiddleware.kt | 6 +- .../redux/walletconnect/WalletConnectState.kt | 2 +- .../features/details/ui/DetailsFragment.kt | 4 +- .../tap/features/feedback/FeedbackManager.kt | 4 +- .../tap/features/home/redux/HomeMiddleware.kt | 2 +- .../features/onboarding/OnboardingHelper.kt | 4 +- .../features/onboarding/OnboardingManager.kt | 2 +- .../note/redux/OnboardingNoteMiddleware.kt | 2 +- .../redux/OnboardingOtherCardsMiddleware.kt | 2 +- .../products/twins/redux/TwinCardsAction.kt | 2 +- .../twins/redux/TwinCardsMiddleware.kt | 4 +- .../products/twins/redux/TwinCardsReducer.kt | 2 +- .../products/twins/redux/TwinCardsState.kt | 2 +- .../products/twins/ui/TwinsCardsFragment.kt | 2 +- .../redux/OnboardingWalletMiddleware.kt | 2 +- .../send/redux/middlewares/SendMiddleware.kt | 4 +- .../features/tokens/redux/TokensMiddleware.kt | 12 +-- .../redux/middlewares/WarningsMiddleware.kt | 6 +- .../wallet/redux/reducers/WalletReducer.kt | 2 +- .../wallet/ui/wallet/SingleWalletView.kt | 2 +- .../com/tangem/domain/common/ScanResponse.kt | 60 ++++++++++++++ .../tangem/domain/common/TapWorkarounds.kt | 78 +++++++++++++++++++ .../com/tangem/domain/common}/TwinsHelper.kt | 2 +- .../global/redux/DomainGlobalAction.kt | 12 +++ .../features/global/redux/DomainGlobalHub.kt | 43 ++++++++++ .../global/redux/DomainGlobalState.kt | 11 +++ 45 files changed, 294 insertions(+), 204 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/ScanResponse.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt rename {app/src/main/java/com/tangem/tap/domain/twins => domain/src/main/java/com/tangem/domain/common}/TwinsHelper.kt (98%) create mode 100644 domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalAction.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalHub.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalState.kt 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 f694e96ba7..7a0488d1a5 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,11 +1,11 @@ package com.tangem.tap.common.extensions +import com.tangem.domain.common.ScanResponse 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 import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index d4ac60c730..61dbbe7f32 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -17,6 +17,7 @@ import com.tangem.common.core.Config import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.ScanResponse import com.tangem.operations.CommandResponse import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask @@ -30,7 +31,6 @@ import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.features.demo.DemoHelper import com.tangem.wallet.R 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 697ec63bd0..f904a0331d 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -3,6 +3,9 @@ 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.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.common.ThrottlerWithValues import com.tangem.tap.common.extensions.dispatchOnMain @@ -11,12 +14,10 @@ import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.currenciesRepository -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.configurable.config.ConfigManager +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.domain.extensions.makeWalletManagersForApp -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.CardCurrencies import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.models.PendingTransactionType diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt deleted file mode 100644 index c0f9c1e239..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ /dev/null @@ -1,78 +0,0 @@ -package com.tangem.tap.domain - -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.Card -import com.tangem.common.card.EllipticCurve -import com.tangem.common.card.FirmwareVersion -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin -import com.tangem.tap.domain.TapWorkarounds.isTangemNote -import com.tangem.tap.domain.extensions.getSingleWallet -import com.tangem.tap.domain.twins.isTangemTwin -import java.util.* - -object TapWorkarounds { - - fun isStart2CoinIssuer(cardIssuer: String?): Boolean { - return cardIssuer?.toLowerCase(Locale.US) == START_2_COIN_ISSUER - } - - val Card.isStart2Coin: Boolean - get() = isStart2CoinIssuer(issuer.name) - - val Card.isTestCard: Boolean - get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) - - fun Card.isExcluded(): Boolean { - val excludedBatch = excludedBatches.contains(batchId) - val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) - return excludedBatch || excludedIssuerName - } - - fun Card.isNotSupportedInThatRelease():Boolean { - return false - } - - @Deprecated("Use ScanResponse.isTangemNote") - fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId) - - fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId) - - fun getTangemNoteBlockchain(card: Card): Blockchain? = tangemNoteBatches[card.batchId] - - private const val START_2_COIN_ISSUER = "start2coin" - private const val TEST_CARD_BATCH = "99FF" - private const val TEST_CARD_ID_STARTS_WITH = "FF99" - - private val excludedBatches = listOf( - "0027", - "0030", - "0031", - "0035" - ) - - private val excludedIssuers = listOf( - "TTM BANK" - ) - - private val tangemWalletBatches = listOf("AC01") - - private val tangemNoteBatches = mapOf( - "AB01" to Blockchain.Bitcoin, - "AB02" to Blockchain.Ethereum, - "AB03" to Blockchain.CardanoShelley, - "AB04" to Blockchain.Dogecoin, - "AB05" to Blockchain.BSC, - "AB06" to Blockchain.XRP, - "AB07" to Blockchain.Bitcoin, - "AB08" to Blockchain.Ethereum, - ) -} - -val DELAY_SDK_DIALOG_CLOSE = 1400L - -val Card.isMultiwalletAllowed: Boolean - get() { - return !isTangemTwin() && !isStart2Coin && !isTangemNote(this) - && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || - getSingleWallet()?.curve == EllipticCurve.Secp256k1) - } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 124ae27387..0dc1819018 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt @@ -2,14 +2,33 @@ package com.tangem.tap.domain.extensions import com.tangem.common.card.Card import com.tangem.common.card.CardWallet +import com.tangem.common.card.EllipticCurve +import com.tangem.common.card.FirmwareVersion import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result +import com.tangem.domain.common.TapWorkarounds +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.common.isTangemTwin import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.tap.domain.twins.TwinCardNumber -import com.tangem.tap.domain.twins.getTwinCardNumber import com.tangem.tap.features.wallet.redux.Artwork + +val Card.remainingSignatures: Int? + get() = this.getSingleWallet()?.remainingSignatures + +val Card.isWalletDataSupported: Boolean + get() = this.firmwareVersion.major >= 4 + +val Card.isMultiwalletAllowed: Boolean + get() { + return !isTangemTwin() && !isStart2Coin && !TapWorkarounds.isTangemNote(this) + && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || + getSingleWallet()?.curve == EllipticCurve.Secp256k1) + } + fun Card.getSingleWallet(): CardWallet? { return wallets.firstOrNull() } @@ -66,10 +85,4 @@ fun Card.getArtworkUrl(artworkId: String?): String? { cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL else -> null } -} - -val Card.remainingSignatures: Int? - get() = this.getSingleWallet()?.remainingSignatures - -val Card.isWalletDataSupported: Boolean - get() = this.firmwareVersion.major >= 4 \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index 3b58ec418e..a9682d11c5 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -7,11 +7,12 @@ import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey -import com.tangem.tap.domain.TapWorkarounds.isTestCard -import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isTestCard fun WalletManagerFactory.makeWalletManagerForApp( - scanResponse: ScanResponse, blockchain: Blockchain + scanResponse: ScanResponse, + blockchain: Blockchain ): WalletManager? { val card = scanResponse.card if (card.isTestCard && blockchain.getTestnetVersion() == null) return null @@ -27,7 +28,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( scanResponse.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> { makeTwinWalletManager( card.cardId, - wallet.publicKey, scanResponse.secondTwinPublicKey.hexToBytes(), + wallet.publicKey, scanResponse.secondTwinPublicKey!!.hexToBytes(), environmentBlockchain, wallet.curve ) } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 1d46b341ec..0750ddbbab 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -9,6 +9,10 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.guard import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.KeyWalletPublicKey +import com.tangem.domain.common.ProductType +import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.operations.CommandResponse import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask @@ -16,11 +20,7 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask -import com.tangem.tap.domain.ProductType -import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.tasks.product.CreateWalletsTask -import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey import com.tangem.tap.domain.tasks.product.ProductCommandProcessor import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets import com.tangem.tap.features.demo.DemoHelper @@ -78,7 +78,7 @@ private class CreateWalletTangemNote : ProductCommandProcessor = mapOf(), - val primaryCard: PrimaryCard? = null -) : CommandResponse { - - fun getBlockchain(): Blockchain { - if (productType == ProductType.Note) return getTangemNoteBlockchain(card) - ?: return Blockchain.Unknown - val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown - return Blockchain.fromId(blockchainName) - } - - fun getPrimaryToken(): Token? { - val cardToken = walletData?.token ?: return null - return Token( - cardToken.name, - cardToken.symbol, - cardToken.contractAddress, - cardToken.decimals, - Blockchain.fromId(walletData.blockchain) - ) - } - - fun isTangemNote(): Boolean = productType == ProductType.Note - fun isTangemWallet(): Boolean = productType == ProductType.Wallet - fun isTangemTwins(): Boolean = productType == ProductType.Twins - - fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed - fun supportsBackup(): Boolean = card.settings.isBackupAllowed - - fun twinsIsTwinned(): Boolean = - card.isTangemTwins() && walletData != null && secondTwinPublicKey != null -} - -typealias KeyWalletPublicKey = ByteArrayKey - class ScanProductTask( val card: Card? = null, private val currenciesRepository: CurrenciesRepository?, @@ -132,8 +85,6 @@ class ScanProductTask( } } -private fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != null - private class ScanNoteProcessor : ProductCommandProcessor { override fun proceed( card: Card, diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt index f7ef809d38..85c03925ff 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt @@ -8,6 +8,7 @@ import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemError import com.tangem.common.extensions.hexToBytes +import com.tangem.domain.common.TwinsHelper import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask import com.tangem.operations.wallet.PurgeWalletCommand diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 51ade7b4c3..73b9e2f40a 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -4,10 +4,10 @@ import com.tangem.common.CompletionResult import com.tangem.common.KeyPair import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable +import com.tangem.domain.common.ScanResponse import com.tangem.operations.PreflightReadMode import com.tangem.operations.PreflightReadTask import com.tangem.tap.domain.tasks.product.ScanProductTask -import com.tangem.tap.domain.tasks.product.ScanResponse class FinalizeTwinTask( private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair, 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 048ceb5ec4..43c91a54fb 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.domain.common.ScanResponse 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.tangemSdkManager class TwinCardsManager( diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt index 3a163149e5..36d3b037ff 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsWidget.kt @@ -6,6 +6,7 @@ import android.animation.PropertyValuesHolder import android.view.View import androidx.core.animation.doOnEnd import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.TwinCardNumber import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 8754f68655..24e8671c33 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.CompletionResult +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.wallet.redux.WalletAction 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 43fb11106d..c7d9cfbc78 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,9 +1,9 @@ package com.tangem.tap.features.demo import com.tangem.common.extensions.guard +import com.tangem.domain.common.ScanResponse 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 import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.ProgressState diff --git a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt index a863282f6d..99ecfbc227 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.demo import com.tangem.blockchain.common.WalletManager -import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.domain.common.ScanResponse /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 4cf2a6fde3..1c087a518a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -2,12 +2,12 @@ package com.tangem.tap.features.details.redux import com.tangem.blockchain.common.Wallet import com.tangem.common.card.Card +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TwinCardNumber import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.redux.global.FiatCurrencyName -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.termsOfUse.CardTou -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.network.coinmarketcap.FiatCurrency import com.tangem.wallet.R import org.rekotlin.Action diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 7ea35d3284..848a5f4a8e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -2,12 +2,12 @@ package com.tangem.tap.features.details.redux import com.tangem.common.card.Card +import com.tangem.domain.common.TapWorkarounds.isStart2Coin +import com.tangem.domain.common.isTangemTwin import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin import com.tangem.tap.domain.extensions.isWalletDataSupported import com.tangem.tap.domain.extensions.signedHashesCount import com.tangem.tap.domain.extensions.toSendableAmounts -import com.tangem.tap.domain.twins.isTangemTwin import com.tangem.tap.features.wallet.models.hasPendingTransactions import org.rekotlin.Action import java.util.* @@ -64,8 +64,8 @@ private fun handleEraseWallet( val card = state.scanResponse?.card val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false val notAllowedByCard = notAllowedByAnyWallet || - (card?.isWalletDataSupported == true && - (!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup())) + (card?.isWalletDataSupported == true && + (!state.scanResponse.isTangemNote() && !state.scanResponse.supportsBackup())) val notEmpty = state.wallets.any { it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty() @@ -136,7 +136,7 @@ private fun handleSecurityAction( is DetailsAction.ManageSecurity.OpenSecurity -> { val allowedSecurityOptions = when { state.scanResponse?.card?.isStart2Coin == true || - state.scanResponse?.isTangemNote() == true -> { + state.scanResponse?.isTangemNote() == true -> { EnumSet.of(SecurityOption.LongTap) } state.scanResponse?.supportsBackup() == true -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 8f55ff5901..48d8ded881 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -2,10 +2,10 @@ package com.tangem.tap.features.details.redux import android.net.Uri import com.tangem.blockchain.common.Wallet +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY import com.tangem.tap.common.redux.global.FiatCurrencyName -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.network.coinmarketcap.FiatCurrency import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 30b6af3508..a180d20d4a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.details.redux.walletconnect import android.app.Activity +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.NotificationAction -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.wallet.R import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 6db0bf1a97..384b459886 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -3,6 +3,8 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.guard +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.tap.common.redux.AppState @@ -10,10 +12,8 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.currenciesRepository -import com.tangem.tap.domain.TapWorkarounds.isTestCard +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.extensions.makeWalletManagerForApp -import com.tangem.tap.domain.isMultiwalletAllowed -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.walletconnect.BnbHelper import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index fdecb27416..0039e247ee 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -5,8 +5,8 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.StateDialog -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData import com.trustwallet.walletconnect.models.WCPeerMeta diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 5df5d5969b..781ba05548 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -7,12 +7,12 @@ import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding +import com.tangem.domain.common.getTwinCardIdForUser import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.isMultiwalletAllowed -import com.tangem.tap.domain.twins.getTwinCardIdForUser +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.SecurityOption diff --git a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt index e3d88e1b99..3da5648caa 100644 --- a/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/features/feedback/FeedbackManager.kt @@ -7,10 +7,10 @@ import android.os.Build import com.tangem.Log import com.tangem.TangemSdkLogger import com.tangem.blockchain.common.* +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.domain.TapWorkarounds -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.wallet.R import timber.log.Timber import java.io.File 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 2845cf4073..e4472de543 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,6 +1,7 @@ package com.tangem.tap.features.home.redux import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsParam import com.tangem.tap.common.analytics.GetCardSourceParams @@ -14,7 +15,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.FragmentShareTransition import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 258701cabc..7efb84deb6 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.onboarding +import com.tangem.domain.common.ProductType +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.navigation.AppScreen -import com.tangem.tap.domain.ProductType import com.tangem.tap.domain.extensions.hasWallets -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.preferencesStorage /** diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index 9c3037a33c..7f44e7c276 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -5,13 +5,13 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.isZero import com.tangem.common.services.Result +import com.tangem.domain.common.ScanResponse import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.extensions.isPositive import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.wallet.models.hasPendingTransactions import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.ProgressState 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 8f58387acf..a913a7a7cb 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 @@ -3,6 +3,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.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.* import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppDialog @@ -10,7 +11,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.domain.extensions.makePrimaryWalletManager 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 597cb67ebb..0f854da103 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 @@ -2,13 +2,13 @@ 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.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.onCardScanned import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.extensions.hasWallets import com.tangem.tap.scope import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt index 766930141a..ee4bd5ddab 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt @@ -3,9 +3,9 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.Message import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.twins.AssetReader import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance 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 065865d074..a7130796dc 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,7 +3,9 @@ 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.ScanResponse import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.extensions.* import com.tangem.tap.common.postUi import com.tangem.tap.common.redux.AppDialog @@ -11,10 +13,8 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makePrimaryWalletManager -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.ProgressState diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt index 437213c588..d482a5a647 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux +import com.tangem.domain.common.getTwinCardNumber import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.twins.getTwinCardNumber import org.rekotlin.Action class TwinCardsReducer { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index eb05dd7402..fc24dbd461 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -1,9 +1,9 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.buyIsAllowed -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index b1c97b64ee..38809f8d57 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -13,6 +13,7 @@ import com.squareup.picasso.Picasso import com.tangem.Message import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.TwinCardNumber import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.navigation.AppScreen @@ -21,7 +22,6 @@ import com.tangem.tap.common.redux.navigation.ShareElement import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.domain.twins.AssetReader -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment 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 89b9652293..4050cd2b20 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,6 +2,7 @@ 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.ScanResponse import com.tangem.domain.common.extensions.withMainContext import com.tangem.operations.backup.BackupService import com.tangem.tap.* @@ -11,7 +12,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.extensions.hasWallets -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.wallet.redux.Artwork 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 f3425918c9..9802cfff3a 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,7 +9,9 @@ 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.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext +import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsParam @@ -18,10 +20,8 @@ import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.extensions.minimalAmount import com.tangem.tap.features.demo.DemoTransactionSender diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index abff50485f..8f74c20cf1 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -7,22 +7,18 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.common.KeyWalletPublicKey +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.operations.derivation.ExtendedPublicKeysMap +import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.currenciesRepository -import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.TapWorkarounds.isTestCard -import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index e8ea067b6f..94ee1e747e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -6,17 +6,17 @@ import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.extensions.isGreaterThan import com.tangem.tap.common.redux.global.GlobalState -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.extensions.getSingleWallet import com.tangem.tap.domain.extensions.hasSignedHashes +import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.extensions.remainingSignatures -import com.tangem.tap.domain.isMultiwalletAllowed -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index 20bbe52092..169796bce0 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux.reducers import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Wallet +import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFiatValue import com.tangem.tap.common.extensions.toFormattedCurrencyString @@ -12,7 +13,6 @@ import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.getArtworkUrl import com.tangem.tap.domain.getFirstToken -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.wallet.redux.* import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index 224f717a2a..a26f2582fb 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -5,12 +5,12 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import androidx.recyclerview.widget.LinearLayoutManager +import com.tangem.domain.common.TwinCardNumber import com.tangem.tap.common.extensions.beginDelayedTransition import com.tangem.tap.common.extensions.fitChipsByGroupWidth import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.StateDialog -import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.wallet.models.PendingTransaction import com.tangem.tap.features.wallet.redux.* diff --git a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt new file mode 100644 index 0000000000..52f61659e4 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.common + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.common.card.Card +import com.tangem.common.card.WalletData +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain +import com.tangem.operations.CommandResponse +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +/** +[REDACTED_AUTHOR] + */ +data class ScanResponse( + val card: Card, + val productType: ProductType, + val walletData: WalletData?, + val secondTwinPublicKey: String? = null, + val derivedKeys: Map = mapOf(), + val primaryCard: PrimaryCard? = null +) : CommandResponse { + + fun getBlockchain(): Blockchain { + if (productType == ProductType.Note) return card.getTangemNoteBlockchain() + ?: return Blockchain.Unknown + val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown + return Blockchain.fromId(blockchainName) + } + + fun getPrimaryToken(): Token? { + val cardToken = walletData?.token ?: return null + return Token( + cardToken.name, + cardToken.symbol, + cardToken.contractAddress, + cardToken.decimals, + Blockchain.fromId(walletData.blockchain) + ) + } + + fun isTangemNote(): Boolean = productType == ProductType.Note + fun isTangemWallet(): Boolean = productType == ProductType.Wallet + fun isTangemTwins(): Boolean = productType == ProductType.Twins + + fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed + fun supportsBackup(): Boolean = card.settings.isBackupAllowed + + fun twinsIsTwinned(): Boolean = + card.isTangemTwins() && walletData != null && secondTwinPublicKey != null +} + +enum class ProductType { + Note, Twins, Wallet +} + +typealias KeyWalletPublicKey = ByteArrayKey + +fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != null \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt new file mode 100644 index 0000000000..661602b270 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -0,0 +1,78 @@ +package com.tangem.domain.common + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.Card +import java.util.* + +/** +[REDACTED_AUTHOR] + */ +object TapWorkarounds { + + private const val START_2_COIN_ISSUER = "start2coin" + private const val TEST_CARD_BATCH = "99FF" + private const val TEST_CARD_ID_STARTS_WITH = "FF99" + + private val excludedBatches = listOf( + "0027", + "0030", + "0031", + "0035" + ) + + private val excludedIssuers = listOf( + "TTM BANK" + ) + + private val tangemNoteBatches = mapOf( + "AB01" to Blockchain.Bitcoin, + "AB02" to Blockchain.Ethereum, + "AB03" to Blockchain.CardanoShelley, + "AB04" to Blockchain.Dogecoin, + "AB05" to Blockchain.BSC, + "AB06" to Blockchain.XRP, + "AB07" to Blockchain.Bitcoin, + "AB08" to Blockchain.Ethereum, + ) + + private val tangemWalletBatchesWithStandardDerivationType = listOf( + "AC01", "AC02" + ) + + fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] + + val Card.derivationType: DerivationType + get() = when { + tangemWalletBatchesWithStandardDerivationType.contains(batchId) -> DerivationType.Standard + else -> DerivationType.Metamask + } + + val Card.isStart2Coin: Boolean + get() = isStart2CoinIssuer(issuer.name) + + val Card.isTestCard: Boolean + get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) + + + fun Card.isExcluded(): Boolean { + val excludedBatch = excludedBatches.contains(batchId) + val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) + return excludedBatch || excludedIssuerName + } + + fun Card.isNotSupportedInThatRelease(): Boolean { + return false + } + + @Deprecated("Use ScanResponse.isTangemNote") + fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId) + + + fun isStart2CoinIssuer(cardIssuer: String?): Boolean { + return cardIssuer?.toLowerCase(Locale.US) == START_2_COIN_ISSUER + } +} + +enum class DerivationType { + Metamask, Standard +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt b/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt rename to domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt index 8196f39b22..e6f9c23475 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinsHelper.kt +++ b/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.twins +package com.tangem.domain.common import com.tangem.common.card.Card import com.tangem.crypto.CryptoUtils diff --git a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalAction.kt b/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalAction.kt new file mode 100644 index 0000000000..ca26927233 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalAction.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.features.global.redux + +import com.tangem.domain.common.ScanResponse +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +//TODO: refactoring: is alias for the GlobalAction +sealed class DomainGlobalAction : Action { + data class SetScanResponse(val scanResponse: ScanResponse?) : DomainGlobalAction() +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalHub.kt b/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalHub.kt new file mode 100644 index 0000000000..40ea093ae5 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalHub.kt @@ -0,0 +1,43 @@ +package com.tangem.domain.features.global.redux + +import android.webkit.ValueCallback +import com.tangem.domain.restore.BaseStoreHub +import com.tangem.domain.restore.DomainState +import org.rekotlin.Action +import org.rekotlin.DispatchFunction + +/** +[REDACTED_AUTHOR] + */ +//TODO: refactoring: is alias for the GlobalMiddleware and the GlobalReducer +internal class DomainGlobalHub : BaseStoreHub("DomainGlobalHub") { + + override fun getHubState(storeState: DomainState): DomainGlobalState { + return storeState.globalState + } + + override fun updateStoreState(storeState: DomainState, newState: DomainGlobalState): DomainState { + return storeState.copy(globalState = newState) + } + + override suspend fun handleAction( + state: DomainState, + action: Action, + dispatch: DispatchFunction, + cancel: ValueCallback + ) { + if (action !is DomainGlobalAction) return + + val state = state.globalState + + when (action) { + } + } + + override fun reduceAction(action: Action, state: DomainGlobalState): DomainGlobalState = when (action) { + is DomainGlobalAction.SetScanResponse -> { + state.copy(scanResponse = action.scanResponse) + } + else -> state + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalState.kt b/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalState.kt new file mode 100644 index 0000000000..6335cc79b3 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalState.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.features.global.redux + +import com.tangem.domain.common.ScanResponse + +/** +[REDACTED_AUTHOR] + */ +//TODO: refactoring: is alias for the GlobalState +data class DomainGlobalState( + val scanResponse: ScanResponse? = null, +) From 0ec9a99b8be0389127ccd0e99089b6856dbe6093 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Apr 2022 13:33:32 +0300 Subject: [PATCH 09/28] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/WaitForMigration.kt | 7 +++++++ .../main/java/com/tangem/tap/common/redux/AppState.kt | 6 +++--- .../com/tangem/tap/common/redux/global/GlobalAction.kt | 2 +- .../tangem/tap/common/redux/global/GlobalMidlleware.kt | 8 +++++++- .../com/tangem/tap/common/redux/global/GlobalState.kt | 2 +- app/src/main/java/com/tangem/tap/domain/ProductType.kt | 9 --------- 6 files changed, 19 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/WaitForMigration.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/ProductType.kt diff --git a/app/src/main/java/com/tangem/tap/WaitForMigration.kt b/app/src/main/java/com/tangem/tap/WaitForMigration.kt new file mode 100644 index 0000000000..536b9711eb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/WaitForMigration.kt @@ -0,0 +1,7 @@ +package com.tangem.tap + +/** +[REDACTED_AUTHOR] + */ + +val DELAY_SDK_DIALOG_CLOSE = 1400L \ No newline at end of file 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 02f3f9d19e..f7f061dc06 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,7 +1,7 @@ package com.tangem.tap.common.redux -import com.tangem.domain.store.DomainState -import com.tangem.domain.store.domainStore +import com.tangem.domain.restore.DomainState +import com.tangem.domain.restore.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 @@ -51,7 +51,7 @@ data class AppState( val shopState: ShopState = ShopState(), ) : StateType { - val featuresState: DomainState + val domainState: DomainState get() = domainStore.state companion object { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 58684ac334..21fd824d71 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -4,13 +4,13 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.GlobalAnalyticsHandler import com.tangem.tap.common.redux.* import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.feedback.EmailData import com.tangem.tap.features.feedback.FeedbackManager 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 aa2218101b..9407ceff77 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 @@ -4,6 +4,8 @@ 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.domain.features.global.redux.DomainGlobalAction +import com.tangem.domain.restore.domainStore import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain @@ -100,10 +102,14 @@ private val globalMiddlewareHandler: Middleware = { dispatch, appState store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) when (result) { is CompletionResult.Success -> { + domainStore.dispatch(DomainGlobalAction.SetScanResponse(result.data)) tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data) action.onSuccess?.invoke(result.data) } - is CompletionResult.Failure -> action.onFailure?.invoke(result.error) + is CompletionResult.Failure -> { + domainStore.dispatch(DomainGlobalAction.SetScanResponse(null)) + action.onFailure?.invoke(result.error) + } } } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index ace0a06ae7..ef6ffeb391 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,5 +1,6 @@ package com.tangem.tap.common.redux.global +import com.tangem.domain.common.ScanResponse import com.tangem.tap.common.analytics.AnalyticsHandler import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY import com.tangem.tap.common.redux.StateDialog @@ -7,7 +8,6 @@ import com.tangem.tap.domain.PayIdManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.features.feedback.FeedbackManager import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.network.coinmarketcap.CoinMarketCapService diff --git a/app/src/main/java/com/tangem/tap/domain/ProductType.kt b/app/src/main/java/com/tangem/tap/domain/ProductType.kt deleted file mode 100644 index 4f57e8949d..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/ProductType.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.domain - -/** -[REDACTED_AUTHOR] - */ -enum class ProductType { - Note, Twins, Wallet - -} \ No newline at end of file From e90cd6d72775e6b459b9139e127e5ee3772e4c28 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 9 Apr 2022 14:23:09 +0300 Subject: [PATCH 10/28] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechService.kt | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt index b8e341d428..4cb45db16b 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt @@ -20,22 +20,38 @@ class TangemTechService { currency: String, ids: List ): Result { - return api.coinsPrices(currency, ids) + return try { + api.coinsPrices(currency, ids) + } catch (ex: Exception) { + Result.Failure(ex) + } } suspend fun coinsCheckAddress( contractAddress: String, networkId: String? = null ): Result { - return api.coinsCheckAddress(contractAddress, networkId) + return try { + api.coinsCheckAddress(contractAddress, networkId) + } catch (ex: Exception) { + Result.Failure(ex) + } } suspend fun coinsCurrencies(): Result { - return api.coinsCurrencies() + return try { + api.coinsCurrencies() + } catch (ex: Exception) { + Result.Failure(ex) + } } suspend fun coinsTokens(): Result { - return api.coinsTokens() + return try { + api.coinsTokens() + } catch (ex: Exception) { + Result.Failure(ex) + } } fun addHeaderInterceptors(interceptors: List) { From cf2b5f79139b07b6fd787d57dfba0f9878c11be6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 9 Apr 2022 14:25:04 +0300 Subject: [PATCH 11/28] Updated on 2026-08-14 --- .../java/com/tangem/tap/common/extensions/compose/Color.kt | 6 +++++- .../tap/features/home/compose/StoriesGeneralContent.kt | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt b/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt index d78a72ead5..fb4d02453c 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt @@ -10,7 +10,11 @@ import androidx.core.graphics.red /** [REDACTED_AUTHOR] */ -fun Color.argb(): Int { +fun Color.toAndroidGraphicsColor(): Int { val argb = this.toArgb() return android.graphics.Color.argb(argb.alpha, argb.red, argb.green, argb.blue) +} + +fun Color.parse(hexColor: String): Color { + return Color(hexColor.removePrefix("#").toInt(16)) } \ No newline at end of file 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 ce59c4cc1b..05c11fbd75 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 @@ -24,7 +24,7 @@ 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.tap.common.extensions.compose.toAndroidGraphicsColor import com.tangem.wallet.R @Composable @@ -95,7 +95,7 @@ fun SubtitleText(subtitleText: String, subtitleTextId: Int?) { textAlignment = View.TEXT_ALIGNMENT_CENTER typeface = Typeface.DEFAULT setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f) - setTextColor(color.argb()) + setTextColor(color.toAndroidGraphicsColor()) } } } From 73524b4c7e78b75a83cf36449fdd40daa68187eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 9 Apr 2022 14:28:00 +0300 Subject: [PATCH 12/28] Updated on 2026-08-14 --- .../com/tangem/tap/common/compose/Button.kt | 2 + .../java/com/tangem/tap/common/compose/Log.kt | 2 +- .../com/tangem/tap/common/compose/Spacer.kt | 60 +++++++++---------- 3 files changed, 33 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt index dc5d278520..fd450f68bd 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Button.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -24,6 +24,7 @@ private class Button {} fun Button( text: String = "", textId: Int? = null, + isEnabled: Boolean = true, modifier: Modifier = Modifier, leftContent: @Composable RowScope.() -> Unit = {}, rightContent: @Composable RowScope.() -> Unit = {}, @@ -31,6 +32,7 @@ fun Button( ) { Button( modifier = modifier.height(42.dp), + enabled = isEnabled, onClick = onClick, ) { leftContent() diff --git a/app/src/main/java/com/tangem/tap/common/compose/Log.kt b/app/src/main/java/com/tangem/tap/common/compose/Log.kt index c55fda6f4b..851798dfe1 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Log.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Log.kt @@ -8,5 +8,5 @@ import timber.log.Timber */ @Composable fun LogSideEffect(message: String) { - Timber.w("SideEffect: $message") + Timber.d("SideEffect: $message") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt index c1d9a7de9a..cfea07e9c8 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt @@ -14,78 +14,78 @@ import androidx.compose.ui.unit.dp */ // ***************************** Horizontal @Composable -fun SpacerH(height: Dp) { - Spacer(modifier = Modifier.height(height)) +fun SpacerH(height: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.height(height)) } @Composable -fun SpacerH4() { - SpacerH(4.dp) +fun SpacerH4(modifier: Modifier = Modifier) { + SpacerH(4.dp, modifier) } @Composable -fun SpacerH8() { - SpacerH(8.dp) +fun SpacerH8(modifier: Modifier = Modifier) { + SpacerH(8.dp, modifier) } @Composable -fun SpacerH16() { - SpacerH(16.dp) +fun SpacerH16(modifier: Modifier = Modifier) { + SpacerH(16.dp, modifier) } @Composable -fun SpacerH24() { - SpacerH(24.dp) +fun SpacerH24(modifier: Modifier = Modifier) { + SpacerH(24.dp, modifier) } // ***************************** Vertical @Composable -fun SpacerV(width: Dp) { - Spacer(modifier = Modifier.width(width)) +fun SpacerV(width: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.width(width)) } @Composable -fun SpacerV4() { - SpacerV(4.dp) +fun SpacerV4(modifier: Modifier = Modifier) { + SpacerV(4.dp, modifier) } @Composable -fun SpacerV8() { - SpacerV(8.dp) +fun SpacerV8(modifier: Modifier = Modifier) { + SpacerV(8.dp, modifier) } @Composable -fun SpacerV16() { - SpacerV(16.dp) +fun SpacerV16(modifier: Modifier = Modifier) { + SpacerV(16.dp, modifier) } @Composable -fun SpacerV24() { - SpacerV(24.dp) +fun SpacerV24(modifier: Modifier = Modifier) { + SpacerV(24.dp, modifier) } // ***************************** Size @Composable -fun SpacerS(size: Dp) { - Spacer(modifier = Modifier.size(size)) +fun SpacerS(size: Dp, modifier: Modifier = Modifier) { + Spacer(modifier = modifier.size(size)) } @Composable -fun SpacerS4() { - SpacerS(4.dp) +fun SpacerS4(modifier: Modifier = Modifier) { + SpacerS(4.dp, modifier) } @Composable -fun SpacerS8() { - SpacerS(8.dp) +fun SpacerS8(modifier: Modifier = Modifier) { + SpacerS(8.dp, modifier) } @Composable -fun SpacerS16() { - SpacerS(16.dp) +fun SpacerS16(modifier: Modifier = Modifier) { + SpacerS(16.dp, modifier) } @Composable -fun SpacerS24() { - SpacerS(24.dp) +fun SpacerS24(modifier: Modifier = Modifier) { + SpacerS(24.dp, modifier) } \ No newline at end of file From badc1e6fb6e693b55c0f98be65c175222ddfef0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 9 Apr 2022 14:28:30 +0300 Subject: [PATCH 13/28] Updated on 2026-08-14 --- app/src/main/res/values-ru/strings.xml | 27 ++++++++++------- .../main/res/values/strings_untranslated.xml | 29 ++++++++++++------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c54c69a74d..aa98785f5c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -187,7 +187,6 @@ Может быть лучше Один вопрос Вам нравится приложение Tangem? - Все поля обязательны к заполнению Токенов пока нет Добавленные токены Удалить токен %s @@ -197,20 +196,26 @@ Начните поиск Управление токенами Добавить пользовательский - Пользовательские токены Популярные токены - Добавить пользовательский токен - Пожалуйста заполните все поля - Введенное число не является допустимым десятичным числом - Имя - Символ токена - Адрес контракта - Десятичные - бывший USD Coin - бывший USDC Добавлен Удалить токен Значок токена + Адрес контракта + Пожалуйста, выберите сеть + Пожалуйста, заполните все поля + Количество знаков после запятой некорректно + Адрес контракта некорректен + Путь деривации некорректен + Сеть + Токен + Знаков после запятой + Сеть + Не выбрано + Например, USD Coin + Название токена + Например, USDC + Символ токена + Путь деривации (необязательно) Блокчейн Управление токенами Блокчейны diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 40ba641e70..1e50f240c9 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -67,8 +67,6 @@ One question How do you like Tangem? - All fields are required - //Manage tokens No tokens yet Added tokens @@ -80,19 +78,28 @@ Add Custom Custom tokens Popular tokens - Add Custom Token - Please, fill all fields - Entered number is not a valid decimals number - Name - Token symbol - Contract address - Decimals - ex. USD Coin - ex. USDC Added Remove token Token icon + Add Token + Contract address + Please select the network + Please fill in all the fields + Decimal number is invalid + Contract address is invalid + Derivation path is invalid + Network + Token + Decimals + Network + Not selected + E.g. USD Coin + Name + E.g. USDC + Token symbol + Derivation Path (optional) + //Details Blockchain Manage tokens From 9a53a65b0f965f1486fd9b64c59ac7dabaa5cb8d Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 9 Apr 2022 14:31:55 +0300 Subject: [PATCH 14/28] Updated on 2026-08-14 --- app/src/main/res/layout/view_compose.xml | 5 +++ .../main/res/layout/view_compose_fragment.xml | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 app/src/main/res/layout/view_compose.xml create mode 100644 app/src/main/res/layout/view_compose_fragment.xml diff --git a/app/src/main/res/layout/view_compose.xml b/app/src/main/res/layout/view_compose.xml new file mode 100644 index 0000000000..0d5a7a4452 --- /dev/null +++ b/app/src/main/res/layout/view_compose.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_compose_fragment.xml b/app/src/main/res/layout/view_compose_fragment.xml new file mode 100644 index 0000000000..af9273a84d --- /dev/null +++ b/app/src/main/res/layout/view_compose_fragment.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + From c779af8173548be939db6d8bbe64fc09f44d1272 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 9 Apr 2022 14:34:45 +0300 Subject: [PATCH 15/28] Updated on 2026-08-14 --- .../tangem/tap/common/compose/ErrorViews.kt | 25 ++++ .../common/compose/OutlinedTextFieldWidget.kt | 122 ++++++++++++------ 2 files changed, 109 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt b/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt new file mode 100644 index 0000000000..7055706474 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.common.compose + +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun ErrorView( + text: String, + modifier: Modifier = Modifier, + style: TextStyle = LocalTextStyle.current +) { + Text( + text, + color = MaterialTheme.colors.error, + modifier = modifier, + style = style + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index 44f5ab1634..65429321eb 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -1,13 +1,18 @@ package com.tangem.tap.common.compose import androidx.compose.animation.* +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.* +import androidx.compose.material.LinearProgressIndicator +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -31,42 +36,91 @@ fun OutlinedTextFieldWidget( label: String = "", placeholderId: Int? = null, placeholder: String = "", + trailingIcon: @Composable (() -> Unit)? = null, isEnabled: Boolean = true, + isVisible: Boolean = true, + isLoading: Boolean = false, error: DomainError? = null, errorConverter: ErrorConverter? = null, debounceTextChanges: Long = 400, visualTransformation: VisualTransformation = VisualTransformation.None, onTextChanged: (String) -> Unit, ) { + if (!isVisible) return + val placeholder = placeholderId?.let { stringResource(id = it) } ?: placeholder val label = labelId?.let { stringResource(id = it) } ?: label - val rTextValue = remember { mutableStateOf(text) } - val textDebouncer = ComposableTextDebouncer(text, debounceTextChanges, onTextChanged) - Column( modifier = modifier.animateContentSize(), ) { - OutlinedTextField( - value = rTextValue.value, - onValueChange = { - rTextValue.value = it - textDebouncer.emmit(it) - }, - modifier = Modifier.fillMaxWidth(), - label = { Text(label) }, - placeholder = { Text(placeholder) }, - singleLine = true, - enabled = isEnabled, - isError = error != null, + OutlinedProgressTextField( + text = text, + modifier = modifier, + label = label, + placeholder = placeholder, + trailingIcon = trailingIcon, + isEnabled = isEnabled, + isLoading = isLoading, + error = error, + debounceTextChanges = debounceTextChanges, visualTransformation = visualTransformation, + onTextChanged = onTextChanged ) errorConverter?.let { TextFieldErrorWidget(error, it) } } } @Composable -fun TextFieldErrorWidget( +private fun OutlinedProgressTextField( + text: String, + modifier: Modifier = Modifier, + label: String = "", + placeholder: String = "", + isEnabled: Boolean = true, + isLoading: Boolean = false, + error: DomainError? = null, + debounceTextChanges: Long = 400, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailingIcon: @Composable (() -> Unit)? = null, + onTextChanged: (String) -> Unit, +) { + val rTextValue = remember { mutableStateOf(text) } + val textDebouncer = ComposableTextDebouncer(text, debounceTextChanges, onTextChanged) + + // add ability to paste text from state + if (rTextValue.value != text) rTextValue.value = text + + Box { + OutlinedTextField( + value = rTextValue.value, + onValueChange = { + // immediately change text for the OutlinedTextField + rTextValue.value = it + textDebouncer.emmit(it) + }, + modifier = Modifier.fillMaxWidth(), + label = { Text(label) }, + placeholder = { Text(placeholder) }, + trailingIcon = trailingIcon, + singleLine = true, + enabled = isEnabled, + isError = error != null, + visualTransformation = visualTransformation, + ) + AnimatedVisibility( + modifier = modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(start = 6.dp, top = 0.dp, end = 6.dp, bottom = 6.dp), + visible = isLoading, + ) { LinearProgressIndicator() } + } + +} + +@Composable +private fun TextFieldErrorWidget( error: DomainError? = null, errorConverter: ErrorConverter, ) { @@ -75,30 +129,12 @@ fun TextFieldErrorWidget( enter = fadeIn() + slideInVertically(), exit = slideOutVertically() + fadeOut(), ) { - ErrorView( - errorConverter.convertError(error!!), - style = TextStyle( - fontSize = 14.sp - ) - ) + error?.let { + ErrorView(errorConverter.convertError(it), style = TextStyle(fontSize = 14.sp)) + } } } -@Composable -fun ErrorView( - text: String, - modifier: Modifier = Modifier, - style: TextStyle = LocalTextStyle.current -) { - Text( - text, - color = MaterialTheme.colors.error, - modifier = modifier, - style = style - ) -} - - @Preview @Composable fun OutlinedTextFieldWithErrorTest() { @@ -142,6 +178,16 @@ fun OutlinedTextFieldWithErrorTest() { errorConverter = converter, onTextChanged = {}, ) + OutlinedTextFieldWidget( + modifier = modifier, + text = "First", + label = "First label", + placeholder = "1 placeholder", + isLoading = true, + error = null, + errorConverter = converter, + onTextChanged = {}, + ) OutlinedTextFieldWidget( modifier = modifier, text = "First", From ee8c426ca9f32c345642ae7f5da805a894c3b128 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Apr 2022 16:12:00 +0300 Subject: [PATCH 16/28] Updated on 2026-08-14 --- .../network/coinmarketcap/CoinMarketCapApi.kt | 20 ++-- .../exchangeServices/onramper/OnnramperApi.kt | 11 --- .../onramper/OnramperService.kt | 5 +- .../addCustomToken/AddCustomTokenManager.kt | 48 ++++++++-- .../network/api/tangemTech/Responses.kt | 87 ++++++++++-------- .../network/api/tangemTech/TangemTechApi.kt | 9 +- .../api/tangemTech/TangemTechService.kt | 92 +++++++++---------- .../com/tangem/network/common/Interceptors.kt | 6 +- .../com/tangem/network/common/Retrofit.kt | 20 ++-- 9 files changed, 161 insertions(+), 137 deletions(-) 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 0de58a0b32..6836861c8c 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,7 +1,7 @@ package com.tangem.tap.network.coinmarketcap +import com.tangem.network.common.AddHeaderInterceptor import com.tangem.network.common.createRetrofitInstance -import okhttp3.Interceptor import retrofit2.http.GET import retrofit2.http.Query @@ -9,9 +9,9 @@ interface CoinMarketCapApi { @GET("v1/tools/price-conversion") suspend fun getRateInfo( - @Query("amount") amount: Int, - @Query("symbol") cryptoCurrencyName: String, - @Query("convert") fiatCurrencyName: String? = null + @Query("amount") amount: Int, + @Query("symbol") cryptoCurrencyName: String, + @Query("convert") fiatCurrencyName: String? = null ): RateInfoResponse @GET("v1/fiat/map") @@ -23,15 +23,11 @@ interface CoinMarketCapApi { fun create(apiKey: String): CoinMarketCapApi { return createRetrofitInstance( - baseUrl, - interceptors = listOf(createCoinMarketRequestInterceptor(apiKey)), + baseUrl = baseUrl, + interceptors = listOf( + AddHeaderInterceptor(mapOf("X-CMC_PRO_API_KEY" to apiKey)), + ), ).create(CoinMarketCapApi::class.java) } } -} - -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/onramper/OnnramperApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt index 01fec5d3d9..30a65ed706 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt @@ -1,8 +1,6 @@ package com.tangem.tap.network.exchangeServices.onramper import com.squareup.moshi.JsonClass -import okhttp3.Interceptor -import okhttp3.Response import retrofit2.http.GET import retrofit2.http.Path @@ -23,15 +21,6 @@ interface OnramperApi { } } -class AddKeyToHeaderInterceptor( - private val key: String -) : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request().newBuilder().addHeader("Authorization", "Basic $key").build() - return chain.proceed(request) - } -} - @JsonClass(generateAdapter = true) data class GatewaysResponse( val gateways: List 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 15b7a73a5b..94bdae395e 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,6 +4,7 @@ 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.AddHeaderInterceptor import com.tangem.network.common.createRetrofitInstance import com.tangem.tap.common.extensions.urlEncode import com.tangem.tap.common.redux.global.CryptoCurrencyName @@ -25,7 +26,9 @@ class OnramperService( private val api: OnramperApi by lazy { createRetrofitInstance( baseUrl = OnramperApi.BASE_URL, - interceptors = listOf(AddKeyToHeaderInterceptor(apiKey)) + interceptors = listOf( + AddHeaderInterceptor(mapOf("Authorization" to "Basic $apiKey")), + ) ).create(OnramperApi::class.java) } 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 index 7317484b19..dac29b2d3a 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt @@ -1,9 +1,9 @@ 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.Coins import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.network.common.AddHeaderInterceptor /** [REDACTED_AUTHOR] @@ -12,20 +12,52 @@ class AddCustomTokenManager( private val tangemTechService: TangemTechService ) { - suspend fun findContractAddress( + suspend fun checkAddress( contractAddress: String, networkId: String? = null - ): List { - val result = tangemTechService.coinsCheckAddress(contractAddress, networkId) + ): List { + val result = tangemTechService.coins.checkAddress(contractAddress, networkId) return when (result) { is Result.Success -> { - result.data.tokens + val resultTokens = result.data.tokens + val newTokensList = mutableListOf() + resultTokens.forEach { + val contractsWithTheSameAddress = it.contracts.filter { it.address == contractAddress } + if (contractsWithTheSameAddress.isNotEmpty()) { + val newToken = it.copy(contracts = contractsWithTheSameAddress) + newTokensList.add(newToken) + } + } + when { + // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679 + newTokensList.size > 1 -> listOf(newTokensList[0]) + else -> newTokensList + } + } + is Result.Failure -> emptyList() + } + } + + suspend fun tokens(): List { + val result = tangemTechService.coins.tokens() + return when (result) { + is Result.Success -> { + val currencies = result.data.tokens + currencies.filter { + it.contracts.isNullOrEmpty() + } } is Result.Failure -> emptyList() } } fun attachAuthKey(authKey: String) { - tangemTechService.addHeaderInterceptors(listOf(TangemAuthInterceptor(authKey))) + tangemTechService.addHeaderInterceptors(listOf( + CardPublicKeyHttpInterceptor(authKey), + )) } -} \ No newline at end of file +} + +private class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor(mapOf( + "card_public_key" to cardPublicKeyHex, +)) \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt index f1a5a921a9..6d73a3955f 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt @@ -5,46 +5,57 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -data class CoinsPricesResponse( - val prices: List -) +interface HttpResponse +interface TangemTechResponse : HttpResponse -data class CoinPrice( - val name: String, - val price: BigDecimal, -) - -data class CoinsCheckAddressResponse( - val imageHost: String, - val tokens: List, - val total: Int, -) { - data class Token( - val id: String, - val name: String, - val symbol: String, - val active: Boolean, - val contracts: List - ) { - data class Contract( - val networkId: String, - val address: String, - val decimalCount: BigDecimal?, - val active: Boolean +sealed class Coins : TangemTechResponse { + data class PricesResponse(val prices: List) : Coins() { + data class Price( + val name: String, + val price: BigDecimal, ) } -} -data class CoinsCurrenciesResponse( - val currencies: List, -) { - data class Currency( - val id: String, - val code: String, - val name: String, - val rateBTC: String, - val unit: String, - val type: String, - ) -} + data class CheckAddressResponse(val imageHost: String, val tokens: List, val total: Int) : Coins() { + data class Token( + val id: String, + val name: String, + val symbol: String, + val active: Boolean, + val contracts: List + ) { + data class Contract( + val networkId: String, + val address: String, + val decimalCount: BigDecimal?, + val active: Boolean + ) + } + } + data class TokensResponse(val imageHost: String, val tokens: List, val total: Int) : Coins() { + data class Token( + val id: String, + val name: String, + val symbol: String, + val contracts: List? + ) { + data class Contract( + val networkId: String, + val address: String, + val decimalCount: BigDecimal?, + ) + } + } + + data class CurrenciesResponse(val currencies: List) { + data class Currency( + val id: String, + val code: String, + val name: String, + val rateBTC: String, + val unit: String, + val type: String, + ) + } +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt index fbb92c73f1..2917063e30 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt @@ -1,6 +1,5 @@ package com.tangem.network.api.tangemTech -import com.tangem.common.services.Result import retrofit2.http.GET import retrofit2.http.Query @@ -13,18 +12,18 @@ interface TangemTechApi { suspend fun coinsPrices( @Query("currency") currency: String, @Query("ids") ids: List, - ): Result + ): Coins.PricesResponse @GET("coins/check-address") suspend fun coinsCheckAddress( @Query("contractAddress") contractAddress: String, @Query("networkId") networkId: String? = null, - ): Result + ): Coins.CheckAddressResponse @GET("coins/currencies") - suspend fun coinsCurrencies(): Result + suspend fun coinsCurrencies(): Coins.CurrenciesResponse @GET("coins/tokens") - suspend fun coinsTokens(): Result + suspend fun coinsTokens(): Coins.TokensResponse } \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt index 4cb45db16b..10c0ceb3cc 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt @@ -1,8 +1,9 @@ package com.tangem.network.api.tangemTech import com.tangem.common.services.Result +import com.tangem.common.services.performRequest import com.tangem.network.common.AddHeaderInterceptor -import com.tangem.network.common.CacheHttpInterceptor +import com.tangem.network.common.CacheControlHttpInterceptor import com.tangem.network.common.createRetrofitInstance /** @@ -10,50 +11,18 @@ import com.tangem.network.common.createRetrofitInstance */ class TangemTechService { + val coins: CoinsRoute = CoinsRoute() + + private val techRoutes: List = listOf( + coins + ) + private val headerInterceptors = mutableListOf( - CacheHttpInterceptor(cacheMaxAge) + CacheControlHttpInterceptor(cacheMaxAge) ) private var api: TangemTechApi = createApi() - suspend fun coinsPrices( - currency: String, - ids: List - ): Result { - return try { - api.coinsPrices(currency, ids) - } catch (ex: Exception) { - Result.Failure(ex) - } - } - - suspend fun coinsCheckAddress( - contractAddress: String, - networkId: String? = null - ): Result { - return try { - api.coinsCheckAddress(contractAddress, networkId) - } catch (ex: Exception) { - Result.Failure(ex) - } - } - - suspend fun coinsCurrencies(): Result { - return try { - api.coinsCurrencies() - } catch (ex: Exception) { - Result.Failure(ex) - } - } - - suspend fun coinsTokens(): Result { - return try { - api.coinsTokens() - } catch (ex: Exception) { - Result.Failure(ex) - } - } - fun addHeaderInterceptors(interceptors: List) { headerInterceptors.removeAll(interceptors) headerInterceptors.addAll(interceptors) @@ -65,8 +34,9 @@ class TangemTechService { baseUrl = baseUrl, interceptors = headerInterceptors.toList() ) - - return retrofit.create(TangemTechApi::class.java) + return retrofit.create(TangemTechApi::class.java).apply { + techRoutes.forEach { it.setApi(this) } + } } companion object { @@ -75,8 +45,36 @@ class TangemTechService { } } -class TangemAuthInterceptor( - private val cardPublicKeyHex: String -) : AddHeaderInterceptor( - mapOf("card_public_key" to cardPublicKeyHex) -) \ No newline at end of file +private interface TangemTechRoute { + fun setApi(api: TangemTechApi) +} + +class CoinsRoute : TangemTechRoute { + private lateinit var api: TangemTechApi + + override fun setApi(api: TangemTechApi) { + this.api = api + } + + suspend fun prices( + currency: String, + ids: List + ): Result { + return performRequest { api.coinsPrices(currency, ids) } + } + + suspend fun checkAddress( + contractAddress: String, + networkId: String? = null + ): Result { + return performRequest { api.coinsCheckAddress(contractAddress, networkId) } + } + + suspend fun currencies(): Result { + return performRequest { api.coinsCurrencies() } + } + + suspend fun tokens(): Result { + return performRequest { api.coinsTokens() } + } +} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/Interceptors.kt b/network/src/main/java/com/tangem/network/common/Interceptors.kt index dab4451281..dff259332e 100644 --- a/network/src/main/java/com/tangem/network/common/Interceptors.kt +++ b/network/src/main/java/com/tangem/network/common/Interceptors.kt @@ -21,6 +21,6 @@ open class AddHeaderInterceptor( } } -class CacheHttpInterceptor( - maxAgeSeconds: Int -) : AddHeaderInterceptor(mapOf("Cache-Control" to "max-age=$maxAgeSeconds")) \ No newline at end of file +class CacheControlHttpInterceptor(maxAgeSeconds: Int) : AddHeaderInterceptor(mapOf( + "Cache-Control" to "max-age=$maxAgeSeconds", +)) \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/Retrofit.kt b/network/src/main/java/com/tangem/network/common/Retrofit.kt index d6e9614290..86ab156c51 100644 --- a/network/src/main/java/com/tangem/network/common/Retrofit.kt +++ b/network/src/main/java/com/tangem/network/common/Retrofit.kt @@ -15,8 +15,13 @@ fun createRetrofitInstance( converterFactory: Converter.Factory = MoshiConverter.createFactory(), logEnabled: Boolean = false ): Retrofit { + okHttpBuilder.apply { + callTimeout(10, TimeUnit.SECONDS) + connectTimeout(20, TimeUnit.SECONDS) + readTimeout(20, TimeUnit.SECONDS) + writeTimeout(20, TimeUnit.SECONDS) + } interceptors.forEach { okHttpBuilder.addInterceptor(it) } - addTimeOuts(okHttpBuilder) if (logEnabled) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor()) @@ -27,15 +32,6 @@ fun createRetrofitInstance( .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) -} - -private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor { - return HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } +private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY } \ No newline at end of file From a38c36ce2db66ab30d88df2902748238145a6118 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Apr 2022 16:17:06 +0300 Subject: [PATCH 17/28] Updated on 2026-08-14 --- .../com/tangem/tap/common/redux/AppState.kt | 4 +- .../common/redux/global/GlobalMidlleware.kt | 4 +- .../tangem/domain/{common => }/DomainError.kt | 11 +- .../com/tangem/domain/DomainStateDialog.kt | 19 + .../tangem/domain/common/TapWorkarounds.kt | 10 - .../domain/common/form/FieldDataConverters.kt | 40 ++ .../domain/common/form/FieldsValidators.kt | 37 +- .../com/tangem/domain/common/form/Form.kt | 67 +--- .../common/{ => util}/ValueDebouncer.kt | 7 +- .../AddCustomTokenStatePrinter.kt | 63 +++ .../features/addCustomToken/CompleteData.kt | 59 +++ .../domain/features/addCustomToken/Errors.kt | 20 +- .../features/addCustomToken/FormFields.kt | 20 +- .../redux/AddCustomTokenAction.kt | 38 +- .../addCustomToken/redux/AddCustomTokenHub.kt | 368 ++++++++++++++---- .../redux/AddCustomTokenState.kt | 117 ++++++ .../redux/AddCustomTokensState.kt | 137 ------- .../features/addCustomToken/redux/Models.kt | 31 ++ .../com/tangem/domain/redux/DomainState.kt | 13 + .../com/tangem/domain/redux/DomainStore.kt | 33 ++ .../com/tangem/domain/redux/ReStoreHub.kt | 107 +++++ .../global}/DomainGlobalAction.kt | 4 +- .../redux => redux/global}/DomainGlobalHub.kt | 19 +- .../global}/DomainGlobalState.kt | 4 +- .../domain/redux/state/StateObservables.kt | 36 ++ .../tangem/domain/redux/state/StatePrinter.kt | 12 + .../com/tangem/domain/store/DomainStore.kt | 37 -- .../java/com/tangem/domain/store/StoreHub.kt | 87 ----- 28 files changed, 940 insertions(+), 464 deletions(-) rename domain/src/main/java/com/tangem/domain/{common => }/DomainError.kt (57%) create mode 100644 domain/src/main/java/com/tangem/domain/DomainStateDialog.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt rename domain/src/main/java/com/tangem/domain/common/{ => util}/ValueDebouncer.kt (86%) create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenStatePrinter.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt delete mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokensState.kt create mode 100644 domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt create mode 100644 domain/src/main/java/com/tangem/domain/redux/DomainState.kt create mode 100644 domain/src/main/java/com/tangem/domain/redux/DomainStore.kt create mode 100644 domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt rename domain/src/main/java/com/tangem/domain/{features/global/redux => redux/global}/DomainGlobalAction.kt (62%) rename domain/src/main/java/com/tangem/domain/{features/global/redux => redux/global}/DomainGlobalHub.kt (63%) rename domain/src/main/java/com/tangem/domain/{features/global/redux => redux/global}/DomainGlobalState.kt (61%) create mode 100644 domain/src/main/java/com/tangem/domain/redux/state/StateObservables.kt create mode 100644 domain/src/main/java/com/tangem/domain/redux/state/StatePrinter.kt delete mode 100644 domain/src/main/java/com/tangem/domain/store/DomainStore.kt delete mode 100644 domain/src/main/java/com/tangem/domain/store/StoreHub.kt 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 f7f061dc06..ac955b82a7 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,7 +1,7 @@ package com.tangem.tap.common.redux -import com.tangem.domain.restore.DomainState -import com.tangem.domain.restore.domainStore +import com.tangem.domain.redux.DomainState +import com.tangem.domain.redux.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 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 9407ceff77..7e1cb3f0cc 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 @@ -4,8 +4,8 @@ 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.domain.features.global.redux.DomainGlobalAction -import com.tangem.domain.restore.domainStore +import com.tangem.domain.redux.domainStore +import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain diff --git a/domain/src/main/java/com/tangem/domain/common/DomainError.kt b/domain/src/main/java/com/tangem/domain/DomainError.kt similarity index 57% rename from domain/src/main/java/com/tangem/domain/common/DomainError.kt rename to domain/src/main/java/com/tangem/domain/DomainError.kt index 67bc6d289c..ad0115427a 100644 --- a/domain/src/main/java/com/tangem/domain/common/DomainError.kt +++ b/domain/src/main/java/com/tangem/domain/DomainError.kt @@ -1,7 +1,10 @@ -package com.tangem.domain.common +package com.tangem.domain /** [REDACTED_AUTHOR] + * @property code describes what feature is the error coming from + * @property message the error description + * @property data any data that can help in the part where this error is being handled */ interface DomainError { val code: Int @@ -9,7 +12,7 @@ interface DomainError { val data: Any? } -open class AnyError( +open class AnError( override val code: Int, override val message: String, override val data: Any? = null, @@ -21,4 +24,6 @@ interface ErrorConverter { interface Validator { fun validate(data: Data? = null): Error? -} \ No newline at end of file +} + +const val ERROR_CODE_ADD_CUSTOM_TOKEN = 100 \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt new file mode 100644 index 0000000000..c6baa9a431 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt @@ -0,0 +1,19 @@ +package com.tangem.domain + +import com.tangem.common.extensions.VoidCallback +import com.tangem.network.api.tangemTech.Coins + +/** +[REDACTED_AUTHOR] + */ +interface DomainStateDialog + +sealed class DomainDialog : DomainStateDialog { + + data class SelectTokenDialog( + val items: List, + val itemNameConverter: (Coins.CheckAddressResponse.Token.Contract) -> String, + val onSelect: (Coins.CheckAddressResponse.Token.Contract) -> Unit, + val onClose: VoidCallback = {} + ) : DomainDialog() +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index 661602b270..61754302fb 100644 --- a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -41,12 +41,6 @@ object TapWorkarounds { fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] - val Card.derivationType: DerivationType - get() = when { - tangemWalletBatchesWithStandardDerivationType.contains(batchId) -> DerivationType.Standard - else -> DerivationType.Metamask - } - val Card.isStart2Coin: Boolean get() = isStart2CoinIssuer(issuer.name) @@ -71,8 +65,4 @@ object TapWorkarounds { fun isStart2CoinIssuer(cardIssuer: String?): Boolean { return cardIssuer?.toLowerCase(Locale.US) == START_2_COIN_ISSUER } -} - -enum class DerivationType { - Metamask, Standard } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt b/domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt new file mode 100644 index 0000000000..a0cd94a04f --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.common.form + +import com.tangem.common.json.MoshiJsonConverter + +/** +[REDACTED_AUTHOR] + */ +interface DataConverterVisitor { + fun visit(data: Data?) + fun getConvertedData(): Result +} + +interface FieldDataConverter : DataConverterVisitor + +abstract class BaseFieldDataConverter() : FieldDataConverter { + protected val collectIds: List + get() = getIdToCollect() + + protected val collectedData: MutableMap = mutableMapOf() + + override fun visit(data: Pair>?) { + val id = data?.first ?: return + + if (collectIds.contains(id)) { + collectedData[id] = data.second.value + } + } + + abstract fun getIdToCollect(): List +} + +class FieldToJsonConverter( + private val fieldsToConvert: List = listOf(), + protected val jsonConverter: MoshiJsonConverter +) : BaseFieldDataConverter() { + + override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ") + + override fun getIdToCollect(): List = fieldsToConvert +} \ No newline at end of file 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 index 94127dfdc2..ff2d9398e7 100644 --- a/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt +++ b/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt @@ -2,9 +2,7 @@ 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.Validator import com.tangem.domain.features.addCustomToken.AddCustomTokenError /** @@ -27,18 +25,16 @@ class StringIsNotEmptyValidator : CustomTokenValidator() { } class TokenContractAddressValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? { + if (data == null || data.isEmpty()) return null - 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 + return if (EthereumAddressService().validate(data)) { + null + } else { + AddCustomTokenError.InvalidContractAddress } } + } class TokenNetworkValidator : CustomTokenValidator() { @@ -48,16 +44,13 @@ class TokenNetworkValidator : CustomTokenValidator() { } } -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 - } +class TokenDecimalsValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? { + val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty + + return when { + decimal > 30 -> AddCustomTokenError.InvalidDecimalsCount + else -> null } } } \ 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 index cd0a0ea261..5788f13b1e 100644 --- a/domain/src/main/java/com/tangem/domain/common/form/Form.kt +++ b/domain/src/main/java/com/tangem/domain/common/form/Form.kt @@ -1,7 +1,5 @@ package com.tangem.domain.common.form -import com.tangem.common.json.MoshiJsonConverter - /** [REDACTED_AUTHOR] */ @@ -13,63 +11,38 @@ class Form( fun getData(id: FieldId): Pair? = getField(id)?.getData() // convert this form data whatever you want - fun getData(converter: FieldDataConverter<*>) { + fun visitDataConverter(converter: FieldDataConverter<*>) { fieldList.forEach { it.visitDataConverter(converter) } } } interface FieldId -interface Field { +interface Field { val id: FieldId - var value: Data - val isEnabled: Boolean - val isVisible: Boolean + var data: Data + + data class Data( + val value: Data, + val isUserInput: Boolean = true + ) } -abstract class BaseDataField( - override val id: FieldId, - override var value: Data -) : DataField { +typealias FieldData = Pair> - override fun getData(): Pair = id to value +interface DataField : Field { + fun getData(): Pair> + fun visitDataConverter(dataConverter: FieldDataConverter<*>) +} + +abstract class BaseDataField( + override val id: FieldId, + override var data: Field.Data, +) : DataField { + + override fun getData(): Pair> = id to data 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/common/ValueDebouncer.kt b/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt similarity index 86% rename from domain/src/main/java/com/tangem/domain/common/ValueDebouncer.kt rename to domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt index c51c711131..36dd4441f4 100644 --- a/domain/src/main/java/com/tangem/domain/common/ValueDebouncer.kt +++ b/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt @@ -1,21 +1,20 @@ -package com.tangem.domain.common +package com.tangem.domain.common.util 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 var value: T? = null private val debounceScope = CoroutineScope(Job() + Dispatchers.Main) private val flow = MutableStateFlow(value) @@ -28,7 +27,7 @@ class ValueDebouncer( flow.filter { if (value == null) true else value != it } .debounce(debounce) .onEach { - Timber.d("onValueChanged: $it") + value = it onValueChanged(it) } .collect() diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenStatePrinter.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenStatePrinter.kt new file mode 100644 index 0000000000..04365d6c82 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenStatePrinter.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.features.addCustomToken + +import com.tangem.common.json.MoshiJsonConverter +import com.tangem.domain.common.form.FieldToJsonConverter +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.redux.DomainState +import com.tangem.domain.redux.state.StatePrinter +import org.rekotlin.Action + +class AddCustomTokenStatePrinter : StatePrinter { + private val jsonConverter: MoshiJsonConverter = MoshiJsonConverter.INSTANCE + private var builder: StringBuilder = StringBuilder() + + override fun print(action: Action, domainState: DomainState): String? { + val action = (action as? AddCustomTokenAction) ?: return null + val state = domainState.addCustomTokensState + + val fieldConverter = FieldToJsonConverter(listOf( + CustomTokenFieldId.ContractAddress, + CustomTokenFieldId.Network, + CustomTokenFieldId.Name, + CustomTokenFieldId.Symbol, + CustomTokenFieldId.Decimals, + CustomTokenFieldId.DerivationPath, + ), jsonConverter) + state.visitDataConverter(fieldConverter) + val errors = state.formErrors.map { + "${it.key}: ${it.value::class.java.simpleName}" + } + val warnings = state.warnings.map { it::class.java.simpleName } + + printAction(action, state) + printStateValue("fields", fieldConverter.getConvertedData()) + printStateValue("fieldErrors", toJson(errors)) + printStateValue("warnings", toJson(warnings)) + printStateValue("screenState", toJson(state.screenState)) + printMessage("------------------------------------------------------") + + val printed = builder.toString() + builder = StringBuilder() + + return printed + } + + override fun getStateObject(domainState: DomainState): AddCustomTokenState = domainState.addCustomTokensState + + private fun printStateValue(name: String, value: String) { + printMessage("$name: $value") + } + + private fun printAction(action: AddCustomTokenAction, state: AddCustomTokenState) { + printMessage("action: $action, state: ${state::class.java.simpleName}") + } + + private fun toJson(value: Any): String { + return jsonConverter.prettyPrint(value) + } + + private fun printMessage(message: String) { + builder.append("$message\n") + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt new file mode 100644 index 0000000000..9f9c4af6f7 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.features.addCustomToken + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.form.BaseFieldDataConverter +import com.tangem.domain.common.form.FieldDataConverter +import com.tangem.domain.common.form.FieldId +import com.tangem.domain.features.addCustomToken.redux.CompleteDataType + +/** +[REDACTED_AUTHOR] + */ +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[CustomTokenFieldId.Network] as Blockchain, + collectedData[CustomTokenFieldId.DerivationPath] as? String, + ) + + override fun getIdToCollect(): List = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.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[CustomTokenFieldId.ContractAddress] as String, + collectedData[CustomTokenFieldId.Network] as Blockchain, + collectedData[CustomTokenFieldId.Name] as String, + collectedData[CustomTokenFieldId.Symbol] as String, + collectedData[CustomTokenFieldId.Decimals] as Int, + collectedData[CustomTokenFieldId.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/features/addCustomToken/Errors.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt index 3503402fa8..c7abc4c0d2 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt @@ -1,19 +1,21 @@ package com.tangem.domain.features.addCustomToken -import com.tangem.domain.common.AnyError +import com.tangem.domain.AnError +import com.tangem.domain.ERROR_CODE_ADD_CUSTOM_TOKEN /** [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() +sealed class AddCustomTokenError : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - error") { object FieldIsEmpty : AddCustomTokenError() object FieldIsNotEmpty : AddCustomTokenError() + object InvalidContractAddress : AddCustomTokenError() + object NetworkIsNotSelected : AddCustomTokenError() + object InvalidDecimalsCount : AddCustomTokenError() object InvalidDerivationPath : AddCustomTokenError() +} + +sealed class AddCustomTokenWarning : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - warning") { + object PotentialScamToken : AddCustomTokenWarning() + object TokenAlreadyAdded : AddCustomTokenWarning() } \ 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 index bc03838408..b6084347d2 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt @@ -2,6 +2,7 @@ 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.Field import com.tangem.domain.common.form.FieldId /** @@ -16,21 +17,16 @@ enum class CustomTokenFieldId : FieldId { DerivationPath, } +data class TokenField( + override val id: FieldId, +) : BaseDataField(id, Field.Data("")) + 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, "") +) : BaseDataField(id, Field.Data(Blockchain.Unknown)) data class TokenDerivationPathField( override val id: FieldId, - override val isEnabled: Boolean = true, - override val isVisible: Boolean = true, -) : BaseDataField(id, "") + val itemList: List, +) : BaseDataField(id, Field.Data(Blockchain.Unknown)) \ No newline at end of file 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 index 7e5e9e6dc6..d4eda81f69 100644 --- 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 @@ -1,34 +1,33 @@ package com.tangem.domain.features.addCustomToken.redux import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.form.Field 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 com.tangem.network.api.tangemTech.Coins 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() + object OnCreate : AddCustomTokenAction() + object OnDestroy : AddCustomTokenAction() + data class OnTokenFieldChanged(val id: FieldId, val value: Field.Data) : AddCustomTokenAction() + data class OnTokenContractAddressChanged(val value: Field.Data) : AddCustomTokenAction() + data class OnTokenNetworkChanged(val value: Field.Data) : AddCustomTokenAction() + data class OnTokenDerivationPathChanged(val value: Field.Data) : AddCustomTokenAction() + data class OnTokenDecimalsChanged(val value: Field.Data) : AddCustomTokenAction() + data class OnCustomTokenSelected(val any: Any = Unit) : AddCustomTokenAction() - // from redux - object UpdateForm : AddCustomTokenAction() + data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction() data class FillTokenFields( - val token: CoinsCheckAddressResponse.Token, - val contract: CoinsCheckAddressResponse.Token.Contract, + val token: Coins.CheckAddressResponse.Token, + val contract: Coins.CheckAddressResponse.Token.Contract, ) : AddCustomTokenAction() sealed class Error : AddCustomTokenAction() { @@ -37,7 +36,14 @@ sealed class AddCustomTokenAction : Action { } sealed class Warning : AddCustomTokenAction() { - data class Add(val warning: AddCustomTokenWarning) : Warning() - data class Remove(val warning: AddCustomTokenWarning) : Warning() + data class Add(val warnings: Set) : Warning() + data class Remove(val warnings: Set) : Warning() + data class Replace(val remove: Set, val add: Set) : Warning() + } + + // To change the screenState + sealed class Screen : AddCustomTokenAction() { + data class UpdateTokenFields(val pairs: List>) : Screen() + data class UpdateAddButton(val addButton: ViewStates.AddButton) : Screen() } } \ 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 index ea05c79fc7..62eef658b1 100644 --- 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 @@ -2,95 +2,184 @@ package com.tangem.domain.features.addCustomToken.redux import android.webkit.ValueCallback import com.tangem.blockchain.common.Blockchain +import com.tangem.common.card.Card +import com.tangem.domain.DomainDialog 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 com.tangem.domain.redux.BaseStoreHub +import com.tangem.domain.redux.DomainState +import com.tangem.domain.redux.dispatchOnMain +import com.tangem.domain.redux.domainStore +import com.tangem.domain.redux.global.DomainGlobalAction +import com.tangem.network.api.tangemTech.Coins import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch import org.rekotlin.Action -import org.rekotlin.DispatchFunction /** [REDACTED_AUTHOR] */ -internal object AddCustomTokenHub : BaseStoreHub("AddCustomTokenHub") { +internal class AddCustomTokenHub : BaseStoreHub("AddCustomTokenHub") { - override val initialState: AddCustomTokensState = AddCustomTokensState() + override fun getHubState(storeState: DomainState): AddCustomTokenState { + return storeState.addCustomTokensState + } - override fun handle(state: () -> DomainState?, action: Action, dispatch: DispatchFunction) = when (action) { - is OnBackPressed -> hubScope.cancel() - else -> super.handle(state, action, dispatch) + override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState { + return storeState.copy(addCustomTokensState = newHubState) } override suspend fun handleAction( - state: DomainState, action: Action, - dispatch: DispatchFunction, + storeState: DomainState, cancel: ValueCallback ) { if (action !is AddCustomTokenAction) return - val state = state.addCustomTokensState +// val card = storeState.globalState.scanResponse?.card +// ?: throw IllegalStateException("ScanResponse must be set before showing the AddCustomToken screen") + val hubState = storeState.addCustomTokensState when (action) { + is OnCreate -> { +// hubState.addCustomTokenManager.attachAuthKey(card.cardPublicKey.toHexString()) + } + is OnDestroy -> hubScope.cancel() is OnTokenContractAddressChanged -> { val contractAddress = action.value - val validator: TokenContractAddressValidator = getValidator(ContractAddress, state) - val error = validator.validate(contractAddress) + val validator: TokenContractAddressValidator = getValidator(ContractAddress, hubState) + val error = validator.validate(contractAddress.value) 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 + if (error != null || contractAddress.value.isEmpty()) { + dispatchOnMain(actionsUnlockTokenFields()) + return } - val foundTokens = manager.findContractAddress(contractAddress, selectedNetwork?.id) + + val foundTokens = requestInfoAboutContractAddress(contractAddress.value, hubState) + val warningsToAdd = mutableSetOf() + val warningsToRemove = mutableSetOf() when { - foundTokens.isEmpty() -> {} - foundTokens.size == 1 -> { - // fill and disable other fields by token info - val token = foundTokens[0] - dispatchOnMain(FillTokenFields(token, token.contracts[0])) + foundTokens.isEmpty() -> { + warningsToAdd.add(AddCustomTokenWarning.PotentialScamToken) } else -> { - // show tokens list for selection - + val token = foundTokens[0] + checkToken(null, token, warningsToAdd, warningsToRemove) } - + } + if (warningsToAdd.isNotEmpty() || warningsToRemove.isNotEmpty()) { + dispatchOnMain(Warning.Replace(warningsToRemove.toSet(), warningsToAdd.toSet())) } } 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)) + val validator: TokenNetworkValidator = getValidator(Network, hubState) + addOrRemoveError(Network, validator.validate(action.value.value)) } is OnTokenDerivationPathChanged -> { - val validator: DerivationPathValidator = getValidator(DerivationPath, state) - addOrRemoveError(DerivationPath, validator.validate(action.value)) +// val validator: TokenDerivationPathValidator = getValidator(DerivationPath, hubState) +// addOrRemoveError(DerivationPath, validator.validate(action.value.value)) + } + is OnTokenDecimalsChanged -> { + val validator: TokenDecimalsValidator = getValidator(Decimals, hubState) + addOrRemoveError(Decimals, validator.validate(action.value.value)) + } + is OnTokenFieldChanged -> { + val validator: StringIsNotEmptyValidator = getValidator(action.id, hubState) + addOrRemoveError(action.id as CustomTokenFieldId, validator.validate(action.value.value)) + } + is OnCustomTokenSelected -> { +// dispatchOnMain() } is FillTokenFields -> { - val networkField = getField(Network, state) - val nameField = getField(Name, state) - val symbolField = getField(Symbol, state) - val decimalsField = getField(Decimals, state) + val networkField = getField(Network, hubState) + val nameField = getField(Name, hubState) + val symbolField = getField(Symbol, hubState) + val decimalsField = getField(Decimals, hubState) 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() + val blockchain = Blockchain.fromNetworkId(contract.networkId) + networkField.data = Field.Data(blockchain, false) + nameField.data = Field.Data(token.name, false) + symbolField.data = Field.Data(token.symbol, false) + decimalsField.data = Field.Data(contract.decimalCount.toString(), false) - dispatchOnMain(UpdateForm) + dispatchOnMain(UpdateForm(hubState)) + } + else -> {} + } + } + + private suspend fun requestInfoAboutContractAddress( + contractAddress: String, + hubState: AddCustomTokenState + ): List { + dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) + val tokenManager = hubState.addCustomTokenManager + val field = getField(Network, hubState) + val selectedNetworkId: String? = field.data.value.let { + if (it == Blockchain.Unknown) null else it + }?.toNetworkId() + val foundTokens = tokenManager.checkAddress(contractAddress, selectedNetworkId) + dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = false)))) + return foundTokens + } + + private suspend fun checkToken( + card: Card?, + token: Coins.CheckAddressResponse.Token, + warningsToAdd: MutableSet, + warningsToRemove: MutableSet, + ) { + val contracts = token.contracts + when { + contracts.isEmpty() -> { + } + contracts.size == 1 -> { + val contract = contracts[0] + val isPersistIntoTheAppAddedTokenList = isPersistIntoTheAppAddedTokenList(token, contract) + + if (isPersistIntoTheAppAddedTokenList) { + warningsToAdd.add(AddCustomTokenWarning.TokenAlreadyAdded) + dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false))) + dispatchOnMain(actionsLockTokenFields()) + } else { + dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true))) +// val isStandardDerivation = card.derivationType == DerivationType.Standard + val isStandardDerivation = true + val isStandardToken = token.active && isStandardDerivation + if (isStandardToken) { + dispatchOnMain(FillTokenFields(token, contract)) + dispatchOnMain(actionsLockTokenFields()) + } else { + warningsToAdd.add(AddCustomTokenWarning.PotentialScamToken) + dispatchOnMain(actionsUnlockTokenFields()) + } + } + } + else -> { + val dialog = DomainDialog.SelectTokenDialog( + items = contracts, + itemNameConverter = { it.address }, + onSelect = { selectedContract -> + hubScope.launch { + // find how to connect to the upper coroutineContext and dispatch through them + dispatchOnMain(FillTokenFields(token, selectedContract)) + dispatchOnMain(FillTokenFields(token, selectedContract)) + } + }, + ) + dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) } } } + private fun isPersistIntoTheAppAddedTokenList( + token: Coins.CheckAddressResponse.Token, + contract: Coins.CheckAddressResponse.Token.Contract + ): Boolean = false + private suspend fun addOrRemoveError(id: CustomTokenFieldId, error: AddCustomTokenError?) { if (error == null) { dispatchOnMain(Error.Remove(id)) @@ -99,38 +188,62 @@ internal object AddCustomTokenHub : BaseStoreHub("AddCusto } } - private inline fun getField(id: FieldId, state: AddCustomTokensState): T { + private fun actionsLockTokenFields(): Action { + val state = domainStore.state.addCustomTokensState + return Screen.UpdateTokenFields(listOf( + Network to state.screenState.network.copy(isEnabled = false), + Name to state.screenState.name.copy(isEnabled = false), + Symbol to state.screenState.symbol.copy(isEnabled = false), + Decimals to state.screenState.decimals.copy(isEnabled = false), + )) + } + + private fun actionsUnlockTokenFields(): Action { + val state = domainStore.state.addCustomTokensState + return Screen.UpdateTokenFields(listOf( + Network to state.screenState.network.copy(isEnabled = true), + Name to state.screenState.name.copy(isEnabled = true), + Symbol to state.screenState.symbol.copy(isEnabled = true), + Decimals to state.screenState.decimals.copy(isEnabled = true), + )) + } + + private inline fun getField(id: FieldId, state: AddCustomTokenState): T { return state.form.getField(id) as T } - private inline fun getValidator(id: FieldId, state: AddCustomTokensState): T { + private inline fun getValidator(id: FieldId, state: AddCustomTokenState): T { return state.getValidator(id) as T } - override fun reduceAction(action: Action, state: AddCustomTokensState): AddCustomTokensState { + override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState { 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 UpdateForm -> { + updateFormState(action.state) } is OnTokenContractAddressChanged -> { val field: TokenField = getField(ContractAddress, state) - field.value = action.value + field.data = action.value + updateFormState(state) + } + is OnTokenNetworkChanged -> { + val field: TokenNetworkField = getField(Network, state) + field.data = action.value updateFormState(state) } is OnTokenDerivationPathChanged -> { - val field: TokenDerivationPathField = getField(Network, state) - field.value = action.value + val field: TokenDerivationPathField = getField(DerivationPath, state) + field.data = action.value + updateFormState(state) + } + is OnTokenDecimalsChanged -> { + val field: TokenField = getField(Decimals, state) + field.data = action.value updateFormState(state) } is OnTokenFieldChanged -> { val field: TokenField = getField(action.id, state) - field.value = action.value + field.data = action.value updateFormState(state) } is Error.Add -> { @@ -142,18 +255,143 @@ internal object AddCustomTokenHub : BaseStoreHub("AddCusto state.copy(formErrors = newMap) } is Warning.Add -> { - val newList = state.warnings.toMutableList().apply { add(action.warning) } - state.copy(warnings = newList) + val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) } + state.copy(warnings = newList.toSet()) } is Warning.Remove -> { - val newList = state.warnings.toMutableList().apply { remove(action.warning) } - state.copy(warnings = newList) + val newList = state.warnings.toMutableSet().apply { removeAll(action.warnings) } + state.copy(warnings = newList.toSet()) + } + is Warning.Replace -> { + val newList = state.warnings.toMutableSet().apply { + removeAll(action.remove) + addAll(action.add) + } + state.copy(warnings = newList.toSet()) + } + is Screen.UpdateTokenFields -> { + var newScreenState = state.screenState + action.pairs.forEach { + newScreenState = when (it.first) { + ContractAddress -> { + if (state.screenState.contractAddressField == it.second) { + newScreenState + } else { + newScreenState.copy(contractAddressField = it.second) + } + } + Network -> { + if (state.screenState.network == it.second) { + newScreenState + } else { + newScreenState.copy(network = it.second) + } + } + Name -> { + if (state.screenState.name == it.second) { + newScreenState + } else { + newScreenState.copy(name = it.second) + } + } + Symbol -> { + if (state.screenState.symbol == it.second) { + newScreenState + } else { + newScreenState.copy(symbol = it.second) + } + } + Decimals -> { + if (state.screenState.decimals == it.second) { + newScreenState + } else { + newScreenState.copy(decimals = it.second) + } + } + else -> newScreenState + } + } + if (state.screenState == newScreenState) { + state + } else { + state.copy(screenState = newScreenState) + } + } + is Screen.UpdateAddButton -> { + val newScreenState = if (state.screenState.addButton == action.addButton) { + state.screenState + } else { + state.screenState.copy(addButton = action.addButton) + } + if (newScreenState == state.screenState) { + state + } else { + state.copy(screenState = newScreenState) + } } else -> state } } - private fun updateFormState(state: AddCustomTokensState): AddCustomTokensState { + private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState { return state.copy(form = Form(state.form.fieldList)) } +} + +//TODO: refactoring: replace by Blockchain.Companion.fromNetworkId +fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain { + return when (networkId) { + "avalanche" -> Blockchain.Avalanche + "binancecoin" -> Blockchain.Binance + "binance-smart-chain" -> Blockchain.BSC + "ethereum" -> Blockchain.Ethereum + "polygon-pos" -> Blockchain.Polygon + "solana" -> Blockchain.Solana + "fantom" -> Blockchain.Fantom + "bitcoin" -> Blockchain.Bitcoin + "bitcoin-cash" -> Blockchain.BitcoinCash + "cardano" -> Blockchain.CardanoShelley + "dogecoin" -> Blockchain.Dogecoin + "ducatus" -> Blockchain.Ducatus + "litecoin" -> Blockchain.Litecoin + "rsk" -> Blockchain.RSK + "stellar" -> Blockchain.Stellar + "tezos" -> Blockchain.Tezos + "ripple" -> Blockchain.XRP + else -> Blockchain.Unknown + } +} + +fun Blockchain.toNetworkId(): String? { + return when (this) { + Blockchain.Unknown -> null + Blockchain.Avalanche -> "avalanche" + Blockchain.AvalancheTestnet -> "avalanche" + Blockchain.Binance -> "binancecoin" + Blockchain.BinanceTestnet -> "binancecoin" + Blockchain.BSC -> "binance-smart-chain" + Blockchain.BSCTestnet -> "binance-smart-chain" + Blockchain.Bitcoin -> "bitcoin" + Blockchain.BitcoinTestnet -> "bitcoin" + Blockchain.BitcoinCash -> "bitcoin-cash" + Blockchain.BitcoinCashTestnet -> "bitcoin-cash" + Blockchain.Cardano -> "cardano" + Blockchain.CardanoShelley -> "cardano" + Blockchain.Dogecoin -> "dogecoin" + Blockchain.Ducatus -> "ducatus" + Blockchain.Ethereum -> "ethereum" + Blockchain.EthereumTestnet -> "ethereum" + Blockchain.Fantom -> "fantom" + Blockchain.FantomTestnet -> "fantom" + Blockchain.Litecoin -> "litecoin" + Blockchain.Polygon -> "matic-network" + Blockchain.PolygonTestnet -> "matic-networks" + Blockchain.RSK -> "rootstock" + Blockchain.Stellar -> "stellar" + Blockchain.StellarTestnet -> "stellar" + Blockchain.Solana -> "solana" + Blockchain.SolanaTestnet -> "solana" + Blockchain.Tezos -> "tezos" + Blockchain.XRP -> "ripple" + } } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt new file mode 100644 index 0000000000..d8ac3b84ee --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -0,0 +1,117 @@ +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 AddCustomTokenState( + val form: Form = Form(createFormFields()), + val formValidators: Map> = createFormValidators(), + val formErrors: Map = emptyMap(), + val warnings: Set = emptySet(), + val screenState: ScreenState = createInitialScreenState(), + val addCustomTokenManager: AddCustomTokenManager = AddCustomTokenManager(TangemTechService()) +) : StateType { + + val completeDataType: CompleteDataType + get() = calculateDataType() + + inline fun visitDataConverter(converter: FieldDataConverter): T { + form.visitDataConverter(converter) + return converter.getConvertedData() + } + + 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 { + fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) { + Blockchain.Unknown -> unknown + Blockchain.Cardano -> "Cardano" + Blockchain.CardanoShelley -> "Cardano Shelley" + else -> blockchain.fullName + } + + fun convertDerivationPathName(blockchain: Blockchain, unknown: String): String = when (blockchain) { + Blockchain.Unknown -> unknown + Blockchain.BSC -> "BNB Smart Chain" + Blockchain.Fantom -> "Fantom Opera" + else -> blockchain.fullName + } + + private fun createFormFields(): List> { + return listOf( + TokenField(ContractAddress), + TokenNetworkField(Network, getSupportedNetworks()), + TokenField(Name), + TokenField(Symbol), + TokenField(Decimals), + TokenDerivationPathField(DerivationPath, getSupportedDerivations()), + ) + } + + private fun createFormValidators(): Map> { + return mapOf( + ContractAddress to TokenContractAddressValidator(), + Network to TokenNetworkValidator(), + Name to StringIsNotEmptyValidator(), + Symbol to StringIsNotEmptyValidator(), + Decimals to TokenDecimalsValidator(), +// DerivationPath to TokenDerivationPathValidator(), + ) + } + + private fun getSupportedNetworks(): List { + return listOf( + Blockchain.Ethereum, + Blockchain.BSC, + Blockchain.Binance, + Blockchain.Polygon, + Blockchain.Avalanche, +// Blockchain.Solana, // not supported until tokens added to the Blockchain SDK + Blockchain.Fantom, + ) + } + + private fun getSupportedDerivations(): List { + val evmBlockchains = Blockchain.values().filter { + !it.isTestnet() && it.getChainId() != null + } + return evmBlockchains + } + + private fun createInitialScreenState(): ScreenState { + return ScreenState( + contractAddressField = ViewStates.TokenField(), + network = ViewStates.TokenField(), + name = ViewStates.TokenField(), + symbol = ViewStates.TokenField(), + decimals = ViewStates.TokenField(), + derivationPath = ViewStates.TokenField(), + addButton = ViewStates.AddButton() + ) + } + } +} \ 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 deleted file mode 100644 index 45976a52d0..0000000000 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokensState.kt +++ /dev/null @@ -1,137 +0,0 @@ -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/features/addCustomToken/redux/Models.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt new file mode 100644 index 0000000000..2e1d3bf88e --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.features.addCustomToken.redux + +/** +[REDACTED_AUTHOR] + */ +enum class CompleteDataType { + Blockchain, Token +} + +// describes state the screen, except the form fields +data class ScreenState( + val contractAddressField: ViewStates.TokenField, + val network: ViewStates.TokenField, + val name: ViewStates.TokenField, + val symbol: ViewStates.TokenField, + val decimals: ViewStates.TokenField, + val derivationPath: ViewStates.TokenField, + val addButton: ViewStates.AddButton +) + +sealed class ViewStates { + data class TokenField( + val isLoading: Boolean = false, + val isEnabled: Boolean = true, + val isVisible: Boolean = true, + ) : ViewStates() + + data class AddButton( + val isEnabled: Boolean = true + ) : ViewStates() +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/DomainState.kt b/domain/src/main/java/com/tangem/domain/redux/DomainState.kt new file mode 100644 index 0000000000..49cc235412 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/DomainState.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.redux + +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.redux.global.DomainGlobalState +import org.rekotlin.StateType + +/** +[REDACTED_AUTHOR] + */ +data class DomainState( + val globalState: DomainGlobalState = DomainGlobalState(), + val addCustomTokensState: AddCustomTokenState = AddCustomTokenState(), +) : StateType \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/DomainStore.kt b/domain/src/main/java/com/tangem/domain/redux/DomainStore.kt new file mode 100644 index 0000000000..78bc7cf624 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/DomainStore.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.redux + +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenHub +import com.tangem.domain.redux.global.DomainGlobalHub +import com.tangem.domain.redux.state.observeReducedStates +import org.rekotlin.Store + +/** +[REDACTED_AUTHOR] + */ +private class DomainStore // for simple search + +private val RE_STORE_HUBS: List> = listOf( + DomainGlobalHub(), + AddCustomTokenHub(), +) + +val domainStore = Store( + state = DomainState(), + middleware = RE_STORE_HUBS.map { it.getMiddleware() }, + reducer = { action, state -> + requireNotNull(state) + + // we can examine the store state after each change by reducer + val reducedSates = RE_STORE_HUBS.mapNotNull { + val reducedState = it.reduce(action, state) + if (reducedState == state) null else Pair(action, reducedState) + } + observeReducedStates(reducedSates) + + if (reducedSates.isEmpty()) state else reducedSates.last().second + } +) diff --git a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt new file mode 100644 index 0000000000..c4c98fc143 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt @@ -0,0 +1,107 @@ +package com.tangem.domain.redux + +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 +import java.util.concurrent.Executors + +/** +[REDACTED_AUTHOR] + * ReStoreHub's should not store the or the , because this can lead to destabilization of + * a state behavior. + */ +// all ReStoreHub's must be marked as internal +internal interface ReStoreHub : HubMiddleware, HubReducer + +internal interface HubMiddleware { + fun getMiddleware(): Middleware +} + +internal interface HubReducer { + fun reduce(action: Action, storeState: StoreState): StoreState +} + +/** + * ReStoreHub is the entry point for actions. It processes it through middleware and reducer. + * Actions handled by ReStoreHub go into coroutine scope, which can be canceled while the action is being processed. + * All action went from the middleware must be dispatched through ReStoreHub.dispatchOnMain(Actions) to prevent + * concurrent modification in the Store + * Only the changed hub State will change its state in the DomainState + * @param name - name of the Hub + * @param dispatcher - main coroutine dispatcher for actions + */ +internal abstract class BaseStoreHub( + private val name: String, + private val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() +) : ReStoreHub { + + val hubScope = CoroutineScope( + Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name) + ) + + private val actionsAndJobs = mutableMapOf() + + override fun getMiddleware(): Middleware { + return { 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(storeStateHolder: () -> DomainState?, action: Action, dispatch: DispatchFunction) { + val storeState = storeStateHolder() + ?: throw UnsupportedOperationException("StoreState for the $name can't be NULL") + + hubScope.launch { + actionsAndJobs[action] = this.coroutineContext.job + actionsAndJobs[action]?.invokeOnCompletion { actionsAndJobs.remove(action) } + + handleAction(action, storeState) { + actionsAndJobs.remove(it)?.cancel() + } + } + } + + /** + * Reduce the action and check if - if the action hasn't updated the hubState, then it doesn't need to update + * storeState + */ + override fun reduce(action: Action, storeState: DomainState): DomainState { + val oldState = getHubState(storeState) + val newState = reduceAction(action, oldState) + return if (oldState === newState) { + storeState + } else { + updateStoreState(storeState, newState) + } + } + + protected abstract suspend fun handleAction(action: Action, storeState: DomainState, cancel: ValueCallback) + protected abstract fun reduceAction(action: Action, state: State): State + + protected abstract fun getHubState(storeState: DomainState): State + protected abstract fun updateStoreState(storeState: DomainState, newHubState: State): DomainState + +} + +internal suspend inline fun ReStoreHub<*, *>.dispatchOnMain(vararg actions: Action) { + withMainContext { actions.forEach { domainStore.dispatch(it) } } +} + +internal suspend inline fun ReStoreHub<*, *>.dispatchOnIO(vararg actions: Action) { + withIOContext { actions.forEach { domainStore.dispatch(it) } } +} diff --git a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalAction.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt similarity index 62% rename from domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalAction.kt rename to domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt index ca26927233..1ce8b77035 100644 --- a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalAction.kt +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt @@ -1,5 +1,6 @@ -package com.tangem.domain.features.global.redux +package com.tangem.domain.redux.global +import com.tangem.domain.DomainStateDialog import com.tangem.domain.common.ScanResponse import org.rekotlin.Action @@ -9,4 +10,5 @@ import org.rekotlin.Action //TODO: refactoring: is alias for the GlobalAction sealed class DomainGlobalAction : Action { data class SetScanResponse(val scanResponse: ScanResponse?) : DomainGlobalAction() + data class ShowDialog(val stateDialog: DomainStateDialog?) : DomainGlobalAction() } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalHub.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt similarity index 63% rename from domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalHub.kt rename to domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt index 40ea093ae5..494dc41e7e 100644 --- a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalHub.kt +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt @@ -1,10 +1,9 @@ -package com.tangem.domain.features.global.redux +package com.tangem.domain.redux.global import android.webkit.ValueCallback -import com.tangem.domain.restore.BaseStoreHub -import com.tangem.domain.restore.DomainState +import com.tangem.domain.redux.BaseStoreHub +import com.tangem.domain.redux.DomainState import org.rekotlin.Action -import org.rekotlin.DispatchFunction /** [REDACTED_AUTHOR] @@ -16,19 +15,18 @@ internal class DomainGlobalHub : BaseStoreHub("DomainGlobalHu return storeState.globalState } - override fun updateStoreState(storeState: DomainState, newState: DomainGlobalState): DomainState { - return storeState.copy(globalState = newState) + override fun updateStoreState(storeState: DomainState, newHubState: DomainGlobalState): DomainState { + return storeState.copy(globalState = newHubState) } override suspend fun handleAction( - state: DomainState, action: Action, - dispatch: DispatchFunction, + storeState: DomainState, cancel: ValueCallback ) { if (action !is DomainGlobalAction) return - val state = state.globalState + val state = storeState.globalState when (action) { } @@ -38,6 +36,9 @@ internal class DomainGlobalHub : BaseStoreHub("DomainGlobalHu is DomainGlobalAction.SetScanResponse -> { state.copy(scanResponse = action.scanResponse) } + is DomainGlobalAction.ShowDialog -> { + state.copy(dialog = action.stateDialog) + } else -> state } } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalState.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt similarity index 61% rename from domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalState.kt rename to domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt index 6335cc79b3..b629778ccc 100644 --- a/domain/src/main/java/com/tangem/domain/features/global/redux/DomainGlobalState.kt +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt @@ -1,5 +1,6 @@ -package com.tangem.domain.features.global.redux +package com.tangem.domain.redux.global +import com.tangem.domain.DomainStateDialog import com.tangem.domain.common.ScanResponse /** @@ -8,4 +9,5 @@ import com.tangem.domain.common.ScanResponse //TODO: refactoring: is alias for the GlobalState data class DomainGlobalState( val scanResponse: ScanResponse? = null, + val dialog: DomainStateDialog? = null, ) diff --git a/domain/src/main/java/com/tangem/domain/redux/state/StateObservables.kt b/domain/src/main/java/com/tangem/domain/redux/state/StateObservables.kt new file mode 100644 index 0000000000..233bbaa618 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/state/StateObservables.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.redux.state + +import com.tangem.domain.features.addCustomToken.AddCustomTokenStatePrinter +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.redux.DomainState +import org.rekotlin.Action +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + * Use it only in debug mode! + */ +internal fun observeReducedStates(reducedSates: List>) { + // we can add any logic to watch for changes of actions, states, etc. + val isDebugMode = true + if (!isDebugMode) return + + logStates(reducedSates) +} + +private fun logStates(reducedSates: List>) { + reducedSates.forEach { + val printer = statePrinters.firstNotNullOfOrNull { entry -> + if (entry.key.isAssignableFrom(it.first::class.java)) entry.value else null + } ?: return@forEach + + val messageToPrint = printer.print(it.first, it.second) ?: return@forEach + + Timber.d(messageToPrint) + } +} + +// TODO: refactoring: mutate to factory +private val statePrinters = mutableMapOf( + AddCustomTokenAction::class.java to AddCustomTokenStatePrinter() +) \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/state/StatePrinter.kt b/domain/src/main/java/com/tangem/domain/redux/state/StatePrinter.kt new file mode 100644 index 0000000000..8f617eea4d --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/redux/state/StatePrinter.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.redux.state + +import com.tangem.domain.redux.DomainState +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +interface StatePrinter { + fun print(action: Action, domainState: DomainState): String? + fun getStateObject(domainState: DomainState): S +} \ 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 deleted file mode 100644 index 57e8c8401d..0000000000 --- a/domain/src/main/java/com/tangem/domain/store/DomainStore.kt +++ /dev/null @@ -1,37 +0,0 @@ -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 deleted file mode 100644 index 7322b42e91..0000000000 --- a/domain/src/main/java/com/tangem/domain/store/StoreHub.kt +++ /dev/null @@ -1,87 +0,0 @@ -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 From 4fc058cd2d0bc9a3d3d2d6609444a62d8544b670 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Apr 2022 16:50:32 +0300 Subject: [PATCH 18/28] Updated on 2026-08-14 --- app/src/main/res/values-ru/strings.xml | 9 +++++++-- app/src/main/res/values/strings_untranslated.xml | 8 ++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index aa98785f5c..02169fe239 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -200,12 +200,15 @@ Добавлен Удалить токен Значок токена + Добавить токен Адрес контракта Пожалуйста, выберите сеть Пожалуйста, заполните все поля - Количество знаков после запятой некорректно + Количество знаков после запятой должно быть корректным числом не больше %d Адрес контракта некорректен Путь деривации некорректен + Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить + Этот токен/сеть уже находится в вашем списке Сеть Токен Знаков после запятой @@ -215,7 +218,9 @@ Название токена Например, USDC Символ токена - Путь деривации (необязательно) + Деривация по BIP44 + По-умолчанию + Добавить токен Блокчейн Управление токенами Блокчейны diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 1e50f240c9..be969d2aae 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -86,9 +86,11 @@ Contract address Please select the network Please fill in all the fields - Decimal number is invalid + Decimal number must be a valid integer, no higher than %d Contract address is invalid Derivation path is invalid + Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. + This token/network has already been added to your list Network Token Decimals @@ -98,7 +100,9 @@ Name E.g. USDC Token symbol - Derivation Path (optional) + BIP44 coin type + Default + Add token //Details Blockchain From a73558501c5f1e6e7fe3a827c062b365382e3361 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Apr 2022 16:51:32 +0300 Subject: [PATCH 19/28] Updated on 2026-08-14 --- .../com/tangem/tap/common/compose/Button.kt | 96 ++++-- .../common/compose/ComposableTextDebouncer.kt | 24 +- .../common/compose/ComposeDialogManager.kt | 125 ++++++++ .../common/compose/ComposeKeyboardObserver.kt | 40 +++ .../tangem/tap/common/compose/ErrorViews.kt | 16 + .../java/com/tangem/tap/common/compose/Log.kt | 12 - .../tap/common/compose/OutlinedSpinner.kt | 18 +- .../common/compose/OutlinedTextFieldWidget.kt | 78 ++--- .../compose => compose/extensions}/Color.kt | 2 +- .../common/compose/extensions/Resources.kt | 21 ++ .../home/compose/StoriesGeneralContent.kt | 2 +- .../addCustomToken/AddCustomTokenFragment.kt | 62 ++++ .../CustomTokenErrorConverter.kt | 44 +++ .../compose/AddCustomTokenScreen.kt | 283 ++++++++++++++++++ .../compose/AddCustomTokenViews.kt | 66 ++++ .../compose/HangingOverKeyboardView.kt | 56 ++++ 16 files changed, 849 insertions(+), 96 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/ComposeKeyboardObserver.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/Log.kt rename app/src/main/java/com/tangem/tap/common/{extensions/compose => compose/extensions}/Color.kt (91%) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenViews.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt index fd450f68bd..64b9daa152 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Button.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -1,71 +1,115 @@ package com.tangem.tap.common.compose -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.height -import androidx.compose.material.Button -import androidx.compose.material.Scaffold -import androidx.compose.material.Text +import androidx.compose.foundation.layout.* +import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.tangem.tap.common.compose.extensions.stringResourceDefault +import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -private class Button {} - @Composable fun Button( + modifier: Modifier = Modifier, text: String = "", textId: Int? = null, isEnabled: Boolean = true, - modifier: Modifier = Modifier, - leftContent: @Composable RowScope.() -> Unit = {}, - rightContent: @Composable RowScope.() -> Unit = {}, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + leadingView: @Composable RowScope.() -> Unit = {}, + middleView: @Composable RowScope.() -> Unit = { TextInButton(text = text, textId = textId) }, + trailingView: @Composable RowScope.() -> Unit = {}, onClick: () -> Unit, ) { Button( - modifier = modifier.height(42.dp), + modifier = modifier, + contentPadding = contentPadding, enabled = isEnabled, onClick = onClick, ) { - leftContent() - ButtonText(text = textId?.let { stringResource(id = it) } ?: text) - rightContent() + leadingView() + middleView() + trailingView() } } @Composable -fun ButtonText( - text: String, - modifier: Modifier = Modifier +private fun TextInButton( + modifier: Modifier = Modifier, + text: String = "", + textId: Int? = null, ) { Text( - text, modifier = modifier, + text = stringResourceDefault(textId, text), maxLines = 1, style = TextStyle( fontSize = 16.sp, lineHeight = 20.sp, fontWeight = FontWeight.Medium, - textAlign = TextAlign.Center, ) ) } +@Composable +fun PasteButton( + modifier: Modifier = Modifier, + enabled: Boolean = true, + dpSize: DpSize = DpSize(40.dp, 40.dp), + onClick: () -> Unit, + content: @Composable (() -> Unit)? = null +) { + IconButton( + modifier = modifier.size(dpSize), + enabled = enabled, + onClick = onClick, + ) { + when (content) { + null -> { + val icon = if (enabled) R.drawable.ic_paste else R.drawable.ic_paste_disabled + Icon(painterResource(id = icon), contentDescription = "Paste") + } + else -> content() + } + } +} + @Preview @Composable fun ButtonTest() { - Scaffold { - Button( - "Some button", - onClick = {} - ) + Scaffold( + ) { + Column(modifier = Modifier.padding(16.dp)) { + PreviewItem("Button") { + Button(text = "Some button") {} + } + PreviewItem("PasteButton") { + PasteButton(onClick = {}) + } + } } +} + +@Composable +fun PreviewItem( + name: String, + content: @Composable RowScope.() -> Unit, +) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier.weight(1f), + text = name, + ) + content() + } + SpacerH8() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt index 7420dce292..0e9040f220 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposableTextDebouncer.kt @@ -1,26 +1,24 @@ package com.tangem.tap.common.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import com.tangem.domain.common.ValueDebouncer +import com.tangem.domain.common.util.ValueDebouncer /** [REDACTED_AUTHOR] * This is an empty compose view. It just remember the ValueDebouncer inside of itself. */ @Composable -fun ComposableTextDebouncer( - text: String, +fun valueDebouncerAsState( debounce: Long = 400, - onTextChanged: (String) -> Unit -): ValueDebouncer { - val rTextDebounce = remember { - mutableStateOf(ValueDebouncer(text, debounce) { changedValue -> - changedValue?.let { onTextChanged(it) } - }) + onValueChanged: (T) -> Unit +): ValueDebouncer { + return remember { + ValueDebouncer( + debounce = debounce, + onValueChanged = { changedValue -> + changedValue?.let { onValueChanged(it) } + }, + ) } - rTextDebounce.value.value = text - - return rTextDebounce.value } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt new file mode 100644 index 0000000000..d1f540e7e1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -0,0 +1,125 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.tangem.domain.DomainDialog +import com.tangem.domain.DomainStateDialog +import com.tangem.domain.redux.domainStore +import com.tangem.domain.redux.global.DomainGlobalAction +import com.tangem.domain.redux.global.DomainGlobalState +import org.rekotlin.StoreSubscriber + +@Composable +fun ComposeDialogManager() { + val dialogSate = remember { mutableStateOf(null) } + val subscriber = remember { + object : StoreSubscriber { + override fun newState(state: DomainGlobalState) { + dialogSate.value = state.dialog + } + } + } + + ShowTheDialog(dialogSate) + + LaunchedEffect(key1 = Unit, block = { + domainStore.subscribe(subscriber) { state -> + state.skipRepeats { oldState, newState -> + oldState.globalState == newState.globalState + }.select { it.globalState } + } + }) + DisposableEffect(key1 = Unit, effect = { + onDispose { domainStore.unsubscribe(subscriber) } + }) +} + +@Composable +fun ShowTheDialog(dialogState: MutableState) { + if (dialogState.value == null) return + + val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) } + + when (val dialog = dialogState.value) { + is DomainDialog.SelectTokenDialog -> { + SimpleDialog( + title = "Select a token", + items = dialog.items, + itemNameConverter = dialog.itemNameConverter, + onSelect = dialog.onSelect, + onDismissRequest = onDismissRequest + ) + } + } +} + +/** + * Dialog with single item selection + */ +@Composable +fun SimpleDialog( + title: String, + items: List, + itemNameConverter: (T) -> String, + onSelect: (T) -> Unit, + onDismissRequest: () -> Unit +) { + Dialog( + properties = DialogProperties(false, false), + onDismissRequest = { } + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(22.dp) + ) { + Text( + text = title, + style = LocalTextStyle.provides( + TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ) + ).value + ) + + SpacerH16() + LazyColumn() { + items(items) { item -> + Row( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .clickable { + onSelect(item) + onDismissRequest() + }, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = itemNameConverter(item), + ) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeKeyboardObserver.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeKeyboardObserver.kt new file mode 100644 index 0000000000..fe045a7c9e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeKeyboardObserver.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.common.compose + +import android.graphics.Rect +import android.view.ViewTreeObserver +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalView + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun keyboardObserverAsState(): State { + val keyboardState: MutableState = remember { mutableStateOf(Keyboard.Closed) } + val view = LocalView.current + DisposableEffect(view) { + val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() + view.getWindowVisibleDisplayFrame(rect) + val screenHeight = view.rootView.height + val keypadHeight = screenHeight - rect.bottom + keyboardState.value = if (keypadHeight > screenHeight * 0.15) { + Keyboard.Opened(keypadHeight) + } else { + Keyboard.Closed + } + } + view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) + + onDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) + } + } + + return keyboardState +} + +sealed class Keyboard { + data class Opened(val height: Int) : Keyboard() + object Closed : Keyboard() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt b/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt index 7055706474..49b6625474 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ErrorViews.kt @@ -1,11 +1,16 @@ package com.tangem.tap.common.compose +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.material.LocalTextStyle import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp /** [REDACTED_AUTHOR] @@ -22,4 +27,15 @@ fun ErrorView( modifier = modifier, style = style ) +} + +@Preview +@Composable +fun ErrorViewTest() { + Scaffold( + ) { + Box(Modifier.padding(16.dp)) { + ErrorView(text = "Some error description") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/Log.kt b/app/src/main/java/com/tangem/tap/common/compose/Log.kt deleted file mode 100644 index 851798dfe1..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/Log.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.common.compose - -import androidx.compose.runtime.Composable -import timber.log.Timber - -/** - * Simple logger for all recompositions - */ -@Composable -fun LogSideEffect(message: String) { - Timber.d("SideEffect: $message") -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt index 7be01ec1d0..ef87477734 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt @@ -8,24 +8,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback +import com.tangem.domain.common.form.Field import com.tangem.tap.common.extensions.ValueCallback /** [REDACTED_AUTHOR] */ +private class OutlinedSpinner + @OptIn(ExperimentalMaterialApi::class) @Composable fun OutlinedSpinner( + modifier: Modifier = Modifier, title: String, itemList: List, - selectedItem: T, + selectedItem: Field.Data, onItemSelected: ValueCallback, - modifier: Modifier = Modifier, itemNameConverter: (T) -> String = { it.toString() }, + isEnabled: Boolean = true, onClose: VoidCallback = {} ) { - val rSelectedItem = remember { mutableStateOf(selectedItem) } val rIsExpanded = remember { mutableStateOf(false) } + val rSelectedItem = remember { mutableStateOf(selectedItem.value) } + if (!selectedItem.isUserInput) { + rSelectedItem.value = selectedItem.value + } val onItemSelectedInternal: (T) -> Unit = { rSelectedItem.value = it @@ -44,6 +51,7 @@ fun OutlinedSpinner( OutlinedTextField( modifier = modifier, readOnly = true, + enabled = isEnabled, value = itemNameConverter(rSelectedItem.value), onValueChange = {}, label = { Text(title) }, @@ -66,12 +74,12 @@ fun OutlinedSpinner( @Preview @Composable -fun TestSpinnerPreview(){ +fun TestSpinnerPreview() { Scaffold() { OutlinedSpinner( title = "Blockchain name", itemList = listOf(Blockchain.values()), - selectedItem = Blockchain.Avalanche, + selectedItem = Field.Data(Blockchain.Avalanche), onItemSelected = {}, ) } diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index 65429321eb..b49579609d 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.LinearProgressIndicator import androidx.compose.material.OutlinedTextField import androidx.compose.material.Scaffold @@ -14,14 +15,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.tangem.domain.common.DomainError -import com.tangem.domain.common.ErrorConverter +import com.tangem.domain.DomainError +import com.tangem.domain.ErrorConverter +import com.tangem.domain.common.form.Field +import com.tangem.tap.common.compose.extensions.stringResourceDefault /** [REDACTED_AUTHOR] @@ -30,8 +32,8 @@ private class OutlinedTextFieldWidget @Composable fun OutlinedTextFieldWidget( - text: String, modifier: Modifier = Modifier, + textFieldData: Field.Data, labelId: Int? = null, label: String = "", placeholderId: Int? = null, @@ -44,62 +46,66 @@ fun OutlinedTextFieldWidget( errorConverter: ErrorConverter? = null, debounceTextChanges: Long = 400, visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, onTextChanged: (String) -> Unit, ) { if (!isVisible) return - val placeholder = placeholderId?.let { stringResource(id = it) } ?: placeholder - val label = labelId?.let { stringResource(id = it) } ?: label - Column( modifier = modifier.animateContentSize(), ) { OutlinedProgressTextField( - text = text, modifier = modifier, - label = label, - placeholder = placeholder, + textFieldData = textFieldData, + label = stringResourceDefault(labelId, label), + placeholder = stringResourceDefault(placeholderId, placeholder), trailingIcon = trailingIcon, isEnabled = isEnabled, isLoading = isLoading, error = error, - debounceTextChanges = debounceTextChanges, + debounce = debounceTextChanges, visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, onTextChanged = onTextChanged ) - errorConverter?.let { TextFieldErrorWidget(error, it) } + errorConverter?.let { AnimatedErrorView(error, it) } } } @Composable private fun OutlinedProgressTextField( - text: String, modifier: Modifier = Modifier, + textFieldData: Field.Data, label: String = "", placeholder: String = "", isEnabled: Boolean = true, isLoading: Boolean = false, error: DomainError? = null, - debounceTextChanges: Long = 400, + debounce: Long = 400, visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, trailingIcon: @Composable (() -> Unit)? = null, onTextChanged: (String) -> Unit, ) { - val rTextValue = remember { mutableStateOf(text) } - val textDebouncer = ComposableTextDebouncer(text, debounceTextChanges, onTextChanged) + val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged) + val rText = remember { mutableStateOf(textFieldData.value) } - // add ability to paste text from state - if (rTextValue.value != text) rTextValue.value = text + fun updateFieldValueAndEmmit(value: String){ + rText.value = value + rTextDebouncer.emmit(value) + } + // This action came from redux. Update the field value and send a new event as if from the user + if (!textFieldData.isUserInput) { + updateFieldValueAndEmmit(textFieldData.value) + } Box { OutlinedTextField( - value = rTextValue.value, - onValueChange = { - // immediately change text for the OutlinedTextField - rTextValue.value = it - textDebouncer.emmit(it) - }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth(), + value = rText.value, + onValueChange = ::updateFieldValueAndEmmit, + keyboardOptions = keyboardOptions, label = { Text(label) }, placeholder = { Text(placeholder) }, trailingIcon = trailingIcon, @@ -120,7 +126,7 @@ private fun OutlinedProgressTextField( } @Composable -private fun TextFieldErrorWidget( +private fun AnimatedErrorView( error: DomainError? = null, errorConverter: ErrorConverter, ) { @@ -162,41 +168,37 @@ fun OutlinedTextFieldWithErrorTest() { ) { OutlinedTextFieldWidget( modifier = modifier, - text = "", + textFieldData = Field.Data(""), label = "First label", placeholder = "1 placeholder", error = null, errorConverter = converter, - onTextChanged = {}, - ) + ) {} OutlinedTextFieldWidget( modifier = modifier, - text = "First", + textFieldData = Field.Data("First"), label = "First label", placeholder = "1 placeholder", error = null, errorConverter = converter, - onTextChanged = {}, - ) + ) {} OutlinedTextFieldWidget( modifier = modifier, - text = "First", + textFieldData = Field.Data("First"), label = "First label", placeholder = "1 placeholder", isLoading = true, error = null, errorConverter = converter, - onTextChanged = {}, - ) + ) {} OutlinedTextFieldWidget( modifier = modifier, - text = "First", + textFieldData = Field.Data("First"), label = "First label", placeholder = "1 placeholder", error = SimpleError(), errorConverter = converter, - onTextChanged = {}, - ) + ) {} } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Color.kt similarity index 91% rename from app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt rename to app/src/main/java/com/tangem/tap/common/compose/extensions/Color.kt index fb4d02453c..d25bc9e26b 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/compose/Color.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Color.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common.extensions.compose +package com.tangem.tap.common.compose.extensions import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt new file mode 100644 index 0000000000..5b8da92e24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Resources.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.common.compose.extensions + +import android.content.res.Resources +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun stringResourceDefault(@StringRes id: Int?, default: String = ""): String { + val resources = LocalContext.current.resources + return try { + resources.getString(requireNotNull(id)) + } catch (ex: Resources.NotFoundException) { + default + } catch (ex: IllegalArgumentException) { + default + } +} \ No newline at end of file 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 05c11fbd75..410a8118aa 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 @@ -24,7 +24,7 @@ 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.toAndroidGraphicsColor +import com.tangem.tap.common.compose.extensions.toAndroidGraphicsColor import com.tangem.wallet.R @Composable diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt new file mode 100644 index 0000000000..0c6e8bc2a1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/AddCustomTokenFragment.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.features.tokens.addCustomToken + +import android.os.Bundle +import android.view.View +import android.view.WindowManager +import androidx.appcompat.widget.Toolbar +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.redux.domainStore +import com.tangem.tap.features.BaseStoreFragment +import com.tangem.tap.features.tokens.addCustomToken.compose.AddCustomTokenScreen +import com.tangem.wallet.R +import org.rekotlin.StoreSubscriber + +/** +[REDACTED_AUTHOR] + */ +class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment), StoreSubscriber { + + private var state: MutableState = mutableStateOf(domainStore.state.addCustomTokensState) + + override fun subscribeToStore() { + domainStore.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.addCustomTokensState == newState.addCustomTokensState + }.select { it.addCustomTokensState } + } + } + + override fun newState(state: AddCustomTokenState) { + if (activity == null || view == null) return + + this.state.value = state + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + requireActivity().window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + view.findViewById(R.id.toolbar)?.let { + it.setTitle(R.string.add_custom_token_title) + } + + view.findViewById(R.id.view_compose)?.setContent { + AppCompatTheme(requireContext()) { + Box(modifier = Modifier + .fillMaxSize() + ) { + AddCustomTokenScreen(state) + } + + } + } +// addBackPressHandler(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt new file mode 100644 index 0000000000..17373aa948 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt @@ -0,0 +1,44 @@ +package com.tangem.tap.features.tokens.addCustomToken + +import android.content.Context +import com.tangem.domain.DomainError +import com.tangem.domain.ErrorConverter +import com.tangem.domain.features.addCustomToken.AddCustomTokenError +import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +class CustomTokenErrorConverter( + private val context: Context +) : ErrorConverter { + + override fun convertError(error: DomainError): String { + val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException() + + val resId = when (customTokenError) { + AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address + AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected + AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path + AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_empty_fields + else -> null + } + return resId?.let { context.getString(it) } ?: "Unknown error: ${customTokenError::class.java.simpleName}" + } +} + +class CustomTokenWarningConverter( + private val context: Context +) : ErrorConverter { + + override fun convertError(error: DomainError): String { + val customTokenWarning = (error as? AddCustomTokenWarning) ?: throw UnsupportedOperationException() + + val resId = when (customTokenWarning) { + AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found + AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added + } + return context.getString(resId) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt new file mode 100644 index 0000000000..f1cc9dcfbf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -0,0 +1,283 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.domain.ErrorConverter +import com.tangem.domain.common.form.DataField +import com.tangem.domain.common.form.Field +import com.tangem.domain.common.form.FieldId +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.features.addCustomToken.redux.AddCustomTokenAction.* +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.features.addCustomToken.redux.ScreenState +import com.tangem.domain.features.addCustomToken.redux.ViewStates +import com.tangem.domain.redux.domainStore +import com.tangem.tap.common.compose.ComposeDialogManager +import com.tangem.tap.common.compose.OutlinedTextFieldWidget +import com.tangem.tap.common.compose.SpacerH8 +import com.tangem.tap.common.compose.keyboardObserverAsState +import com.tangem.tap.features.tokens.addCustomToken.CustomTokenErrorConverter +import com.tangem.tap.features.tokens.addCustomToken.CustomTokenWarningConverter +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +private class AddCustomTokenScreen {} // for simple search + +@Composable +fun AddCustomTokenScreen(state: MutableState) { + val scaffoldState = rememberScaffoldState() + + Scaffold( + scaffoldState = scaffoldState, + backgroundColor = colorResource(id = R.color.backgroundLightGray), + ) { + Box(Modifier.fillMaxSize()) { + LazyColumn( + contentPadding = PaddingValues(bottom = 80.dp) + ) { + item { + Surface( + modifier = Modifier.padding(16.dp), + shape = RoundedCornerShape(4.dp), + elevation = 4.dp, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + FormFields(state) + } + } + } + item { Warnings(state.value.warnings.toList()) } + } + HangingOverKeyboardView( + modifier = Modifier + .align(Alignment.BottomCenter), + keyboardState = keyboardObserverAsState(), + defaultBottomPadding = 30.dp, + spaceBetweenKeyboard = 20.dp, + ) { + AddButton( + isEnabled = state.value.screenState.addButton.isEnabled + ) { + } + } + } + ComposeDialogManager() + } + + LaunchedEffect(key1 = Unit, block = { domainStore.dispatch(AddCustomTokenAction.OnCreate) }) + DisposableEffect(key1 = Unit, effect = { onDispose { domainStore.dispatch(AddCustomTokenAction.OnDestroy) } }) +} + +@Composable +private fun FormFields(state: MutableState) { + val context = LocalContext.current + val errorConverter = remember { CustomTokenErrorConverter(context) } + + state.value.form.fieldList.forEach { field -> + val data = ScreenFieldData.fromState(field, state.value, errorConverter) + when (field.id) { + ContractAddress -> TokenContractAddressView(data) + Network -> TokenNetworkView(data) + Name -> TokenNameView(data) + Symbol -> TokenSymbolView(data) + Decimals -> TokenDecimalsView(data) + DerivationPath -> TokenDerivationPathView(data) + } + } +} + +@Composable +private fun TokenContractAddressView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_contract_address_input_title, + placeholder = "0x0000000000000000", + isEnabled = screenFieldData.viewState.isEnabled, + isLoading = screenFieldData.viewState.isLoading, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { + domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it))) + } + SpacerH8() +} + +@Composable +private fun TokenNameView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_name_input_title, + placeholderId = R.string.custom_token_name_input_placeholder, + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { + domainStore.dispatch(OnTokenFieldChanged(screenFieldData.field.id, Field.Data(it))) + } + SpacerH8() +} + +@Composable +private fun TokenNetworkView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) + val networkField = screenFieldData.field as TokenNetworkField + + TokenNetworkSpinner( + title = R.string.custom_token_network_input_title, + itemList = networkField.itemList, + selectedItem = networkField.data, + isEnabled = screenFieldData.viewState.isEnabled, + itemNameConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) }, + ) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +private fun TokenSymbolView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_token_symbol_input_title, + placeholderId = R.string.custom_token_token_symbol_input_placeholder, + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { domainStore.dispatch(OnTokenFieldChanged(screenFieldData.field.id, Field.Data(it))) } + SpacerH8() +} + +@Composable +private fun TokenDecimalsView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_decimals_input_title, + placeholder = "8", + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +private fun TokenDerivationPathView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) + val networkField = screenFieldData.field as TokenDerivationPathField + + TokenNetworkSpinner( + title = R.string.custom_token_network_input_title, + itemList = networkField.itemList, + selectedItem = networkField.data, + isEnabled = screenFieldData.viewState.isEnabled, + itemNameConverter = { AddCustomTokenState.convertDerivationPathName(it, notSelected) }, + ) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +private fun Warnings(warnings: List) { + if (warnings.isEmpty()) return + + val context = LocalContext.current + val warningConverter = remember { CustomTokenWarningConverter(context) } + + Column { + warnings.forEachIndexed { index, item -> + val modifier = when (index) { + 0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 16.dp) + warnings.lastIndex -> Modifier.padding(16.dp, 16.dp, 16.dp, 16.dp) + else -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp) + } + Surface( + modifier = modifier.fillMaxWidth(), + shape = RoundedCornerShape(4.dp), + color = colorResource(id = R.color.darkGray2), + contentColor = colorResource(id = R.color.darkGray3) + ) { + Text( + modifier = Modifier.padding(16.dp), + text = warningConverter.convertError(item), + color = colorResource(id = R.color.lightGray0), + fontSize = 14.sp + ) + } + } + } +} + +private data class ScreenFieldData( + val field: DataField<*>, + val error: AddCustomTokenError?, + val errorConverter: ErrorConverter, + val viewState: ViewStates.TokenField +) { + companion object { + fun fromState( + field: DataField<*>, + state: AddCustomTokenState, + errorConverter: CustomTokenErrorConverter + ): ScreenFieldData { + return ScreenFieldData( + field = field, + error = state.getError(field.id), + errorConverter = errorConverter, + viewState = selectField(field.id, state.screenState) + ) + } + + private fun selectField(id: FieldId, screenState: ScreenState): ViewStates.TokenField { + return when (id) { + ContractAddress -> screenState.contractAddressField + Network -> screenState.network + Name -> screenState.name + Symbol -> screenState.symbol + Decimals -> screenState.decimals + DerivationPath -> screenState.derivationPath + else -> throw UnsupportedOperationException() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenViews.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenViews.kt new file mode 100644 index 0000000000..dee8d3b535 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenViews.kt @@ -0,0 +1,66 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.form.Field +import com.tangem.tap.common.compose.Button +import com.tangem.tap.common.compose.OutlinedSpinner +import com.tangem.tap.common.extensions.ValueCallback +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun TokenNetworkSpinner( + title: Int, + itemList: List, + selectedItem: Field.Data, + isEnabled: Boolean = true, + itemNameConverter: (Blockchain) -> String, + onItemSelected: ValueCallback, +) { + + OutlinedSpinner( + modifier = Modifier.fillMaxWidth(), + title = stringResource(id = title), + itemList = itemList, + selectedItem = selectedItem, + itemNameConverter = itemNameConverter, + isEnabled = isEnabled, + onItemSelected = onItemSelected + ) +} + +@Composable +fun AddButton( + modifier: Modifier = Modifier, + isEnabled: Boolean, + textId: Int = R.string.common_add, + onClick: () -> Unit, +) { + Button( + textId = textId, + isEnabled = isEnabled, + modifier = modifier + .height(52.dp) + .padding(horizontal = 16.dp) + .fillMaxWidth(), + leadingView = { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "Add", + ) + }, + onClick = onClick + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt new file mode 100644 index 0000000000..0a8f736373 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt @@ -0,0 +1,56 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import android.content.Context +import android.util.TypedValue +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.tangem_sdk_new.extensions.pxToDp +import com.tangem.tap.common.compose.Keyboard + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun HangingOverKeyboardView( + modifier: Modifier = Modifier, + keyboardState: State, + defaultBottomPadding: Dp = 0.dp, + spaceBetweenKeyboard: Dp = 10.dp, + calculateWithActionBarHeight: Boolean = true, + content: @Composable() (BoxScope.() -> Unit) +) { + fun getActionBarHeight(context: Context): Int { + val typedValue = TypedValue() + return if (context.theme.resolveAttribute(android.R.attr.actionBarSize, typedValue, true)) { + val data = typedValue.data + val displayMetrics = context.resources.displayMetrics + TypedValue.complexToDimensionPixelSize(data, displayMetrics) + } else { + 0 + } + } + + val context = LocalContext.current + val calculatedPadding = when (keyboardState.value) { + Keyboard.Closed -> defaultBottomPadding + is Keyboard.Opened -> { + val keyboardHeight = (keyboardState.value as Keyboard.Opened).height + val keyboardPadding = context.pxToDp(keyboardHeight.toFloat()).dp + if (calculateWithActionBarHeight) { + val actionBarHeight = context.pxToDp(getActionBarHeight(context).toFloat()).dp + keyboardPadding + spaceBetweenKeyboard - actionBarHeight + } else { + keyboardPadding + spaceBetweenKeyboard + } + + } + } + Box(modifier.padding(bottom = calculatedPadding)) { content() } +} \ No newline at end of file From b718f390489f45115e5618fcc45acb62250c5a30 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Apr 2022 17:08:21 +0300 Subject: [PATCH 20/28] Updated on 2026-08-14 --- .../src/main/java/com/tangem/domain/common/TapWorkarounds.kt | 2 +- .../tangem/domain/features/addCustomToken/CompleteData.kt | 5 ++++- .../tangem/domain/features/addCustomToken/redux/Models.kt | 4 ---- domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt | 4 ++-- .../main/java/com/tangem/network/api/tangemTech/Responses.kt | 2 +- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index 61754302fb..d60e1ef857 100644 --- a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -36,7 +36,7 @@ object TapWorkarounds { ) private val tangemWalletBatchesWithStandardDerivationType = listOf( - "AC01", "AC02" + "AC01", "AC02", "CB95" ) fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt index 9f9c4af6f7..88ef38e4cd 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt @@ -4,11 +4,14 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.form.BaseFieldDataConverter import com.tangem.domain.common.form.FieldDataConverter import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.redux.CompleteDataType /** [REDACTED_AUTHOR] */ +enum class CompleteDataType { + Blockchain, Token +} + sealed class CompleteData() { companion object { diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt index 2e1d3bf88e..3dba21a577 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt @@ -3,10 +3,6 @@ package com.tangem.domain.features.addCustomToken.redux /** [REDACTED_AUTHOR] */ -enum class CompleteDataType { - Blockchain, Token -} - // describes state the screen, except the form fields data class ScreenState( val contractAddressField: ViewStates.TokenField, diff --git a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt index c4c98fc143..183599aa4a 100644 --- a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt +++ b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt @@ -14,8 +14,8 @@ import java.util.concurrent.Executors [REDACTED_AUTHOR] * ReStoreHub's should not store the or the , because this can lead to destabilization of * a state behavior. + * All ReStoreHub's must be marked as internal */ -// all ReStoreHub's must be marked as internal internal interface ReStoreHub : HubMiddleware, HubReducer internal interface HubMiddleware { @@ -77,7 +77,7 @@ internal abstract class BaseStoreHub( } /** - * Reduce the action and check if - if the action hasn't updated the hubState, then it doesn't need to update + * Reduce the action and check it. If the action hasn't updated the hubState, then it doesn't need to update * storeState */ override fun reduce(action: Action, storeState: DomainState): DomainState { diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt index 6d73a3955f..0c9029673d 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt @@ -6,7 +6,7 @@ import java.math.BigDecimal [REDACTED_AUTHOR] */ interface HttpResponse -interface TangemTechResponse : HttpResponse +sealed interface TangemTechResponse : HttpResponse sealed class Coins : TangemTechResponse { data class PricesResponse(val prices: List) : Coins() { From 0b0088f613de0823cdfda5f42f5e800bfab66516 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Apr 2022 10:24:25 +0300 Subject: [PATCH 21/28] Updated on 2026-08-14 --- .../java/com/tangem/domain/DomainException.kt | 13 ++++++ .../domain/common/form/FieldsValidators.kt | 9 +++- .../redux/AddCustomTokenAction.kt | 3 +- .../addCustomToken/redux/AddCustomTokenHub.kt | 41 +++++++++++++------ .../redux/AddCustomTokenState.kt | 7 ++-- 5 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 domain/src/main/java/com/tangem/domain/DomainException.kt diff --git a/domain/src/main/java/com/tangem/domain/DomainException.kt b/domain/src/main/java/com/tangem/domain/DomainException.kt new file mode 100644 index 0000000000..4788eed8f8 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainException.kt @@ -0,0 +1,13 @@ +package com.tangem.domain + +/** +[REDACTED_AUTHOR] + * Must be handled by the module or sent to Crashlytics + */ +interface DomainInternalException + +sealed class DomainException(message: String?) : Throwable(message), DomainInternalException { + data class SelectTokeNetworkException(val networkId: String) : DomainException( + "Unknown network [$networkId] should not be included in the network selection dialog." + ) +} \ No newline at end of file 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 index ff2d9398e7..83ba44c309 100644 --- a/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt +++ b/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt @@ -34,7 +34,6 @@ class TokenContractAddressValidator : CustomTokenValidator() { AddCustomTokenError.InvalidContractAddress } } - } class TokenNetworkValidator : CustomTokenValidator() { @@ -44,6 +43,14 @@ class TokenNetworkValidator : CustomTokenValidator() { } } +class TokenNameValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) +} + +class TokenSymbolValidator : CustomTokenValidator() { + override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data) +} + class TokenDecimalsValidator : CustomTokenValidator() { override fun validate(data: String?): AddCustomTokenError? { val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty 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 index d4eda81f69..4b1a940dcf 100644 --- 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 @@ -16,9 +16,10 @@ sealed class AddCustomTokenAction : Action { // from user, ui object OnCreate : AddCustomTokenAction() object OnDestroy : AddCustomTokenAction() - data class OnTokenFieldChanged(val id: FieldId, val value: Field.Data) : AddCustomTokenAction() data class OnTokenContractAddressChanged(val value: Field.Data) : AddCustomTokenAction() data class OnTokenNetworkChanged(val value: Field.Data) : AddCustomTokenAction() + data class OnTokenNameChanged(val value: Field.Data) : AddCustomTokenAction() + data class OnTokenSymbolChanged(val value: Field.Data) : AddCustomTokenAction() data class OnTokenDerivationPathChanged(val value: Field.Data) : AddCustomTokenAction() data class OnTokenDecimalsChanged(val value: Field.Data) : AddCustomTokenAction() data class OnCustomTokenSelected(val any: Any = Unit) : AddCustomTokenAction() 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 index 62eef658b1..0077bd1907 100644 --- 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 @@ -3,7 +3,9 @@ package com.tangem.domain.features.addCustomToken.redux import android.webkit.ValueCallback import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.Card +import com.tangem.common.services.Result import com.tangem.domain.DomainDialog +import com.tangem.domain.DomainException import com.tangem.domain.common.form.* import com.tangem.domain.features.addCustomToken.* import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* @@ -73,21 +75,29 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } } is OnTokenNetworkChanged -> { - val validator: TokenNetworkValidator = getValidator(Network, hubState) - addOrRemoveError(Network, validator.validate(action.value.value)) +// val validator: TokenNetworkValidator = getValidator(Network, hubState) +// addOrRemoveError(Network, validator.validate(action.value.value)) + +// dispatchOnMain(OnTokenContractAddressChanged(Field.Data( +// getField(ContractAddress, hubState).data.value, false +// ))) } - is OnTokenDerivationPathChanged -> { -// val validator: TokenDerivationPathValidator = getValidator(DerivationPath, hubState) -// addOrRemoveError(DerivationPath, validator.validate(action.value.value)) + is OnTokenNameChanged -> { + val validator: TokenNameValidator = getValidator(Name, hubState) + addOrRemoveError(Name, validator.validate(action.value.value)) + } + is OnTokenSymbolChanged -> { + val validator: TokenSymbolValidator = getValidator(Symbol, hubState) + addOrRemoveError(Symbol, validator.validate(action.value.value)) } is OnTokenDecimalsChanged -> { val validator: TokenDecimalsValidator = getValidator(Decimals, hubState) addOrRemoveError(Decimals, validator.validate(action.value.value)) } - is OnTokenFieldChanged -> { - val validator: StringIsNotEmptyValidator = getValidator(action.id, hubState) - addOrRemoveError(action.id as CustomTokenFieldId, validator.validate(action.value.value)) - } +// is OnTokenDerivationPathChanged -> { +// val validator: TokenDerivationPathValidator = getValidator(DerivationPath, hubState) +// addOrRemoveError(DerivationPath, validator.validate(action.value.value)) +// } is OnCustomTokenSelected -> { // dispatchOnMain() } @@ -231,8 +241,13 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT field.data = action.value updateFormState(state) } - is OnTokenDerivationPathChanged -> { - val field: TokenDerivationPathField = getField(DerivationPath, state) + is OnTokenNameChanged -> { + val field: TokenField = getField(Name, state) + field.data = action.value + updateFormState(state) + } + is OnTokenSymbolChanged -> { + val field: TokenField = getField(Symbol, state) field.data = action.value updateFormState(state) } @@ -241,8 +256,8 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT field.data = action.value updateFormState(state) } - is OnTokenFieldChanged -> { - val field: TokenField = getField(action.id, state) + is OnTokenDerivationPathChanged -> { + val field: TokenDerivationPathField = getField(DerivationPath, state) field.data = action.value updateFormState(state) } diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index d8ac3b84ee..cdf726e361 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -76,10 +76,9 @@ data class AddCustomTokenState( return mapOf( ContractAddress to TokenContractAddressValidator(), Network to TokenNetworkValidator(), - Name to StringIsNotEmptyValidator(), - Symbol to StringIsNotEmptyValidator(), + Name to TokenNameValidator(), + Symbol to TokenSymbolValidator(), Decimals to TokenDecimalsValidator(), -// DerivationPath to TokenDerivationPathValidator(), ) } @@ -99,7 +98,7 @@ data class AddCustomTokenState( val evmBlockchains = Blockchain.values().filter { !it.isTestnet() && it.getChainId() != null } - return evmBlockchains + return listOf(Blockchain.Unknown) + evmBlockchains } private fun createInitialScreenState(): ScreenState { From 35d74021ace70c8c5e82d9c52e88ba3f8e83664e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Apr 2022 10:25:07 +0300 Subject: [PATCH 22/28] Updated on 2026-08-14 --- .../main/java/com/tangem/network/api/tangemTech/Responses.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt index 0c9029673d..534074aa0c 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt @@ -16,7 +16,7 @@ sealed class Coins : TangemTechResponse { ) } - data class CheckAddressResponse(val imageHost: String, val tokens: List, val total: Int) : Coins() { + data class CheckAddressResponse(val imageHost: String?, val tokens: List, val total: Int) : Coins() { data class Token( val id: String, val name: String, From 0a25bbedbb1c22f33383663363cb2da63567c663 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Apr 2022 10:30:27 +0300 Subject: [PATCH 23/28] Updated on 2026-08-14 --- .../tap/common/compose/BlockchainSpinner.kt | 31 ++++++++ .../common/compose/ComposeDialogManager.kt | 25 ++----- .../tap/common/compose/OutlinedSpinner.kt | 40 ++++++----- .../common/compose/OutlinedTextFieldWidget.kt | 4 +- .../com/tangem/tap/common/compose/Spacer.kt | 18 ++--- .../tangem/tap/common/compose/Undefined.kt | 27 +++++++ .../home/compose/StoriesProgressBar.kt | 4 +- .../CustomTokenErrorConverter.kt | 9 ++- .../compose/AddCustomTokenScreen.kt | 70 +++++++++++++------ .../compose/AddCustomTokenViews.kt | 66 ----------------- .../compose/SelectTokenNetworkDialog.kt | 21 ++++++ .../com/tangem/domain/DomainStateDialog.kt | 2 +- .../addCustomToken/AddCustomTokenManager.kt | 29 ++++---- .../domain/features/addCustomToken/Errors.kt | 4 ++ .../features/addCustomToken/FormFields.kt | 2 +- .../addCustomToken/redux/AddCustomTokenHub.kt | 33 ++++++--- .../redux/AddCustomTokenState.kt | 12 ++-- 17 files changed, 224 insertions(+), 173 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/BlockchainSpinner.kt create mode 100644 app/src/main/java/com/tangem/tap/common/compose/Undefined.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenViews.kt create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/SelectTokenNetworkDialog.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/BlockchainSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/BlockchainSpinner.kt new file mode 100644 index 0000000000..5a67842827 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/BlockchainSpinner.kt @@ -0,0 +1,31 @@ +package com.tangem.tap.common.compose + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.form.Field + +@Composable +fun BlockchainSpinner( + @StringRes title: Int, + itemList: List, + selectedItem: Field.Data, + isEnabled: Boolean = true, + textFieldConverter: (Blockchain) -> String, + dropdownItemView: @Composable ((Blockchain) -> Unit)? = null, + onItemSelected: (Blockchain) -> Unit, +) { + OutlinedSpinner( + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = title), + itemList = itemList, + selectedItem = selectedItem, + textFieldConverter = textFieldConverter, + dropdownItemView = dropdownItemView, + isEnabled = isEnabled, + onItemSelected = onItemSelected + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt index d1f540e7e1..80cb2936ec 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -4,8 +4,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.LocalTextStyle +import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.* @@ -22,6 +22,7 @@ import com.tangem.domain.DomainStateDialog import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.domain.redux.global.DomainGlobalState +import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog import org.rekotlin.StoreSubscriber @Composable @@ -56,15 +57,7 @@ fun ShowTheDialog(dialogState: MutableState) { val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) } when (val dialog = dialogState.value) { - is DomainDialog.SelectTokenDialog -> { - SimpleDialog( - title = "Select a token", - items = dialog.items, - itemNameConverter = dialog.itemNameConverter, - onSelect = dialog.onSelect, - onDismissRequest = onDismissRequest - ) - } + is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest) } } @@ -75,9 +68,9 @@ fun ShowTheDialog(dialogState: MutableState) { fun SimpleDialog( title: String, items: List, - itemNameConverter: (T) -> String, onSelect: (T) -> Unit, - onDismissRequest: () -> Unit + onDismissRequest: () -> Unit, + itemContent: @Composable (T) -> Unit, ) { Dialog( properties = DialogProperties(false, false), @@ -85,7 +78,7 @@ fun SimpleDialog( ) { Surface( modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) + shape = MaterialTheme.shapes.medium ) { Column( modifier = Modifier.padding(22.dp) @@ -112,11 +105,7 @@ fun SimpleDialog( onDismissRequest() }, verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = itemNameConverter(item), - ) - } + ) { itemContent(item) } } } } diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt index ef87477734..b7eb414062 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedSpinner.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback @@ -14,17 +16,16 @@ import com.tangem.tap.common.extensions.ValueCallback /** [REDACTED_AUTHOR] */ -private class OutlinedSpinner - @OptIn(ExperimentalMaterialApi::class) @Composable fun OutlinedSpinner( modifier: Modifier = Modifier, - title: String, + label: String, itemList: List, selectedItem: Field.Data, onItemSelected: ValueCallback, - itemNameConverter: (T) -> String = { it.toString() }, + textFieldConverter: (T) -> String = { it.toString() }, + dropdownItemView: @Composable ((T) -> Unit)? = null, isEnabled: Boolean = true, onClose: VoidCallback = {} ) { @@ -48,24 +49,27 @@ fun OutlinedSpinner( expanded = rIsExpanded.value, onExpandedChange = { rIsExpanded.value = !rIsExpanded.value }, ) { - OutlinedTextField( - modifier = modifier, - readOnly = true, - enabled = isEnabled, - value = itemNameConverter(rSelectedItem.value), - onValueChange = {}, - label = { Text(title) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, - ) + ProvideTextStyle(value = TextStyle(color = Color.Blue)) { + OutlinedTextField( + modifier = modifier, + readOnly = true, + enabled = isEnabled, + value = textFieldConverter(rSelectedItem.value), + onValueChange = {}, + label = { Text(label) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = rIsExpanded.value) }, + ) + } ExposedDropdownMenu( expanded = rIsExpanded.value, onDismissRequest = onDismissRequest, ) { itemList.forEach { item -> - DropdownMenuItem( - onClick = { onItemSelectedInternal(item) } - ) { - Text(itemNameConverter(item)) + DropdownMenuItem(onClick = { onItemSelectedInternal(item) }) { + when (dropdownItemView) { + null -> Text(textFieldConverter(item)) + else -> dropdownItemView(item) + } } } } @@ -77,7 +81,7 @@ fun OutlinedSpinner( fun TestSpinnerPreview() { Scaffold() { OutlinedSpinner( - title = "Blockchain name", + label = "Blockchain name", itemList = listOf(Blockchain.values()), selectedItem = Field.Data(Blockchain.Avalanche), onItemSelected = {}, diff --git a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt index b49579609d..d0819b0817 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/OutlinedTextFieldWidget.kt @@ -28,8 +28,6 @@ import com.tangem.tap.common.compose.extensions.stringResourceDefault /** [REDACTED_AUTHOR] */ -private class OutlinedTextFieldWidget - @Composable fun OutlinedTextFieldWidget( modifier: Modifier = Modifier, @@ -90,7 +88,7 @@ private fun OutlinedProgressTextField( val rTextDebouncer = valueDebouncerAsState(debounce, onTextChanged) val rText = remember { mutableStateOf(textFieldData.value) } - fun updateFieldValueAndEmmit(value: String){ + fun updateFieldValueAndEmmit(value: String) { rText.value = value rTextDebouncer.emmit(value) } diff --git a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt index cfea07e9c8..b31a15c4ae 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt @@ -40,28 +40,28 @@ fun SpacerH24(modifier: Modifier = Modifier) { // ***************************** Vertical @Composable -fun SpacerV(width: Dp, modifier: Modifier = Modifier) { +fun SpacerW(width: Dp, modifier: Modifier = Modifier) { Spacer(modifier = modifier.width(width)) } @Composable -fun SpacerV4(modifier: Modifier = Modifier) { - SpacerV(4.dp, modifier) +fun SpacerW4(modifier: Modifier = Modifier) { + SpacerW(4.dp, modifier) } @Composable -fun SpacerV8(modifier: Modifier = Modifier) { - SpacerV(8.dp, modifier) +fun SpacerW8(modifier: Modifier = Modifier) { + SpacerW(8.dp, modifier) } @Composable -fun SpacerV16(modifier: Modifier = Modifier) { - SpacerV(16.dp, modifier) +fun SpacerW16(modifier: Modifier = Modifier) { + SpacerW(16.dp, modifier) } @Composable -fun SpacerV24(modifier: Modifier = Modifier) { - SpacerV(24.dp, modifier) +fun SpacerW24(modifier: Modifier = Modifier) { + SpacerW(24.dp, modifier) } // ***************************** Size diff --git a/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt b/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt new file mode 100644 index 0000000000..6cd0b70d72 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/Undefined.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.common.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.sp + +/** +[REDACTED_AUTHOR] + * Compose views are not typically used as a main or base view. + */ + +@Composable +fun TitleSubtitle( + title: String, + subtitle: String +) { + Column() { + Text(text = title) + Text( + text = subtitle, + fontSize = 12.sp, + color = Color.Gray + ) + } +} \ No newline at end of file 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 7822c5cee0..d121949529 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,7 +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 +import com.tangem.tap.common.compose.SpacerW4 @Composable fun StoriesProgressBar( @@ -68,7 +68,7 @@ fun StoriesProgressBar( ) {} } if (index != steps) { - SpacerV4() + SpacerW4() } } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt index 17373aa948..f151530a1b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt @@ -35,10 +35,15 @@ class CustomTokenWarningConverter( override fun convertError(error: DomainError): String { val customTokenWarning = (error as? AddCustomTokenWarning) ?: throw UnsupportedOperationException() - val resId = when (customTokenWarning) { + val rawMessage = when (customTokenWarning) { AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added + AddCustomTokenWarning.Network.CheckAddressRequestError -> "CheckAddressRequestError" + } + return when (rawMessage) { + is Int -> context.getString(rawMessage) + is String -> rawMessage + else -> "Unknown error: ${customTokenWarning::class.java.simpleName}" } - return context.getString(resId) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index f1cc9dcfbf..2ad938960a 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -2,12 +2,10 @@ package com.tangem.tap.features.tokens.addCustomToken.compose import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.Scaffold -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.material.rememberScaffoldState +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -29,10 +27,7 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState import com.tangem.domain.features.addCustomToken.redux.ScreenState import com.tangem.domain.features.addCustomToken.redux.ViewStates import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.ComposeDialogManager -import com.tangem.tap.common.compose.OutlinedTextFieldWidget -import com.tangem.tap.common.compose.SpacerH8 -import com.tangem.tap.common.compose.keyboardObserverAsState +import com.tangem.tap.common.compose.* import com.tangem.tap.features.tokens.addCustomToken.CustomTokenErrorConverter import com.tangem.tap.features.tokens.addCustomToken.CustomTokenWarningConverter import com.tangem.wallet.R @@ -52,12 +47,12 @@ fun AddCustomTokenScreen(state: MutableState) { ) { Box(Modifier.fillMaxSize()) { LazyColumn( - contentPadding = PaddingValues(bottom = 80.dp) + contentPadding = PaddingValues(bottom = 90.dp) ) { item { Surface( modifier = Modifier.padding(16.dp), - shape = RoundedCornerShape(4.dp), + shape = MaterialTheme.shapes.small, elevation = 4.dp, ) { Column( @@ -143,7 +138,7 @@ private fun TokenNameView(screenFieldData: ScreenFieldData) { error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, ) { - domainStore.dispatch(OnTokenFieldChanged(screenFieldData.field.id, Field.Data(it))) + domainStore.dispatch(OnTokenNameChanged(Field.Data(it))) } SpacerH8() } @@ -153,14 +148,14 @@ private fun TokenNetworkView(screenFieldData: ScreenFieldData) { if (!screenFieldData.viewState.isVisible) return val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) - val networkField = screenFieldData.field as TokenNetworkField + val networkField = screenFieldData.field as TokenBlockchainField - TokenNetworkSpinner( + BlockchainSpinner( title = R.string.custom_token_network_input_title, itemList = networkField.itemList, selectedItem = networkField.data, isEnabled = screenFieldData.viewState.isEnabled, - itemNameConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) }, + textFieldConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) }, ) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it))) } SpacerH8() } @@ -178,7 +173,7 @@ private fun TokenSymbolView(screenFieldData: ScreenFieldData) { isEnabled = screenFieldData.viewState.isEnabled, error = screenFieldData.error, errorConverter = screenFieldData.errorConverter, - ) { domainStore.dispatch(OnTokenFieldChanged(screenFieldData.field.id, Field.Data(it))) } + ) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it))) } SpacerH8() } @@ -207,12 +202,17 @@ private fun TokenDerivationPathView(screenFieldData: ScreenFieldData) { val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) val networkField = screenFieldData.field as TokenDerivationPathField - TokenNetworkSpinner( - title = R.string.custom_token_network_input_title, + BlockchainSpinner( + title = R.string.custom_token_derivation_path_input_title, itemList = networkField.itemList, selectedItem = networkField.data, isEnabled = screenFieldData.viewState.isEnabled, - itemNameConverter = { AddCustomTokenState.convertDerivationPathName(it, notSelected) }, + textFieldConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) }, + dropdownItemView = { blockchain -> + val derivationPathLabel = AddCustomTokenState.convertDerivationPathLabel(blockchain, notSelected) + val blockchainName = AddCustomTokenState.convertBlockchainName(blockchain, notSelected) + TitleSubtitle(derivationPathLabel, blockchainName) + } ) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it))) } SpacerH8() } @@ -227,13 +227,13 @@ private fun Warnings(warnings: List) { Column { warnings.forEachIndexed { index, item -> val modifier = when (index) { - 0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 16.dp) - warnings.lastIndex -> Modifier.padding(16.dp, 16.dp, 16.dp, 16.dp) - else -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp) + 0 -> Modifier.padding(16.dp, 16.dp, 16.dp, 0.dp) + warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp) + else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp) } Surface( modifier = modifier.fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + shape = MaterialTheme.shapes.small, color = colorResource(id = R.color.darkGray2), contentColor = colorResource(id = R.color.darkGray3) ) { @@ -248,6 +248,30 @@ private fun Warnings(warnings: List) { } } +@Composable +private fun AddButton( + modifier: Modifier = Modifier, + isEnabled: Boolean, + textId: Int = R.string.common_add, + onClick: () -> Unit, +) { + Button( + textId = textId, + isEnabled = isEnabled, + modifier = modifier + .height(52.dp) + .padding(horizontal = 16.dp) + .fillMaxWidth(), + leadingView = { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = "Add", + ) + }, + onClick = onClick + ) +} + private data class ScreenFieldData( val field: DataField<*>, val error: AddCustomTokenError?, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenViews.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenViews.kt deleted file mode 100644 index dee8d3b535..0000000000 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenViews.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.tap.features.tokens.addCustomToken.compose - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.common.form.Field -import com.tangem.tap.common.compose.Button -import com.tangem.tap.common.compose.OutlinedSpinner -import com.tangem.tap.common.extensions.ValueCallback -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun TokenNetworkSpinner( - title: Int, - itemList: List, - selectedItem: Field.Data, - isEnabled: Boolean = true, - itemNameConverter: (Blockchain) -> String, - onItemSelected: ValueCallback, -) { - - OutlinedSpinner( - modifier = Modifier.fillMaxWidth(), - title = stringResource(id = title), - itemList = itemList, - selectedItem = selectedItem, - itemNameConverter = itemNameConverter, - isEnabled = isEnabled, - onItemSelected = onItemSelected - ) -} - -@Composable -fun AddButton( - modifier: Modifier = Modifier, - isEnabled: Boolean, - textId: Int = R.string.common_add, - onClick: () -> Unit, -) { - Button( - textId = textId, - isEnabled = isEnabled, - modifier = modifier - .height(52.dp) - .padding(horizontal = 16.dp) - .fillMaxWidth(), - leadingView = { - Icon( - imageVector = Icons.Filled.Add, - contentDescription = "Add", - ) - }, - onClick = onClick - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/SelectTokenNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/SelectTokenNetworkDialog.kt new file mode 100644 index 0000000000..946690c107 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/SelectTokenNetworkDialog.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.tangem.domain.DomainDialog +import com.tangem.tap.common.compose.SimpleDialog +import com.tangem.tap.common.compose.TitleSubtitle +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun SelectTokenNetworkDialog(dialog: DomainDialog.SelectTokenDialog, onDismissRequest: () -> Unit) { + SimpleDialog( + title = stringResource(id = R.string.custom_token_type_network), + items = dialog.items, + onSelect = dialog.onSelect, + onDismissRequest = onDismissRequest + ) { contract -> TitleSubtitle(dialog.networkIdConverter(contract.networkId), contract.address) } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt index c6baa9a431..f2df99a7c1 100644 --- a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt +++ b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt @@ -12,7 +12,7 @@ sealed class DomainDialog : DomainStateDialog { data class SelectTokenDialog( val items: List, - val itemNameConverter: (Coins.CheckAddressResponse.Token.Contract) -> String, + val networkIdConverter: (String) -> String, val onSelect: (Coins.CheckAddressResponse.Token.Contract) -> Unit, val onClose: VoidCallback = {} ) : DomainDialog() 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 index dac29b2d3a..0b49f500d7 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt @@ -15,35 +15,36 @@ class AddCustomTokenManager( suspend fun checkAddress( contractAddress: String, networkId: String? = null - ): List { + ): Result> { val result = tangemTechService.coins.checkAddress(contractAddress, networkId) return when (result) { is Result.Success -> { val resultTokens = result.data.tokens - val newTokensList = mutableListOf() - resultTokens.forEach { - val contractsWithTheSameAddress = it.contracts.filter { it.address == contractAddress } + var tokensList = mutableListOf() + resultTokens.forEach { token -> + val contractsWithTheSameAddress = token.contracts + .filter { it.address == contractAddress } + .filter { it.decimalCount != null } if (contractsWithTheSameAddress.isNotEmpty()) { - val newToken = it.copy(contracts = contractsWithTheSameAddress) - newTokensList.add(newToken) + val newToken = token.copy(contracts = contractsWithTheSameAddress) + tokensList.add(newToken) } } - when { + if (tokensList.size > 1) { // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679 - newTokensList.size > 1 -> listOf(newTokensList[0]) - else -> newTokensList + tokensList = mutableListOf(tokensList[0]) } + Result.Success(tokensList) } - is Result.Failure -> emptyList() + is Result.Failure -> result } } suspend fun tokens(): List { - val result = tangemTechService.coins.tokens() - return when (result) { + return when (val result = tangemTechService.coins.tokens()) { is Result.Success -> { - val currencies = result.data.tokens - currencies.filter { + val tokens = result.data.tokens + tokens.filter { it.contracts.isNullOrEmpty() } } 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 index c7abc4c0d2..b651dd844b 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt @@ -18,4 +18,8 @@ sealed class AddCustomTokenError : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add cus sealed class AddCustomTokenWarning : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - warning") { object PotentialScamToken : AddCustomTokenWarning() object TokenAlreadyAdded : AddCustomTokenWarning() + + sealed class Network : AddCustomTokenWarning() { + object CheckAddressRequestError : Network() + } } \ 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 index b6084347d2..33336b560e 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt @@ -21,7 +21,7 @@ data class TokenField( override val id: FieldId, ) : BaseDataField(id, Field.Data("")) -data class TokenNetworkField( +data class TokenBlockchainField( override val id: FieldId, val itemList: List, ) : BaseDataField(id, Field.Data(Blockchain.Unknown)) 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 index 0077bd1907..f4dc8c9746 100644 --- 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 @@ -25,6 +25,9 @@ import org.rekotlin.Action */ internal class AddCustomTokenHub : BaseStoreHub("AddCustomTokenHub") { + private val hubState: AddCustomTokenState + get() = domainStore.state.addCustomTokensState + override fun getHubState(storeState: DomainState): AddCustomTokenState { return storeState.addCustomTokensState } @@ -41,7 +44,6 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT if (action !is AddCustomTokenAction) return // val card = storeState.globalState.scanResponse?.card // ?: throw IllegalStateException("ScanResponse must be set before showing the AddCustomToken screen") - val hubState = storeState.addCustomTokensState when (action) { is OnCreate -> { @@ -102,7 +104,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT // dispatchOnMain() } is FillTokenFields -> { - val networkField = getField(Network, hubState) + val networkField = getField(Network, hubState) val nameField = getField(Name, hubState) val symbolField = getField(Symbol, hubState) val decimalsField = getField(Decimals, hubState) @@ -127,13 +129,22 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT ): List { dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) val tokenManager = hubState.addCustomTokenManager - val field = getField(Network, hubState) + val field = getField(Network, hubState) val selectedNetworkId: String? = field.data.value.let { if (it == Blockchain.Unknown) null else it }?.toNetworkId() - val foundTokens = tokenManager.checkAddress(contractAddress, selectedNetworkId) + +// delay(1000) + val result = when (val foundTokensResult = tokenManager.checkAddress(contractAddress, selectedNetworkId)) { + is Result.Success -> foundTokensResult.data + is Result.Failure -> { +// val warning = AddCustomTokenWarning.Network.CheckAddressRequestError +// dispatchOnMain(Warning.Add(setOf(warning))) + emptyList() + } + } dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = false)))) - return foundTokens + return result } private suspend fun checkToken( @@ -171,12 +182,18 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT else -> { val dialog = DomainDialog.SelectTokenDialog( items = contracts, - itemNameConverter = { it.address }, + networkIdConverter = { networkId -> + val blockchain = Blockchain.fromNetworkId(networkId) + if (blockchain == Blockchain.Unknown) { + throw DomainException.SelectTokeNetworkException(networkId) + } + AddCustomTokenState.convertBlockchainName(blockchain, "") + }, onSelect = { selectedContract -> hubScope.launch { // find how to connect to the upper coroutineContext and dispatch through them dispatchOnMain(FillTokenFields(token, selectedContract)) - dispatchOnMain(FillTokenFields(token, selectedContract)) + dispatchOnMain(actionsLockTokenFields()) } }, ) @@ -237,7 +254,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT updateFormState(state) } is OnTokenNetworkChanged -> { - val field: TokenNetworkField = getField(Network, state) + val field: TokenBlockchainField = getField(Network, state) field.data = action.value updateFormState(state) } diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index cdf726e361..f9488b54b4 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -49,22 +49,17 @@ data class AddCustomTokenState( companion object { fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) { Blockchain.Unknown -> unknown - Blockchain.Cardano -> "Cardano" - Blockchain.CardanoShelley -> "Cardano Shelley" else -> blockchain.fullName } - fun convertDerivationPathName(blockchain: Blockchain, unknown: String): String = when (blockchain) { - Blockchain.Unknown -> unknown - Blockchain.BSC -> "BNB Smart Chain" - Blockchain.Fantom -> "Fantom Opera" - else -> blockchain.fullName + fun convertDerivationPathLabel(blockchain: Blockchain, unknown: String): String { + return blockchain.derivationPath()?.rawPath ?: unknown } private fun createFormFields(): List> { return listOf( TokenField(ContractAddress), - TokenNetworkField(Network, getSupportedNetworks()), + TokenBlockchainField(Network, getSupportedNetworks()), TokenField(Name), TokenField(Symbol), TokenField(Decimals), @@ -84,6 +79,7 @@ data class AddCustomTokenState( private fun getSupportedNetworks(): List { return listOf( + Blockchain.Unknown, Blockchain.Ethereum, Blockchain.BSC, Blockchain.Binance, From 536af28f801d507b78860840bd8296f03371a744 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Apr 2022 10:31:28 +0300 Subject: [PATCH 24/28] Updated on 2026-08-14 --- .../compose/AddCustomTokenScreen.kt | 1 + .../addCustomToken/compose/DebugActions.kt | 243 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index 2ad938960a..688c373b24 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -49,6 +49,7 @@ fun AddCustomTokenScreen(state: MutableState) { LazyColumn( contentPadding = PaddingValues(bottom = 90.dp) ) { + item { AddCustomTokenDebugActions() } item { Surface( modifier = Modifier.padding(16.dp), diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt new file mode 100644 index 0000000000..56ab3e1f37 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt @@ -0,0 +1,243 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.VoidCallback +import com.tangem.common.services.Result +import com.tangem.domain.common.form.Field +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.redux.domainStore +import com.tangem.wallet.BuildConfig +import timber.log.Timber + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun AddCustomTokenDebugActions() { + if (!BuildConfig.DEBUG) return + + Column() { + // deep test +// ActionRow("Invalid field value") { InvalidFieldValue() } +// ActionRow("Active(true)") { ActiveTrue() } +// ActionRow("Active(false) && decimalCount != null") { ActiveFalseDecimals() } +// ActionRow("In several networks") { InSeveralNetworks() } +// ActionRow("Address not found") { UnknownContracts() } + + // test + ActionRow("All in one") { AllInOne() } + + // Any action +// ActionRow("CustomActions - find tokens active=false, decimals != null") { CustomActions() } + } +} + +@Composable +private fun AllInOne() { + // validation error + ContractAddressButton( + name = "invalid", + address = "unk" + ) + // active = true + ContractAddressButton( + name = "true", + address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + ) + // active = false, decimalCount != null + ContractAddressButton( + name = "false", + address = "0x2147efff675e4a4ee1c2f918d181cdbd7a8e208f" + ) + // more than one network + ContractAddressButton( + name = ">1 network", + address = "0xa1faa113cbe53436df28ff0aee54275c13b40975" + ) + // unknown + ContractAddressButton( + name = "unk", + address = "0x1111111111111111112111111111111111111113" + ) +} + +@Composable +private fun InvalidFieldValue() { + ContractAddressButton( + name = "unk", + address = "unk" + ) + ContractAddressButton( + name = "someText", + address = "someText" + ) + ContractAddressButton( + name = "alskdml...fa s", + address = "alskdmlasd asdln alsdknflasd flasd fa s" + ) +} + +@Composable +private fun ActiveTrue() { + ContractAddressButton( + name = "USDC- Ethereum", + address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48" + ) + ContractAddressButton( + name = "USDT- Avalanche", + address = "0xc7198437980c041c805a1edcba50c1ce5db95118" + ) + ContractAddressButton( + name = "VID- Fantom", + address = "0x922d641a426dcffaef11680e5358f34d97d112e1" + ) +} + +@Composable +private fun ActiveFalseDecimals() { + ContractAddressButton( + name = "ALPHA- avalanche", + address = "0x2147efff675e4a4ee1c2f918d181cdbd7a8e208f" + ) + ContractAddressButton( + name = "BETA- avalanche", + address = "0x511d35c52a3c244e7b8bd92c0c297755fbd89212" + ) +} + +@Composable +private fun InSeveralNetworks() { + ContractAddressButton( + name = "ALPHA- Eth,Bsc", + address = "0xa1faa113cbe53436df28ff0aee54275c13b40975" + ) + ContractAddressButton( + name = "BETA- Eth,Bsc", + address = "0xbe1a001fe942f96eea22ba08783140b9dcc09d28" + ) +} + +@Composable +private fun UnknownContracts() { + ContractAddressButton( + name = "0x11...113", + address = "0x1111111111111111112111111111111111111113" + ) + ContractAddressButton( + name = "0xc7...111", + address = "0xc7198437980c041c805a1edcba50c1ce5db95111" + ) +} + +@Composable +private fun CustomActions() { + + CustomActionButton( + name = "Find tokens in several networks", + action = { + val manager = domainStore.state.addCustomTokensState.addCustomTokenManager + val currencies = manager.tokens() + val asdfsd = mutableMapOf>() + val contractAddresses = currencies.mapNotNull { currency -> + currency.contracts?.map { it.address } + }.flatten() + contractAddresses.take(500).forEachIndexed() { index, address -> + when (val result = manager.checkAddress(address)) { + is Result.Success -> { + val contractList = mutableListOf() + result.data.forEach { token -> + token.contracts.forEach { contract -> + if (!contract.active && contract.decimalCount != null) { + contractList.add(contract) + } + } + } + if (contractList.isNotEmpty()) { + val list = asdfsd[address] ?: mutableListOf() + list.addAll(contractList) + asdfsd[address] = list + } + Timber.e("Success. handle $index item from size ${contractAddresses.size}. Result = ${asdfsd.size}") + } + is Result.Failure -> {} + } + } + val result = asdfsd.filter { it.value.size > 1 } + if (result.isEmpty()) return@CustomActionButton + } + ) +} + +@Composable +private fun ActionRow( + name: String, + content: @Composable () -> Unit +) { + Column() { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = name, + fontSize = 14.sp + ) + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { content() } + } + +} + +@Composable +private fun ActionButton( + name: String, + onClick: VoidCallback, +) { + Button( + modifier = Modifier.padding(horizontal = 4.dp), + onClick = onClick + ) { Text(name, fontSize = 8.sp) } +} + +@Composable +private fun ContractAddressButton( + name: String, + address: String, +) { + ActionButton(name = name) { + resetTokenValues() + domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(address, false))) + } +} + +@Composable +private fun CustomActionButton( + name: String, + action: suspend () -> Unit +) { + val startValue = 0 + val anyValue = remember { mutableStateOf(startValue) } + LaunchedEffect(key1 = anyValue.value, block = { + if (anyValue.value != startValue) { + action() + } + }) + + ActionButton(name) { anyValue.value = anyValue.value + 1 } +} + +private fun resetTokenValues() { + domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(Blockchain.Unknown, false))) +// domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data("", false))) +// domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data("", false))) +// domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data("", false))) +} \ No newline at end of file From 5fa7c042c7647600db2f030dd27259f84150b2c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Apr 2022 19:15:50 +0300 Subject: [PATCH 25/28] Updated on 2026-08-14 --- .../redux/AddCustomTokenAction.kt | 23 ++- .../addCustomToken/redux/AddCustomTokenHub.kt | 192 ++++++++++-------- 2 files changed, 118 insertions(+), 97 deletions(-) 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 index 4b1a940dcf..0fa2c84d99 100644 --- 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 @@ -16,26 +16,27 @@ sealed class AddCustomTokenAction : Action { // from user, ui object OnCreate : AddCustomTokenAction() object OnDestroy : AddCustomTokenAction() - data class OnTokenContractAddressChanged(val value: Field.Data) : AddCustomTokenAction() - data class OnTokenNetworkChanged(val value: Field.Data) : AddCustomTokenAction() - data class OnTokenNameChanged(val value: Field.Data) : AddCustomTokenAction() - data class OnTokenSymbolChanged(val value: Field.Data) : AddCustomTokenAction() - data class OnTokenDerivationPathChanged(val value: Field.Data) : AddCustomTokenAction() - data class OnTokenDecimalsChanged(val value: Field.Data) : AddCustomTokenAction() - data class OnCustomTokenSelected(val any: Any = Unit) : AddCustomTokenAction() - + data class OnTokenContractAddressChanged(val contractAddress: Field.Data) : AddCustomTokenAction() + data class OnTokenNetworkChanged(val blockchainNetwork: Field.Data) : AddCustomTokenAction() + data class OnTokenNameChanged(val tokenName: Field.Data) : AddCustomTokenAction() + data class OnTokenSymbolChanged(val tokenSymbol: Field.Data) : AddCustomTokenAction() + data class OnTokenDerivationPathChanged(val blockchainDerivationPath: Field.Data) : AddCustomTokenAction() + data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data) : AddCustomTokenAction() + // form fields data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction() + object ClearTokenFields : AddCustomTokenAction() data class FillTokenFields( val token: Coins.CheckAddressResponse.Token, val contract: Coins.CheckAddressResponse.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 FieldError : AddCustomTokenAction() { + data class Add(val id: CustomTokenFieldId, val error: AddCustomTokenError) : FieldError() + data class Remove(val id: CustomTokenFieldId) : FieldError() } + // warnings sealed class Warning : AddCustomTokenAction() { data class Add(val warnings: Set) : Warning() data class Remove(val warnings: Set) : Warning() 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 index f4dc8c9746..4443abd14b 100644 --- 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 @@ -19,6 +19,7 @@ import com.tangem.network.api.tangemTech.Coins import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import org.rekotlin.Action +import timber.log.Timber /** [REDACTED_AUTHOR] @@ -51,57 +52,53 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } is OnDestroy -> hubScope.cancel() is OnTokenContractAddressChanged -> { - val contractAddress = action.value + val contractAddress = action.contractAddress.value val validator: TokenContractAddressValidator = getValidator(ContractAddress, hubState) - val error = validator.validate(contractAddress.value) + val error = validator.validate(contractAddress) addOrRemoveError(ContractAddress, error) - if (error != null || contractAddress.value.isEmpty()) { + + if (error != null || contractAddress.isEmpty()) { dispatchOnMain(actionsUnlockTokenFields()) return } + if (!action.contractAddress.isUserInput) return - val foundTokens = requestInfoAboutContractAddress(contractAddress.value, hubState) - val warningsToAdd = mutableSetOf() - val warningsToRemove = mutableSetOf() - when { - foundTokens.isEmpty() -> { - warningsToAdd.add(AddCustomTokenWarning.PotentialScamToken) - } - else -> { - val token = foundTokens[0] - checkToken(null, token, warningsToAdd, warningsToRemove) - } - } - if (warningsToAdd.isNotEmpty() || warningsToRemove.isNotEmpty()) { - dispatchOnMain(Warning.Replace(warningsToRemove.toSet(), warningsToAdd.toSet())) - } + val foundTokens = requestInfoAboutContractAddress(contractAddress, hubState) + manageTokenChanges(null, foundTokens) } is OnTokenNetworkChanged -> { -// val validator: TokenNetworkValidator = getValidator(Network, hubState) -// addOrRemoveError(Network, validator.validate(action.value.value)) + if (!action.blockchainNetwork.isUserInput) return -// dispatchOnMain(OnTokenContractAddressChanged(Field.Data( -// getField(ContractAddress, hubState).data.value, false -// ))) + val contractAddress = getField(ContractAddress, hubState).data.value + val foundTokens = requestInfoAboutContractAddress(contractAddress, hubState) + manageTokenChanges(null, foundTokens) } is OnTokenNameChanged -> { val validator: TokenNameValidator = getValidator(Name, hubState) - addOrRemoveError(Name, validator.validate(action.value.value)) + addOrRemoveError(Name, validator.validate(action.tokenName.value)) } is OnTokenSymbolChanged -> { val validator: TokenSymbolValidator = getValidator(Symbol, hubState) - addOrRemoveError(Symbol, validator.validate(action.value.value)) + addOrRemoveError(Symbol, validator.validate(action.tokenSymbol.value)) } is OnTokenDecimalsChanged -> { val validator: TokenDecimalsValidator = getValidator(Decimals, hubState) - addOrRemoveError(Decimals, validator.validate(action.value.value)) + addOrRemoveError(Decimals, validator.validate(action.tokenDecimals.value)) } // is OnTokenDerivationPathChanged -> { // val validator: TokenDerivationPathValidator = getValidator(DerivationPath, hubState) // addOrRemoveError(DerivationPath, validator.validate(action.value.value)) // } - is OnCustomTokenSelected -> { -// dispatchOnMain() + is ClearTokenFields -> { + val nameField = getField(Name, hubState) + val symbolField = getField(Symbol, hubState) + val decimalsField = getField(Decimals, hubState) + + nameField.data = Field.Data("", false) + symbolField.data = Field.Data("", false) + decimalsField.data = Field.Data("", false) + + dispatchOnMain(UpdateForm(hubState)) } is FillTokenFields -> { val networkField = getField(Network, hubState) @@ -147,58 +144,81 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT return result } - private suspend fun checkToken( + private suspend fun manageTokenChanges( card: Card?, - token: Coins.CheckAddressResponse.Token, - warningsToAdd: MutableSet, - warningsToRemove: MutableSet, + foundTokens: List, ) { - val contracts = token.contracts - when { - contracts.isEmpty() -> { - } - contracts.size == 1 -> { - val contract = contracts[0] - val isPersistIntoTheAppAddedTokenList = isPersistIntoTheAppAddedTokenList(token, contract) + val toAddWarnings = mutableSetOf() + val toRemoveWarnings = mutableSetOf() - if (isPersistIntoTheAppAddedTokenList) { - warningsToAdd.add(AddCustomTokenWarning.TokenAlreadyAdded) - dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false))) - dispatchOnMain(actionsLockTokenFields()) - } else { - dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true))) -// val isStandardDerivation = card.derivationType == DerivationType.Standard - val isStandardDerivation = true - val isStandardToken = token.active && isStandardDerivation - if (isStandardToken) { - dispatchOnMain(FillTokenFields(token, contract)) - dispatchOnMain(actionsLockTokenFields()) - } else { - warningsToAdd.add(AddCustomTokenWarning.PotentialScamToken) - dispatchOnMain(actionsUnlockTokenFields()) + when { + foundTokens.isEmpty() -> { + toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken) + toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) + dispatchOnMain(ClearTokenFields) + dispatchOnMain(actionsUnlockTokenFields()) + } + else -> { + val token = foundTokens[0] + val contracts = token.contracts + when { + contracts.isEmpty() -> { + // TODO: refactoring: + Timber.e("Unexpected state -> throw to FB") + } + contracts.size == 1 -> { + val contract = contracts[0] + val isPersistIntoTheAppAddedTokenList = isPersistIntoTheAppAddedTokenList(token, contract) + + if (isPersistIntoTheAppAddedTokenList) { + toAddWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) + toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken) + + dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false))) + dispatchOnMain(actionsLockTokenFields()) + } else { + toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) + dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true))) + + val isStandardDerivation = true + val tokenContract = token.contracts[0] + if (tokenContract.active && isStandardDerivation) { + toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken) + dispatchOnMain(FillTokenFields(token, contract)) + dispatchOnMain(actionsLockTokenFields()) + } else { + toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken) + dispatchOnMain(ClearTokenFields) + dispatchOnMain(actionsUnlockTokenFields()) + } + } + } + else -> { + val dialog = DomainDialog.SelectTokenDialog( + items = contracts, + networkIdConverter = { networkId -> + val blockchain = Blockchain.fromNetworkId(networkId) + if (blockchain == Blockchain.Unknown) { + throw DomainException.SelectTokeNetworkException(networkId) + } + AddCustomTokenState.convertBlockchainName(blockchain, "") + }, + onSelect = { selectedContract -> + hubScope.launch { + // find how to connect to the upper coroutineContext and dispatch through them + dispatchOnMain(FillTokenFields(token, selectedContract)) + dispatchOnMain(actionsLockTokenFields()) + } + }, + ) + dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) } } } - else -> { - val dialog = DomainDialog.SelectTokenDialog( - items = contracts, - networkIdConverter = { networkId -> - val blockchain = Blockchain.fromNetworkId(networkId) - if (blockchain == Blockchain.Unknown) { - throw DomainException.SelectTokeNetworkException(networkId) - } - AddCustomTokenState.convertBlockchainName(blockchain, "") - }, - onSelect = { selectedContract -> - hubScope.launch { - // find how to connect to the upper coroutineContext and dispatch through them - dispatchOnMain(FillTokenFields(token, selectedContract)) - dispatchOnMain(actionsLockTokenFields()) - } - }, - ) - dispatchOnMain(DomainGlobalAction.ShowDialog(dialog)) - } + } + + if (toAddWarnings.isNotEmpty() || toRemoveWarnings.isNotEmpty()) { + dispatchOnMain(Warning.Replace(toRemoveWarnings.toSet(), toAddWarnings.toSet())) } } @@ -209,14 +229,14 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT private suspend fun addOrRemoveError(id: CustomTokenFieldId, error: AddCustomTokenError?) { if (error == null) { - dispatchOnMain(Error.Remove(id)) + dispatchOnMain(FieldError.Remove(id)) } else { - dispatchOnMain(Error.Add(id, error)) + dispatchOnMain(FieldError.Add(id, error)) } } private fun actionsLockTokenFields(): Action { - val state = domainStore.state.addCustomTokensState + val state = hubState return Screen.UpdateTokenFields(listOf( Network to state.screenState.network.copy(isEnabled = false), Name to state.screenState.name.copy(isEnabled = false), @@ -226,7 +246,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } private fun actionsUnlockTokenFields(): Action { - val state = domainStore.state.addCustomTokensState + val state = hubState return Screen.UpdateTokenFields(listOf( Network to state.screenState.network.copy(isEnabled = true), Name to state.screenState.name.copy(isEnabled = true), @@ -250,39 +270,39 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } is OnTokenContractAddressChanged -> { val field: TokenField = getField(ContractAddress, state) - field.data = action.value + field.data = action.contractAddress updateFormState(state) } is OnTokenNetworkChanged -> { val field: TokenBlockchainField = getField(Network, state) - field.data = action.value + field.data = action.blockchainNetwork updateFormState(state) } is OnTokenNameChanged -> { val field: TokenField = getField(Name, state) - field.data = action.value + field.data = action.tokenName updateFormState(state) } is OnTokenSymbolChanged -> { val field: TokenField = getField(Symbol, state) - field.data = action.value + field.data = action.tokenSymbol updateFormState(state) } is OnTokenDecimalsChanged -> { val field: TokenField = getField(Decimals, state) - field.data = action.value + field.data = action.tokenDecimals updateFormState(state) } is OnTokenDerivationPathChanged -> { val field: TokenDerivationPathField = getField(DerivationPath, state) - field.data = action.value + field.data = action.blockchainDerivationPath updateFormState(state) } - is Error.Add -> { + is FieldError.Add -> { val newMap = state.formErrors.toMutableMap().apply { this[action.id] = action.error } state.copy(formErrors = newMap) } - is Error.Remove -> { + is FieldError.Remove -> { val newMap = state.formErrors.toMutableMap().apply { remove(action.id) } state.copy(formErrors = newMap) } From 787d354d3d8f4566cf81dc0503e861b72ae8be6f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Apr 2022 20:51:03 +0300 Subject: [PATCH 26/28] Updated on 2026-08-14 --- .../common/compose/ComposeKeyboardObserver.kt | 40 -------- .../com/tangem/tap/domain/TapWorkarounds.kt | 91 ------------------- .../tasks/product/CreateProductWalletTask.kt | 5 +- .../domain/tasks/product/ScanProductTask.kt | 61 ++----------- .../tap/domain/tokens/CurrenciesRepository.kt | 2 +- .../walletconnect/WalletConnectMiddleware.kt | 2 - .../compose/AddCustomTokenScreen.kt | 23 ++--- .../features/tokens/redux/TokensMiddleware.kt | 6 +- .../tap/features/wallet/redux/WalletState.kt | 2 +- .../wallet/ui/adapters/WalletAdapter.kt | 2 +- .../wallet/ui/wallet/MultiWalletView.kt | 10 +- domain/build.gradle | 6 +- .../com/tangem/domain/common/ScanResponse.kt | 1 - .../tangem/domain/common/TapWorkarounds.kt | 69 ++++++++------ .../redux/AddCustomTokenAction.kt | 9 +- .../addCustomToken/redux/AddCustomTokenHub.kt | 11 ++- .../redux/AddCustomTokenState.kt | 22 +++-- network/build.gradle | 2 +- 18 files changed, 102 insertions(+), 262 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/compose/ComposeKeyboardObserver.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeKeyboardObserver.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeKeyboardObserver.kt deleted file mode 100644 index fe045a7c9e..0000000000 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposeKeyboardObserver.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.tap.common.compose - -import android.graphics.Rect -import android.view.ViewTreeObserver -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalView - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun keyboardObserverAsState(): State { - val keyboardState: MutableState = remember { mutableStateOf(Keyboard.Closed) } - val view = LocalView.current - DisposableEffect(view) { - val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom - keyboardState.value = if (keypadHeight > screenHeight * 0.15) { - Keyboard.Opened(keypadHeight) - } else { - Keyboard.Closed - } - } - view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) - - onDispose { - view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) - } - } - - return keyboardState -} - -sealed class Keyboard { - data class Opened(val height: Int) : Keyboard() - object Closed : Keyboard() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt deleted file mode 100644 index 02a62b4215..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.tangem.tap.domain - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.DerivationStyle -import com.tangem.common.card.Card -import com.tangem.common.card.EllipticCurve -import com.tangem.common.card.FirmwareVersion -import com.tangem.tap.domain.TapWorkarounds.isStart2Coin -import com.tangem.tap.domain.TapWorkarounds.isTangemNote -import com.tangem.tap.domain.extensions.getSingleWallet -import com.tangem.tap.domain.twins.isTangemTwin -import java.util.* - -object TapWorkarounds { - - fun isStart2CoinIssuer(cardIssuer: String?): Boolean { - return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER - } - - val Card.isStart2Coin: Boolean - get() = isStart2CoinIssuer(issuer.name) - - val Card.isTestCard: Boolean - get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) - - val Card.useOldStyleDerivation: Boolean - get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95" - - val Card.derivationStyle: DerivationStyle? - get() = if (!settings.isHDWalletAllowed) { - null - } else if (useOldStyleDerivation) { - DerivationStyle.LEGACY - } else { - DerivationStyle.NEW - } - - fun Card.isExcluded(): Boolean { - val excludedBatch = excludedBatches.contains(batchId) - val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) - return excludedBatch || excludedIssuerName - } - - fun Card.isNotSupportedInThatRelease(): Boolean { - return false - } - - @Deprecated("Use ScanResponse.isTangemNote") - fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId) - - fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId) - - fun getTangemNoteBlockchain(card: Card): Blockchain? = tangemNoteBatches[card.batchId] - - private const val START_2_COIN_ISSUER = "start2coin" - private const val TEST_CARD_BATCH = "99FF" - private const val TEST_CARD_ID_STARTS_WITH = "FF99" - - private val excludedBatches = listOf( - "0027", - "0030", - "0031", - "0035" - ) - - private val excludedIssuers = listOf( - "TTM BANK" - ) - - private val tangemWalletBatches = listOf("AC01") - - private val tangemNoteBatches = mapOf( - "AB01" to Blockchain.Bitcoin, - "AB02" to Blockchain.Ethereum, - "AB03" to Blockchain.CardanoShelley, - "AB04" to Blockchain.Dogecoin, - "AB05" to Blockchain.BSC, - "AB06" to Blockchain.XRP, - "AB07" to Blockchain.Bitcoin, - "AB08" to Blockchain.Ethereum, - ) -} - -val DELAY_SDK_DIALOG_CLOSE = 1400L - -val Card.isMultiwalletAllowed: Boolean - get() { - return !isTangemTwin() && !isStart2Coin && !isTangemNote(this) - && (firmwareVersion >= FirmwareVersion.MultiWalletAvailable || - getSingleWallet()?.curve == EllipticCurve.Secp256k1) - } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 72f65dc3f8..06afdcbd3c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -11,6 +11,7 @@ import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.KeyWalletPublicKey import com.tangem.domain.common.ProductType +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.operations.CommandResponse @@ -20,10 +21,6 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletTask -import com.tangem.domain.common.ProductType -import com.tangem.domain.common.TapWorkarounds.derivationStyle -import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain -import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.domain.tasks.product.CreateWalletsTask import com.tangem.tap.domain.tasks.product.ProductCommandProcessor import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 7bb5070c4c..b22865b7ee 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -1,12 +1,10 @@ package com.tangem.tap.domain.tasks.product import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion -import com.tangem.common.card.WalletData import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemError @@ -17,8 +15,10 @@ import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.* +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isExcluded import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease +import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.operations.PreflightReadMode import com.tangem.operations.PreflightReadTask import com.tangem.operations.ScanTask @@ -27,57 +27,12 @@ import com.tangem.operations.backup.StartPrimaryCardLinkingTask import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.tap.domain.TapSdkError -import com.tangem.domain.common.TapWorkarounds -import com.tangem.domain.common.TapWorkarounds.derivationStyle -import com.tangem.domain.common.TapWorkarounds.getTangemNoteBlockchain -import com.tangem.domain.common.TapWorkarounds.isExcluded -import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease -import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.tap.domain.extensions.getPrimaryCurve import com.tangem.tap.domain.extensions.getSingleWallet import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.preferencesStorage -data class 1ScanResponse( - val card: Card, - val productType: ProductType, - val walletData: WalletData?, - val secondTwinPublicKey: String? = null, - val derivedKeys: Map = mapOf(), - val primaryCard: PrimaryCard? = null -) : CommandResponse { - - fun getBlockchain(): Blockchain { - if (productType == ProductType.Note) return getTangemNoteBlockchain(card) - ?: return Blockchain.Unknown - val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown - return Blockchain.fromId(blockchainName) - } - - fun getPrimaryToken(): Token? { - val cardToken = walletData?.token ?: return null - return Token( - cardToken.name, - cardToken.symbol, - cardToken.contractAddress, - cardToken.decimals, - ) - } - - fun isTangemNote(): Boolean = productType == ProductType.Note - fun isTangemWallet(): Boolean = productType == ProductType.Wallet - fun isTangemTwins(): Boolean = productType == ProductType.Twins - - fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed - fun supportsBackup(): Boolean = card.settings.isBackupAllowed - - fun twinsIsTwinned(): Boolean = - card.isTangemTwins() && walletData != null && secondTwinPublicKey != null -} - -typealias KeyWalletPublicKey = ByteArrayKey - class ScanProductTask( val card: Card? = null, private val currenciesRepository: CurrenciesRepository?, @@ -296,12 +251,12 @@ private class ScanWalletProcessor( if (!card.useOldStyleDerivation) { blockchainsToDerive.removeAll( listOf( - Blockchain.BSC, Blockchain.BSCTestnet, - Blockchain.Polygon, Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Fantom, Blockchain.FantomTestnet, - Blockchain.Avalanche, Blockchain.AvalancheTestnet, - ).map { BlockchainNetwork(it, card) } + Blockchain.BSC, Blockchain.BSCTestnet, + Blockchain.Polygon, Blockchain.PolygonTestnet, + Blockchain.RSK, + Blockchain.Fantom, Blockchain.FantomTestnet, + Blockchain.Avalanche, Blockchain.AvalancheTestnet, + ).map { BlockchainNetwork(it, card) } ) } return blockchainsToDerive.distinct() 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 cedc9391c7..640faa30fb 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 @@ -12,10 +12,10 @@ import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager import com.tangem.common.card.Card import com.tangem.common.card.FirmwareVersion +import com.tangem.domain.common.TapWorkarounds.derivationStyle 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.TapWorkarounds.derivationStyle import com.tangem.tap.domain.extensions.setCustomIconUrl import com.tangem.tap.features.demo.DemoHelper import timber.log.Timber diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 54b1b1b532..c5a6ae1c60 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -16,8 +16,6 @@ import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.currenciesRepository import com.tangem.tap.domain.extensions.isMultiwalletAllowed import com.tangem.tap.domain.extensions.makeWalletManagerForApp -import com.tangem.tap.domain.isMultiwalletAllowed -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.domain.walletconnect.BnbHelper import com.tangem.tap.domain.walletconnect.WalletConnectManager diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index 688c373b24..473d4aca55 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -70,7 +70,7 @@ fun AddCustomTokenScreen(state: MutableState) { HangingOverKeyboardView( modifier = Modifier .align(Alignment.BottomCenter), - keyboardState = keyboardObserverAsState(), + keyboardState = keyboardAsState(), defaultBottomPadding = 30.dp, spaceBetweenKeyboard = 20.dp, ) { @@ -92,15 +92,16 @@ private fun FormFields(state: MutableState) { val context = LocalContext.current val errorConverter = remember { CustomTokenErrorConverter(context) } - state.value.form.fieldList.forEach { field -> - val data = ScreenFieldData.fromState(field, state.value, errorConverter) + val stateValue = state.value + stateValue.form.fieldList.forEach { field -> + val data = ScreenFieldData.fromState(field, stateValue, errorConverter) when (field.id) { ContractAddress -> TokenContractAddressView(data) - Network -> TokenNetworkView(data) + Network -> TokenNetworkView(data, stateValue) Name -> TokenNameView(data) Symbol -> TokenSymbolView(data) Decimals -> TokenDecimalsView(data) - DerivationPath -> TokenDerivationPathView(data) + DerivationPath -> TokenDerivationPathView(data, stateValue) } } } @@ -145,7 +146,7 @@ private fun TokenNameView(screenFieldData: ScreenFieldData) { } @Composable -private fun TokenNetworkView(screenFieldData: ScreenFieldData) { +private fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) { if (!screenFieldData.viewState.isVisible) return val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) @@ -156,7 +157,7 @@ private fun TokenNetworkView(screenFieldData: ScreenFieldData) { itemList = networkField.itemList, selectedItem = networkField.data, isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) }, + textFieldConverter = { state.convertBlockchainName(it, notSelected) }, ) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it))) } SpacerH8() } @@ -197,7 +198,7 @@ private fun TokenDecimalsView(screenFieldData: ScreenFieldData) { } @Composable -private fun TokenDerivationPathView(screenFieldData: ScreenFieldData) { +private fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) { if (!screenFieldData.viewState.isVisible) return val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) @@ -208,10 +209,10 @@ private fun TokenDerivationPathView(screenFieldData: ScreenFieldData) { itemList = networkField.itemList, selectedItem = networkField.data, isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { AddCustomTokenState.convertBlockchainName(it, notSelected) }, + textFieldConverter = { state.convertBlockchainName(it, notSelected) }, dropdownItemView = { blockchain -> - val derivationPathLabel = AddCustomTokenState.convertDerivationPathLabel(blockchain, notSelected) - val blockchainName = AddCustomTokenState.convertBlockchainName(blockchain, notSelected) + val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected) + val blockchainName = state.convertBlockchainName(blockchain, notSelected) TitleSubtitle(derivationPathLabel, blockchainName) } ) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it))) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index a88d69cbdb..dcd52c1c49 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -10,6 +10,7 @@ import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath import com.tangem.domain.common.KeyWalletPublicKey import com.tangem.domain.common.ScanResponse +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* @@ -19,12 +20,7 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError -import com.tangem.domain.common.TapWorkarounds.derivationStyle -import com.tangem.domain.common.TapWorkarounds.derivationStyle -import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.domain.extensions.makeWalletManagerForApp -import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.BlockchainNetwork import com.tangem.tap.features.wallet.redux.WalletAction import kotlinx.coroutines.delay diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 3f8d57e676..abecd0f54a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -5,12 +5,12 @@ import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchain.extensions.isAboveZero import com.tangem.common.extensions.isZero +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.tap.common.entities.Button import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.toggleWidget.WidgetState -import com.tangem.tap.domain.TapWorkarounds.derivationStyle import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.extensions.buyIsAllowed import com.tangem.tap.domain.extensions.sellIsAllowed diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index 7218fd7758..fde2ce883d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -8,10 +8,10 @@ import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.tap.common.extensions.getString import com.tangem.tap.common.extensions.loadCurrenciesIcon import com.tangem.tap.common.extensions.show -import com.tangem.tap.domain.TapWorkarounds.derivationStyle import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletData diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 94d1081bb1..4338c9411d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -3,14 +3,14 @@ package com.tangem.tap.features.wallet.ui.wallet import android.app.Dialog import androidx.recyclerview.widget.LinearLayoutManager import com.tangem.common.card.Card +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.currenciesRepository -import com.tangem.tap.domain.TapWorkarounds.derivationStyle -import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletDialog @@ -103,9 +103,9 @@ class MultiWalletView : WalletView { val card = store.state.globalState.scanResponse!!.card store.dispatch(TokensAction.LoadCurrencies( supportedBlockchains = currenciesRepository.getBlockchains( - card.firmwareVersion, - card.isTestCard - ))) + card.firmwareVersion, + card.isTestCard + ))) store.dispatch(TokensAction.AllowToAddTokens(true)) store.dispatch(TokensAction.SetAddedCurrencies( wallets = state.walletsData, diff --git a/domain/build.gradle b/domain/build.gradle index 8d302d8eda..6b60a72e31 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -46,9 +46,9 @@ 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' + implementation 'com.tangem:blockchain:develop-70' + implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142' + implementation 'com.tangem.tangem-sdk-kotlin:android:develop-142' // Kotlin implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' diff --git a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt index 52f61659e4..b41eb4f116 100644 --- a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt +++ b/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt @@ -36,7 +36,6 @@ data class ScanResponse( cardToken.symbol, cardToken.contractAddress, cardToken.decimals, - Blockchain.fromId(walletData.blockchain) ) } diff --git a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt index d60e1ef857..a5ce7922c8 100644 --- a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt +++ b/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt @@ -1,6 +1,7 @@ package com.tangem.domain.common import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.card.Card import java.util.* @@ -9,6 +10,45 @@ import java.util.* */ object TapWorkarounds { + fun isStart2CoinIssuer(cardIssuer: String?): Boolean { + return cardIssuer?.lowercase(Locale.US) == START_2_COIN_ISSUER + } + + val Card.isStart2Coin: Boolean + get() = isStart2CoinIssuer(issuer.name) + + val Card.isTestCard: Boolean + get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) + + val Card.useOldStyleDerivation: Boolean + get() = batchId == "AC01" || batchId == "AC02" || batchId == "CB95" + + val Card.derivationStyle: DerivationStyle? + get() = if (!settings.isHDWalletAllowed) { + null + } else if (useOldStyleDerivation) { + DerivationStyle.LEGACY + } else { + DerivationStyle.NEW + } + + fun Card.isExcluded(): Boolean { + val excludedBatch = excludedBatches.contains(batchId) + val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) + return excludedBatch || excludedIssuerName + } + + fun Card.isNotSupportedInThatRelease(): Boolean { + return false + } + + @Deprecated("Use ScanResponse.isTangemNote") + fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId) + + fun isTangemWalletBatch(card: Card): Boolean = tangemWalletBatches.contains(card.batchId) + + fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] + private const val START_2_COIN_ISSUER = "start2coin" private const val TEST_CARD_BATCH = "99FF" private const val TEST_CARD_ID_STARTS_WITH = "FF99" @@ -24,6 +64,8 @@ object TapWorkarounds { "TTM BANK" ) + private val tangemWalletBatches = listOf("AC01") + private val tangemNoteBatches = mapOf( "AB01" to Blockchain.Bitcoin, "AB02" to Blockchain.Ethereum, @@ -38,31 +80,4 @@ object TapWorkarounds { private val tangemWalletBatchesWithStandardDerivationType = listOf( "AC01", "AC02", "CB95" ) - - fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId] - - val Card.isStart2Coin: Boolean - get() = isStart2CoinIssuer(issuer.name) - - val Card.isTestCard: Boolean - get() = batchId == TEST_CARD_BATCH && cardId.startsWith(TEST_CARD_ID_STARTS_WITH) - - - fun Card.isExcluded(): Boolean { - val excludedBatch = excludedBatches.contains(batchId) - val excludedIssuerName = excludedIssuers.contains(issuer.name.uppercase(Locale.ROOT)) - return excludedBatch || excludedIssuerName - } - - fun Card.isNotSupportedInThatRelease(): Boolean { - return false - } - - @Deprecated("Use ScanResponse.isTangemNote") - fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId) - - - fun isStart2CoinIssuer(cardIssuer: String?): Boolean { - return cardIssuer?.toLowerCase(Locale.US) == START_2_COIN_ISSUER - } } \ No newline at end of file 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 index 0fa2c84d99..03cb125db1 100644 --- 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 @@ -1,6 +1,7 @@ package com.tangem.domain.features.addCustomToken.redux import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle import com.tangem.domain.common.form.Field import com.tangem.domain.common.form.FieldId import com.tangem.domain.features.addCustomToken.AddCustomTokenError @@ -13,9 +14,13 @@ import org.rekotlin.Action [REDACTED_AUTHOR] */ sealed class AddCustomTokenAction : Action { - // from user, ui - object OnCreate : AddCustomTokenAction() + object OnCreate : AddCustomTokenAction() { + data class SetDerivationStyle(val derivationStyle: DerivationStyle?) : AddCustomTokenAction() + } + object OnDestroy : AddCustomTokenAction() + + // from user, ui data class OnTokenContractAddressChanged(val contractAddress: Field.Data) : AddCustomTokenAction() data class OnTokenNetworkChanged(val blockchainNetwork: Field.Data) : AddCustomTokenAction() data class OnTokenNameChanged(val tokenName: Field.Data) : AddCustomTokenAction() 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 index 4443abd14b..ea1a059fe5 100644 --- 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 @@ -3,9 +3,11 @@ package com.tangem.domain.features.addCustomToken.redux import android.webkit.ValueCallback import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.Card +import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.DomainDialog import com.tangem.domain.DomainException +import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.form.* import com.tangem.domain.features.addCustomToken.* import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* @@ -43,12 +45,13 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT cancel: ValueCallback ) { if (action !is AddCustomTokenAction) return -// val card = storeState.globalState.scanResponse?.card -// ?: throw IllegalStateException("ScanResponse must be set before showing the AddCustomToken screen") + val card = storeState.globalState.scanResponse?.card + ?: throw IllegalStateException("ScanResponse must be set before showing the AddCustomToken screen") when (action) { is OnCreate -> { -// hubState.addCustomTokenManager.attachAuthKey(card.cardPublicKey.toHexString()) + hubState.addCustomTokenManager.attachAuthKey(card.cardPublicKey.toHexString()) + dispatchOnMain(OnCreate.SetDerivationStyle(card.derivationStyle)) } is OnDestroy -> hubScope.cancel() is OnTokenContractAddressChanged -> { @@ -201,7 +204,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT if (blockchain == Blockchain.Unknown) { throw DomainException.SelectTokeNetworkException(networkId) } - AddCustomTokenState.convertBlockchainName(blockchain, "") + hubState.convertBlockchainName(blockchain, "") }, onSelect = { selectedContract -> hubScope.launch { diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index f9488b54b4..cfcdf6da1b 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -1,6 +1,7 @@ package com.tangem.domain.features.addCustomToken.redux import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.DerivationStyle import com.tangem.domain.common.form.* import com.tangem.domain.features.addCustomToken.* import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* @@ -13,7 +14,8 @@ data class AddCustomTokenState( val formErrors: Map = emptyMap(), val warnings: Set = emptySet(), val screenState: ScreenState = createInitialScreenState(), - val addCustomTokenManager: AddCustomTokenManager = AddCustomTokenManager(TangemTechService()) + val addCustomTokenManager: AddCustomTokenManager = AddCustomTokenManager(TangemTechService()), + val derivationStyle: DerivationStyle? = null ) : StateType { val completeDataType: CompleteDataType @@ -32,6 +34,15 @@ data class AddCustomTokenState( return formErrors[id] } + fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) { + Blockchain.Unknown -> unknown + else -> blockchain.fullName + } + + fun convertDerivationPathLabel(blockchain: Blockchain, unknown: String): String { + return blockchain.derivationPath(derivationStyle)?.rawPath ?: unknown + } + private fun calculateDataType(): CompleteDataType { val idsToCheck = listOf(ContractAddress, Name, Symbol, Decimals) val fieldsToCheck = form.fieldList.filter { idsToCheck.contains(it.id) } @@ -47,15 +58,6 @@ data class AddCustomTokenState( } companion object { - fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) { - Blockchain.Unknown -> unknown - else -> blockchain.fullName - } - - fun convertDerivationPathLabel(blockchain: Blockchain, unknown: String): String { - return blockchain.derivationPath()?.rawPath ?: unknown - } - private fun createFormFields(): List> { return listOf( TokenField(ContractAddress), diff --git a/network/build.gradle b/network/build.gradle index df2d688c42..f89fb54c38 100644 --- a/network/build.gradle +++ b/network/build.gradle @@ -10,7 +10,7 @@ java { dependencies { // Tangem sdk's - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-140' + implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142' // Network implementation(platform("com.squareup.okhttp3:okhttp-bom:4.9.3")) From a07f371456eeab94146e4705bb46058a69a7daaa Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Apr 2022 17:28:33 +0300 Subject: [PATCH 27/28] Updated on 2026-08-14 --- .../com/tangem/tap/common/compose/Button.kt | 4 +- .../common/compose/ComposeDialogManager.kt | 64 ++++-- .../tap/common/compose/KeyboardState.kt | 13 +- .../tap/common/compose/extensions/Context.kt | 19 ++ .../tap/common/extensions/Navigation.kt | 2 + .../tangem/tap/common/extensions/Picasso.kt | 2 +- .../redux/navigation/NavigationState.kt | 2 +- .../com/tangem/tap/domain/tokens/Currency.kt | 59 +---- ...orConverter.kt => DomainErrorConverter.kt} | 40 ++-- .../compose/AddCustomTokenScreen.kt | 206 +++++------------ .../addCustomToken/compose/DebugActions.kt | 4 +- .../addCustomToken/compose/FormFieldViews.kt | 134 +++++++++++ .../compose/HangingOverKeyboardView.kt | 31 +-- .../tap/features/tokens/redux/TokensAction.kt | 4 +- .../features/tokens/redux/TokensMiddleware.kt | 24 ++ .../tap/features/tokens/redux/TokensState.kt | 2 +- .../features/tokens/ui/AddTokensFragment.kt | 4 + .../ui/compose/CollapsedCurrencyItem.kt | 2 +- .../tokens/ui/compose/CurrenciesScreen.kt | 2 +- .../tokens/ui/compose/ExpandedCurrencyItem.kt | 2 +- app/src/main/res/menu/popular_tokens.xml | 7 + app/src/main/res/values/colors.xml | 21 +- .../java/com/tangem/domain/DomainError.kt | 2 +- .../java/com/tangem/domain/DomainException.kt | 4 + .../java/com/tangem/domain/DomainMessage.kt | 15 ++ .../com/tangem/domain/DomainStateDialog.kt | 2 + .../java/com/tangem/domain/DomainWrapped.kt | 18 ++ .../domain/common/extensions/Blockchain.kt | 60 +++++ .../features/addCustomToken/CompleteData.kt | 53 +++-- .../domain/features/addCustomToken/Errors.kt | 10 +- ...Manager.kt => TangemTechServiceManager.kt} | 2 +- .../redux/AddCustomTokenAction.kt | 11 + .../addCustomToken/redux/AddCustomTokenHub.kt | 212 ++++++++---------- .../redux/AddCustomTokenState.kt | 81 +++++-- .../features/addCustomToken/redux/Models.kt | 10 +- .../com/tangem/domain/redux/ReStoreHub.kt | 7 + 36 files changed, 666 insertions(+), 469 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt rename app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/{CustomTokenErrorConverter.kt => DomainErrorConverter.kt} (71%) create mode 100644 app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt create mode 100644 domain/src/main/java/com/tangem/domain/DomainMessage.kt create mode 100644 domain/src/main/java/com/tangem/domain/DomainWrapped.kt create mode 100644 domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt rename domain/src/main/java/com/tangem/domain/features/addCustomToken/{AddCustomTokenManager.kt => TangemTechServiceManager.kt} (98%) diff --git a/app/src/main/java/com/tangem/tap/common/compose/Button.kt b/app/src/main/java/com/tangem/tap/common/compose/Button.kt index 64b9daa152..992939febe 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Button.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Button.kt @@ -19,7 +19,7 @@ import com.tangem.wallet.R [REDACTED_AUTHOR] */ @Composable -fun Button( +fun RectangleButton( modifier: Modifier = Modifier, text: String = "", textId: Int? = null, @@ -90,7 +90,7 @@ fun ButtonTest() { ) { Column(modifier = Modifier.padding(16.dp)) { PreviewItem("Button") { - Button(text = "Some button") {} + RectangleButton(text = "Some button") {} } PreviewItem("PasteButton") { PasteButton(onClick = {}) diff --git a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt index 80cb2936ec..aa0e7e71d0 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/ComposeDialogManager.kt @@ -4,13 +4,12 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.LocalTextStyle -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -22,7 +21,9 @@ import com.tangem.domain.DomainStateDialog import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.domain.redux.global.DomainGlobalState +import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter import com.tangem.tap.features.tokens.addCustomToken.compose.SelectTokenNetworkDialog +import com.tangem.wallet.R import org.rekotlin.StoreSubscriber @Composable @@ -51,12 +52,19 @@ fun ComposeDialogManager() { } @Composable -fun ShowTheDialog(dialogState: MutableState) { +private fun ShowTheDialog(dialogState: MutableState) { if (dialogState.value == null) return + val context = LocalContext.current + val errorConverter = remember { DomainErrorConverter(context) } val onDismissRequest = { domainStore.dispatch(DomainGlobalAction.ShowDialog(null)) } when (val dialog = dialogState.value) { + is DomainDialog.DialogError -> ErrorDialog( + title = stringResource(id = R.string.common_error), + body = errorConverter.convertError(dialog.error), + onDismissRequest + ) is DomainDialog.SelectTokenDialog -> SelectTokenNetworkDialog(dialog, onDismissRequest) } } @@ -83,17 +91,7 @@ fun SimpleDialog( Column( modifier = Modifier.padding(22.dp) ) { - Text( - text = title, - style = LocalTextStyle.provides( - TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 20.sp - ) - ).value - ) - - SpacerH16() + DialogTitle(title = title) LazyColumn() { items(items) { item -> Row( @@ -111,4 +109,36 @@ fun SimpleDialog( } } } -} \ No newline at end of file +} + +@Composable +private fun DialogTitle(title: String) { + Text( + text = title, + style = LocalTextStyle.provides( + TextStyle( + fontWeight = FontWeight.Bold, + fontSize = 20.sp + ) + ).value + ) + SpacerH16() +} + +@Composable +fun ErrorDialog( + title: String, + body: String, + onDismissRequest: () -> Unit, +) { + AlertDialog( + title = { DialogTitle(title) }, + text = { Text(body) }, + onDismissRequest = onDismissRequest, + confirmButton = { + Button(onClick = onDismissRequest) { + Text(text = stringResource(id = R.string.common_ok)) + } + } + ) +} diff --git a/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt b/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt index 2c8fc377be..2f08f3ae15 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/KeyboardState.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalView sealed class Keyboard { - data class Opened(val height: Int): Keyboard() + data class Opened(val height: Int) : Keyboard() object Closed : Keyboard() } @@ -14,12 +14,21 @@ sealed class Keyboard { fun keyboardAsState(): State { val keyboardState: MutableState = remember { mutableStateOf(Keyboard.Closed) } val view = LocalView.current + val discrepancy = remember { + mutableStateOf(0) + } DisposableEffect(view) { val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() view.getWindowVisibleDisplayFrame(rect) val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom + val keypadHeight: Int = screenHeight - (rect.bottom + rect.top) - discrepancy.value + if (discrepancy.value == 0) { + discrepancy.value = keypadHeight; + if (keypadHeight == 0) discrepancy.value = 1 + } + keyboardState.value = if (keypadHeight > screenHeight * 0.15) { Keyboard.Opened(keypadHeight) } else { diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt new file mode 100644 index 0000000000..25ae94623c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Context.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.common.compose.extensions + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.tangem.tap.common.extensions.copyToClipboard +import com.tangem.tap.common.extensions.getFromClipboard + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun copyToClipboard(value: Any, label: String = "") { + LocalContext.current.copyToClipboard(value, label) +} + +@Composable +fun getFromClipboard(default: CharSequence? = null): CharSequence? { + return LocalContext.current.getFromClipboard(default) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 5fdd3a9279..86d8393672 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -20,6 +20,7 @@ import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.shop.ui.ShopFragment +import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment import com.tangem.tap.features.tokens.ui.AddTokensFragment import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment @@ -82,6 +83,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.DetailsSecurity -> DetailsSecurityFragment() AppScreen.Disclaimer -> DisclaimerFragment() AppScreen.AddTokens -> AddTokensFragment() + AppScreen.AddCustomToken -> AddCustomTokenFragment() AppScreen.WalletDetails -> WalletDetailsFragment() AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment() AppScreen.QrScan -> QrScanFragment() diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt index 4799d802ad..dbeefadbe8 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt @@ -9,9 +9,9 @@ import com.squareup.picasso.Transformation import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.IconsUtil import com.tangem.blockchain.common.Token +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.tap.domain.extensions.getCustomIconUrl import com.tangem.tap.domain.tokens.getIconUrl -import com.tangem.tap.domain.tokens.toNetworkId import com.tangem.wallet.R fun Picasso.loadCurrenciesIcon( diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 6f68439f61..1fc6832148 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -17,7 +17,7 @@ enum class AppScreen { Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, - AddTokens, + AddTokens, AddCustomToken, WalletConnectSessions, QrScan } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt index be3d1b4229..128190acd2 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt @@ -2,6 +2,7 @@ package com.tangem.tap.domain.tokens import com.squareup.moshi.JsonClass import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId @JsonClass(generateAdapter = true) data class CurrencyFromJson( @@ -74,62 +75,4 @@ data class Contract( fun getIconUrl(id: String): String { return "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/$id.png" -} - - -fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { - return when (networkId) { - "avalanche" -> Blockchain.Avalanche - "binancecoin" -> Blockchain.Binance - "binance-smart-chain" -> Blockchain.BSC - "ethereum" -> Blockchain.Ethereum - "polygon-pos" -> Blockchain.Polygon - "solana" -> Blockchain.Solana - "fantom" -> Blockchain.Fantom - "bitcoin" -> Blockchain.Bitcoin - "bitcoin-cash" -> Blockchain.BitcoinCash - "cardano" -> Blockchain.CardanoShelley - "dogecoin" -> Blockchain.Dogecoin - "ducatus" -> Blockchain.Ducatus - "litecoin" -> Blockchain.Litecoin - "rsk" -> Blockchain.RSK - "stellar" -> Blockchain.Stellar - "tezos" -> Blockchain.Tezos - "ripple" -> Blockchain.XRP - else -> null - } -} - -fun Blockchain.toNetworkId(): String { - return when (this) { - Blockchain.Unknown -> "unknown" - Blockchain.Avalanche -> "avalanche" - Blockchain.AvalancheTestnet -> "avalaunche" - Blockchain.Binance -> "binancecoin" - Blockchain.BinanceTestnet -> "binancecoin" - Blockchain.BSC -> "binance-smart-chain" - Blockchain.BSCTestnet -> "binance-smart-chain" - Blockchain.Bitcoin -> "bitcoin" - Blockchain.BitcoinTestnet -> "bitcoin" - Blockchain.BitcoinCash -> "bitcoin-cash" - Blockchain.BitcoinCashTestnet -> "bitcoin-cash" - Blockchain.Cardano -> "cardano" - Blockchain.CardanoShelley -> "cardano" - Blockchain.Dogecoin -> "dogecoin" - Blockchain.Ducatus -> "ducatus" - Blockchain.Ethereum -> "ethereum" - Blockchain.EthereumTestnet -> "ethereum" - Blockchain.Fantom -> "fantom" - Blockchain.FantomTestnet -> "fantom" - Blockchain.Litecoin -> "litecoin" - Blockchain.Polygon -> "matic-network" - Blockchain.PolygonTestnet -> "matic-networks" - Blockchain.RSK -> "rootstock" - Blockchain.Stellar -> "stellar" - Blockchain.StellarTestnet -> "stellar" - Blockchain.Solana -> "solana" - Blockchain.SolanaTestnet -> "solana" - Blockchain.Tezos -> "tezos" - Blockchain.XRP -> "ripple" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt similarity index 71% rename from app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt rename to app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt index f151530a1b..89c205afee 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/CustomTokenErrorConverter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/DomainErrorConverter.kt @@ -10,40 +10,40 @@ import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -class CustomTokenErrorConverter( +class DomainErrorConverter( + private val context: Context +) : ErrorConverter { + + override fun convertError(error: DomainError): String { + val errorMessage = when (error) { + is AddCustomTokenError -> AddCustomTokenConverter(context).convertError(error) + else -> null + } + return errorMessage?.let { it } ?: "Unknown error: ${error::class.java.simpleName}" + } +} + +private class AddCustomTokenConverter( private val context: Context ) : ErrorConverter { override fun convertError(error: DomainError): String { val customTokenError = (error as? AddCustomTokenError) ?: throw UnsupportedOperationException() - val resId = when (customTokenError) { + val rawMessage = when (customTokenError) { + AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found + AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added AddCustomTokenError.InvalidContractAddress -> R.string.custom_token_creation_error_invalid_contract_address AddCustomTokenError.NetworkIsNotSelected -> R.string.custom_token_creation_error_network_not_selected AddCustomTokenError.InvalidDerivationPath -> R.string.custom_token_creation_error_invalid_derivation_path + AddCustomTokenError.InvalidDecimalsCount -> R.string.custom_token_creation_error_wrong_decimals AddCustomTokenError.FieldIsEmpty -> R.string.custom_token_creation_error_empty_fields else -> null } - return resId?.let { context.getString(it) } ?: "Unknown error: ${customTokenError::class.java.simpleName}" - } -} - -class CustomTokenWarningConverter( - private val context: Context -) : ErrorConverter { - - override fun convertError(error: DomainError): String { - val customTokenWarning = (error as? AddCustomTokenWarning) ?: throw UnsupportedOperationException() - - val rawMessage = when (customTokenWarning) { - AddCustomTokenWarning.PotentialScamToken -> R.string.custom_token_validation_error_not_found - AddCustomTokenWarning.TokenAlreadyAdded -> R.string.custom_token_validation_error_already_added - AddCustomTokenWarning.Network.CheckAddressRequestError -> "CheckAddressRequestError" - } return when (rawMessage) { is Int -> context.getString(rawMessage) - is String -> rawMessage - else -> "Unknown error: ${customTokenWarning::class.java.simpleName}" +// is String -> rawMessage + else -> "Unknown error: ${customTokenError::class.java.simpleName}" } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt index 473d4aca55..ad66229ed4 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/AddCustomTokenScreen.kt @@ -2,34 +2,31 @@ package com.tangem.tap.features.tokens.addCustomToken.compose import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tangem.domain.ErrorConverter import com.tangem.domain.common.form.DataField -import com.tangem.domain.common.form.Field import com.tangem.domain.common.form.FieldId -import com.tangem.domain.features.addCustomToken.* +import com.tangem.domain.features.addCustomToken.AddCustomTokenError +import com.tangem.domain.features.addCustomToken.AddCustomTokenWarning import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction -import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.* import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState import com.tangem.domain.features.addCustomToken.redux.ScreenState import com.tangem.domain.features.addCustomToken.redux.ViewStates import com.tangem.domain.redux.domainStore -import com.tangem.tap.common.compose.* -import com.tangem.tap.features.tokens.addCustomToken.CustomTokenErrorConverter -import com.tangem.tap.features.tokens.addCustomToken.CustomTokenWarningConverter +import com.tangem.tap.common.compose.ComposeDialogManager +import com.tangem.tap.common.compose.keyboardAsState +import com.tangem.tap.features.tokens.addCustomToken.DomainErrorConverter import com.tangem.wallet.R /** @@ -44,12 +41,18 @@ fun AddCustomTokenScreen(state: MutableState) { Scaffold( scaffoldState = scaffoldState, backgroundColor = colorResource(id = R.color.backgroundLightGray), + floatingActionButton = { + HangingOverKeyboardView(keyboardState = keyboardAsState()) { + AddButton(state) + } + }, + floatingActionButtonPosition = FabPosition.Center, ) { Box(Modifier.fillMaxSize()) { LazyColumn( contentPadding = PaddingValues(bottom = 90.dp) ) { - item { AddCustomTokenDebugActions() } +// item { AddCustomTokenDebugActions() } item { Surface( modifier = Modifier.padding(16.dp), @@ -67,18 +70,6 @@ fun AddCustomTokenScreen(state: MutableState) { } item { Warnings(state.value.warnings.toList()) } } - HangingOverKeyboardView( - modifier = Modifier - .align(Alignment.BottomCenter), - keyboardState = keyboardAsState(), - defaultBottomPadding = 30.dp, - spaceBetweenKeyboard = 20.dp, - ) { - AddButton( - isEnabled = state.value.screenState.addButton.isEnabled - ) { - } - } } ComposeDialogManager() } @@ -90,7 +81,7 @@ fun AddCustomTokenScreen(state: MutableState) { @Composable private fun FormFields(state: MutableState) { val context = LocalContext.current - val errorConverter = remember { CustomTokenErrorConverter(context) } + val errorConverter = remember { DomainErrorConverter(context) } val stateValue = state.value stateValue.form.fieldList.forEach { field -> @@ -107,124 +98,11 @@ private fun FormFields(state: MutableState) { } @Composable -private fun TokenContractAddressView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - textFieldData = tokenField.data, - labelId = R.string.custom_token_contract_address_input_title, - placeholder = "0x0000000000000000", - isEnabled = screenFieldData.viewState.isEnabled, - isLoading = screenFieldData.viewState.isLoading, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { - domainStore.dispatch(OnTokenContractAddressChanged(Field.Data(it))) - } - SpacerH8() -} - -@Composable -private fun TokenNameView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - textFieldData = tokenField.data, - labelId = R.string.custom_token_name_input_title, - placeholderId = R.string.custom_token_name_input_placeholder, - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { - domainStore.dispatch(OnTokenNameChanged(Field.Data(it))) - } - SpacerH8() -} - -@Composable -private fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) { - if (!screenFieldData.viewState.isVisible) return - - val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) - val networkField = screenFieldData.field as TokenBlockchainField - - BlockchainSpinner( - title = R.string.custom_token_network_input_title, - itemList = networkField.itemList, - selectedItem = networkField.data, - isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.convertBlockchainName(it, notSelected) }, - ) { domainStore.dispatch(OnTokenNetworkChanged(Field.Data(it))) } - SpacerH8() -} - -@Composable -private fun TokenSymbolView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - textFieldData = tokenField.data, - labelId = R.string.custom_token_token_symbol_input_title, - placeholderId = R.string.custom_token_token_symbol_input_placeholder, - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - ) { domainStore.dispatch(OnTokenSymbolChanged(Field.Data(it))) } - SpacerH8() -} - -@Composable -private fun TokenDecimalsView(screenFieldData: ScreenFieldData) { - if (!screenFieldData.viewState.isVisible) return - - val tokenField = screenFieldData.field as TokenField - - OutlinedTextFieldWidget( - textFieldData = tokenField.data, - labelId = R.string.custom_token_decimals_input_title, - placeholder = "8", - isEnabled = screenFieldData.viewState.isEnabled, - error = screenFieldData.error, - errorConverter = screenFieldData.errorConverter, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - ) { domainStore.dispatch(OnTokenDecimalsChanged(Field.Data(it))) } - SpacerH8() -} - -@Composable -private fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) { - if (!screenFieldData.viewState.isVisible) return - - val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) - val networkField = screenFieldData.field as TokenDerivationPathField - - BlockchainSpinner( - title = R.string.custom_token_derivation_path_input_title, - itemList = networkField.itemList, - selectedItem = networkField.data, - isEnabled = screenFieldData.viewState.isEnabled, - textFieldConverter = { state.convertBlockchainName(it, notSelected) }, - dropdownItemView = { blockchain -> - val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected) - val blockchainName = state.convertBlockchainName(blockchain, notSelected) - TitleSubtitle(derivationPathLabel, blockchainName) - } - ) { domainStore.dispatch(OnTokenDerivationPathChanged(Field.Data(it))) } - SpacerH8() -} - -@Composable -private fun Warnings(warnings: List) { +fun Warnings(warnings: List) { if (warnings.isEmpty()) return val context = LocalContext.current - val warningConverter = remember { CustomTokenWarningConverter(context) } + val warningConverter = remember { DomainErrorConverter(context) } Column { warnings.forEachIndexed { index, item -> @@ -236,8 +114,8 @@ private fun Warnings(warnings: List) { Surface( modifier = modifier.fillMaxWidth(), shape = MaterialTheme.shapes.small, - color = colorResource(id = R.color.darkGray2), - contentColor = colorResource(id = R.color.darkGray3) + color = colorResource(id = R.color.warning_warning), + elevation = 4.dp, ) { Text( modifier = Modifier.padding(16.dp), @@ -251,30 +129,48 @@ private fun Warnings(warnings: List) { } @Composable -private fun AddButton( +private fun AddButton(state: MutableState) { + AddCustomTokenFab( + modifier = Modifier + .widthIn(210.dp, 280.dp), + isEnabled = state.value.screenState.addButton.isEnabled + ) { domainStore.dispatch(AddCustomTokenAction.OnAddCustomTokenClicked) } +} + +@Composable +fun AddCustomTokenFab( modifier: Modifier = Modifier, - isEnabled: Boolean, - textId: Int = R.string.common_add, - onClick: () -> Unit, + isEnabled: Boolean = true, + onClick: () -> Unit ) { - Button( - textId = textId, - isEnabled = isEnabled, - modifier = modifier - .height(52.dp) - .padding(horizontal = 16.dp) - .fillMaxWidth(), - leadingView = { + val contentColor = if (isEnabled) { + Color.White + } else { + colorResource(id = R.color.darkGray1) + } + val backgroundColor = Color(0xFF1ACE80) + + ExtendedFloatingActionButton( + modifier = modifier, + icon = { Icon( imageVector = Icons.Filled.Add, + tint = contentColor, contentDescription = "Add", ) }, - onClick = onClick + text = { + Text( + text = stringResource(id = R.string.common_add), + ) + }, + onClick = onClick, + backgroundColor = backgroundColor, + contentColor = contentColor, ) } -private data class ScreenFieldData( +data class ScreenFieldData( val field: DataField<*>, val error: AddCustomTokenError?, val errorConverter: ErrorConverter, @@ -284,7 +180,7 @@ private data class ScreenFieldData( fun fromState( field: DataField<*>, state: AddCustomTokenState, - errorConverter: CustomTokenErrorConverter + errorConverter: DomainErrorConverter ): ScreenFieldData { return ScreenFieldData( field = field, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt index 56ab3e1f37..18c1a5fc2e 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/DebugActions.kt @@ -14,8 +14,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.VoidCallback import com.tangem.common.services.Result import com.tangem.domain.common.form.Field +import com.tangem.domain.features.addCustomToken.TangemTechServiceManager import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction import com.tangem.domain.redux.domainStore +import com.tangem.network.api.tangemTech.TangemTechService import com.tangem.wallet.BuildConfig import timber.log.Timber @@ -145,7 +147,7 @@ private fun CustomActions() { CustomActionButton( name = "Find tokens in several networks", action = { - val manager = domainStore.state.addCustomTokensState.addCustomTokenManager + val manager = TangemTechServiceManager(TangemTechService()) val currencies = manager.tokens() val asdfsd = mutableMapOf>() val contractAddresses = currencies.mapNotNull { currency -> diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt new file mode 100644 index 0000000000..e9207987e7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/FormFieldViews.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.features.tokens.addCustomToken.compose + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.domain.common.form.Field +import com.tangem.domain.features.addCustomToken.TokenBlockchainField +import com.tangem.domain.features.addCustomToken.TokenDerivationPathField +import com.tangem.domain.features.addCustomToken.TokenField +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState +import com.tangem.domain.redux.domainStore +import com.tangem.tap.common.compose.BlockchainSpinner +import com.tangem.tap.common.compose.OutlinedTextFieldWidget +import com.tangem.tap.common.compose.SpacerH8 +import com.tangem.tap.common.compose.TitleSubtitle +import com.tangem.wallet.R + +/** +[REDACTED_AUTHOR] + */ +@Composable +fun TokenContractAddressView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_contract_address_input_title, + placeholder = "0x0000000000000000", + isEnabled = screenFieldData.viewState.isEnabled, + isLoading = screenFieldData.viewState.isLoading, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { + domainStore.dispatch(AddCustomTokenAction.OnTokenContractAddressChanged(Field.Data(it))) + } + SpacerH8() +} + +@Composable +fun TokenNameView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_name_input_title, + placeholderId = R.string.custom_token_name_input_placeholder, + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { + domainStore.dispatch(AddCustomTokenAction.OnTokenNameChanged(Field.Data(it))) + } + SpacerH8() +} + +@Composable +fun TokenNetworkView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) { + if (!screenFieldData.viewState.isVisible) return + + val notSelected = stringResource(id = R.string.custom_token_network_input_not_selected) + val networkField = screenFieldData.field as TokenBlockchainField + + BlockchainSpinner( + title = R.string.custom_token_network_input_title, + itemList = networkField.itemList, + selectedItem = networkField.data, + isEnabled = screenFieldData.viewState.isEnabled, + textFieldConverter = { state.convertBlockchainName(it, notSelected) }, + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenNetworkChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +fun TokenSymbolView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_token_symbol_input_title, + placeholderId = R.string.custom_token_token_symbol_input_placeholder, + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenSymbolChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +fun TokenDecimalsView(screenFieldData: ScreenFieldData) { + if (!screenFieldData.viewState.isVisible) return + + val tokenField = screenFieldData.field as TokenField + + OutlinedTextFieldWidget( + textFieldData = tokenField.data, + labelId = R.string.custom_token_decimals_input_title, + placeholder = "8", + isEnabled = screenFieldData.viewState.isEnabled, + error = screenFieldData.error, + errorConverter = screenFieldData.errorConverter, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDecimalsChanged(Field.Data(it))) } + SpacerH8() +} + +@Composable +fun TokenDerivationPathView(screenFieldData: ScreenFieldData, state: AddCustomTokenState) { + if (!screenFieldData.viewState.isVisible) return + + val notSelected = stringResource(id = R.string.custom_token_derivation_path_default) + val networkField = screenFieldData.field as TokenDerivationPathField + + BlockchainSpinner( + title = R.string.custom_token_derivation_path_input_title, + itemList = networkField.itemList, + selectedItem = networkField.data, + isEnabled = screenFieldData.viewState.isEnabled, + textFieldConverter = { state.convertBlockchainName(it, notSelected) }, + dropdownItemView = { blockchain -> + val derivationPathLabel = state.convertDerivationPathLabel(blockchain, notSelected) + val blockchainName = state.convertBlockchainName(blockchain, notSelected) + TitleSubtitle(derivationPathLabel, blockchainName) + } + ) { domainStore.dispatch(AddCustomTokenAction.OnTokenDerivationPathChanged(Field.Data(it))) } + SpacerH8() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt index 0a8f736373..46e31151a8 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/addCustomToken/compose/HangingOverKeyboardView.kt @@ -1,7 +1,5 @@ package com.tangem.tap.features.tokens.addCustomToken.compose -import android.content.Context -import android.util.TypedValue import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding @@ -21,36 +19,19 @@ import com.tangem.tap.common.compose.Keyboard fun HangingOverKeyboardView( modifier: Modifier = Modifier, keyboardState: State, - defaultBottomPadding: Dp = 0.dp, - spaceBetweenKeyboard: Dp = 10.dp, - calculateWithActionBarHeight: Boolean = true, + spaceBetweenKeyboard: Dp = 0.dp, content: @Composable() (BoxScope.() -> Unit) ) { - fun getActionBarHeight(context: Context): Int { - val typedValue = TypedValue() - return if (context.theme.resolveAttribute(android.R.attr.actionBarSize, typedValue, true)) { - val data = typedValue.data - val displayMetrics = context.resources.displayMetrics - TypedValue.complexToDimensionPixelSize(data, displayMetrics) - } else { - 0 - } - } val context = LocalContext.current - val calculatedPadding = when (keyboardState.value) { - Keyboard.Closed -> defaultBottomPadding + val padding = when (keyboardState.value) { + Keyboard.Closed -> 0.dp is Keyboard.Opened -> { val keyboardHeight = (keyboardState.value as Keyboard.Opened).height val keyboardPadding = context.pxToDp(keyboardHeight.toFloat()).dp - if (calculateWithActionBarHeight) { - val actionBarHeight = context.pxToDp(getActionBarHeight(context).toFloat()).dp - keyboardPadding + spaceBetweenKeyboard - actionBarHeight - } else { - keyboardPadding + spaceBetweenKeyboard - } - + keyboardPadding + spaceBetweenKeyboard } } - Box(modifier.padding(bottom = calculatedPadding)) { content() } + + Box(modifier.padding(bottom = padding)) { content() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt index 9582b3e9a7..d9bafa0dd3 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -18,11 +18,13 @@ sealed class TokensAction : Action { data class SetAddedCurrencies( val wallets: List, val derivationStyle: DerivationStyle? - ) : TokensAction() + ) : TokensAction() data class SetNonRemovableCurrencies(val wallets: List) : TokensAction() data class SaveChanges( val addedTokens: List, val addedBlockchains: List ) : TokensAction() + + object PrepareAndNavigateToAddCustomToken : TokensAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index dcd52c1c49..2984b99f31 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -8,16 +8,22 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.common.hdWallet.DerivationPath +import com.tangem.domain.DomainWrapped import com.tangem.domain.common.KeyWalletPublicKey import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.features.addCustomToken.CompleteData +import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction +import com.tangem.domain.features.addCustomToken.redux.AddedCurrencies +import com.tangem.domain.redux.domainStore import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makeWalletManagerForApp @@ -26,6 +32,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Middleware +import timber.log.Timber class TokensMiddleware { @@ -35,6 +42,7 @@ class TokensMiddleware { when (action) { is TokensAction.LoadCurrencies -> handleLoadCurrencies(action) is TokensAction.SaveChanges -> handleSaveChanges(action) + is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken(action) } next(action) } @@ -80,6 +88,22 @@ class TokensMiddleware { } } + private fun handleAddingCustomToken(action: TokensAction.PrepareAndNavigateToAddCustomToken) { + val tokensState = store.state.tokensState + val addedTokensList = tokensState.addedTokens.map { + DomainWrapped.TokenWithBlockchain(it.token.copy(), it.blockchain) + } + val addedBlockchains = tokensState.addedBlockchains.map { it } + val addedCurrencies = AddedCurrencies(addedTokensList, addedBlockchains) + domainStore.dispatch(AddCustomTokenAction.Init.SetAddedCurrencies(addedCurrencies)) + + val callback = fun(data: CompleteData) { + Timber.e("Yoooohhhoooo") + } + domainStore.dispatch(AddCustomTokenAction.Init.SetOnAddTokenCallback(callback)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) + } + private fun deriveMissingBlockchains( scanResponse: ScanResponse, blockchains: List, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt index 22db9bc589..29b7289131 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.tokens.redux import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.Token +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.domain.tokens.fromNetworkId import com.tangem.tap.features.wallet.redux.WalletData import org.rekotlin.StateType diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index fd1062e1dc..e0ec165944 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -103,6 +103,10 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.menu_search -> true + R.id.menu_navigate_add_custom_token -> { + store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken) + true + } else -> super.onOptionsItemSelected(item) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt index 5d5de44aa4..51db0013b3 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CollapsedCurrencyItem.kt @@ -16,10 +16,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.SubcomposeAsyncImage import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.common.extensions.getGreyedOutIconRes import com.tangem.tap.common.extensions.getRoundIconRes import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.domain.tokens.fromNetworkId import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt index e9b9d3f3eb..a37109a9ef 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt @@ -13,11 +13,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.common.compose.Keyboard import com.tangem.tap.common.compose.keyboardAsState import com.tangem.tap.common.extensions.pixelsToDp import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.domain.tokens.fromNetworkId import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.tap.features.tokens.redux.TokensState diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt index 1756881f8f..e5f57c2eaf 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt @@ -18,8 +18,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.SubcomposeAsyncImage import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.domain.tokens.Currency -import com.tangem.tap.domain.tokens.fromNetworkId import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.TokenWithBlockchain import com.tangem.wallet.R diff --git a/app/src/main/res/menu/popular_tokens.xml b/app/src/main/res/menu/popular_tokens.xml index 80d52565c9..5cc1ac82f8 100644 --- a/app/src/main/res/menu/popular_tokens.xml +++ b/app/src/main/res/menu/popular_tokens.xml @@ -9,4 +9,11 @@ app:actionViewClass="androidx.appcompat.widget.SearchView" app:showAsAction="always" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3318e7609d..ecb9562aad 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -17,8 +17,17 @@ #FFB71B #FFFFFF - #8E8E93 - #636366 + + #F3F3F3 + #F8F8FB + #DADADF + #D0D0D5 + #C7C7CC + #D1D1D6 + #CACACC + #B6B6B8 + #8E8E90 + #666668 #48484A #3A3A3C #2C2C2E @@ -37,14 +46,6 @@ #14181D - #F3F3F3 - #F8F8FB - #DADADF - #D0D0D5 - #C7C7CC - #D1D1D6 - #C9C9CD - #1F000000 #14212121 diff --git a/domain/src/main/java/com/tangem/domain/DomainError.kt b/domain/src/main/java/com/tangem/domain/DomainError.kt index ad0115427a..7f6b14c1ed 100644 --- a/domain/src/main/java/com/tangem/domain/DomainError.kt +++ b/domain/src/main/java/com/tangem/domain/DomainError.kt @@ -6,7 +6,7 @@ package com.tangem.domain * @property message the error description * @property data any data that can help in the part where this error is being handled */ -interface DomainError { +interface DomainError : DomainMessage { val code: Int val message: String val data: Any? diff --git a/domain/src/main/java/com/tangem/domain/DomainException.kt b/domain/src/main/java/com/tangem/domain/DomainException.kt index 4788eed8f8..f137262717 100644 --- a/domain/src/main/java/com/tangem/domain/DomainException.kt +++ b/domain/src/main/java/com/tangem/domain/DomainException.kt @@ -10,4 +10,8 @@ sealed class DomainException(message: String?) : Throwable(message), DomainInter data class SelectTokeNetworkException(val networkId: String) : DomainException( "Unknown network [$networkId] should not be included in the network selection dialog." ) + + data class UnAppropriateInitializationException(val of: String, val info: String? = null) : DomainException( + "The [$of], must be properly initialized. Info []" + ) } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainMessage.kt b/domain/src/main/java/com/tangem/domain/DomainMessage.kt new file mode 100644 index 0000000000..5ff7266ab9 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainMessage.kt @@ -0,0 +1,15 @@ +package com.tangem.domain + +/** +[REDACTED_AUTHOR] + */ +sealed interface DomainMessage + +sealed interface DomainNotification : DomainMessage { + interface Toast : DomainNotification {} + + interface Snackbar : DomainNotification {} + + interface Dialog : DomainNotification {} + +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt index f2df99a7c1..a3c7aa9da2 100644 --- a/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt +++ b/domain/src/main/java/com/tangem/domain/DomainStateDialog.kt @@ -10,6 +10,8 @@ interface DomainStateDialog sealed class DomainDialog : DomainStateDialog { + data class DialogError(val error: DomainError) : DomainDialog() + data class SelectTokenDialog( val items: List, val networkIdConverter: (String) -> String, diff --git a/domain/src/main/java/com/tangem/domain/DomainWrapped.kt b/domain/src/main/java/com/tangem/domain/DomainWrapped.kt new file mode 100644 index 0000000000..41ab1c8569 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/DomainWrapped.kt @@ -0,0 +1,18 @@ +package com.tangem.domain + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token + +/** +[REDACTED_AUTHOR] + * Provides a temporary copies of the app module classes, data structures, etc. + */ +//TODO: refactoring: : after refactoring they should be unwrapped and moved +// to appropriate parts of module +sealed interface DomainWrapped { + + data class TokenWithBlockchain( + val token: Token, + val blockchain: Blockchain + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt new file mode 100644 index 0000000000..94e5daec42 --- /dev/null +++ b/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.common.extensions + +import com.tangem.blockchain.common.Blockchain + +fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { + return when (networkId) { + "avalanche" -> Blockchain.Avalanche + "binancecoin" -> Blockchain.Binance + "binance-smart-chain" -> Blockchain.BSC + "ethereum" -> Blockchain.Ethereum + "polygon-pos" -> Blockchain.Polygon + "solana" -> Blockchain.Solana + "fantom" -> Blockchain.Fantom + "bitcoin" -> Blockchain.Bitcoin + "bitcoin-cash" -> Blockchain.BitcoinCash + "cardano" -> Blockchain.CardanoShelley + "dogecoin" -> Blockchain.Dogecoin + "ducatus" -> Blockchain.Ducatus + "litecoin" -> Blockchain.Litecoin + "rsk" -> Blockchain.RSK + "stellar" -> Blockchain.Stellar + "tezos" -> Blockchain.Tezos + "ripple" -> Blockchain.XRP + else -> null + } +} + +fun Blockchain.toNetworkId(): String { + return when (this) { + Blockchain.Unknown -> "unknown" + Blockchain.Avalanche -> "avalanche" + Blockchain.AvalancheTestnet -> "avalaunche" + Blockchain.Binance -> "binancecoin" + Blockchain.BinanceTestnet -> "binancecoin" + Blockchain.BSC -> "binance-smart-chain" + Blockchain.BSCTestnet -> "binance-smart-chain" + Blockchain.Bitcoin -> "bitcoin" + Blockchain.BitcoinTestnet -> "bitcoin" + Blockchain.BitcoinCash -> "bitcoin-cash" + Blockchain.BitcoinCashTestnet -> "bitcoin-cash" + Blockchain.Cardano -> "cardano" + Blockchain.CardanoShelley -> "cardano" + Blockchain.Dogecoin -> "dogecoin" + Blockchain.Ducatus -> "ducatus" + Blockchain.Ethereum -> "ethereum" + Blockchain.EthereumTestnet -> "ethereum" + Blockchain.Fantom -> "fantom" + Blockchain.FantomTestnet -> "fantom" + Blockchain.Litecoin -> "litecoin" + Blockchain.Polygon -> "matic-network" + Blockchain.PolygonTestnet -> "matic-networks" + Blockchain.RSK -> "rootstock" + Blockchain.Stellar -> "stellar" + Blockchain.StellarTestnet -> "stellar" + Blockchain.Solana -> "solana" + Blockchain.SolanaTestnet -> "solana" + Blockchain.Tezos -> "tezos" + Blockchain.XRP -> "ripple" + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt index 88ef38e4cd..90b11472d2 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/CompleteData.kt @@ -1,8 +1,8 @@ package com.tangem.domain.features.addCustomToken import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token import com.tangem.domain.common.form.BaseFieldDataConverter -import com.tangem.domain.common.form.FieldDataConverter import com.tangem.domain.common.form.FieldId /** @@ -14,47 +14,44 @@ enum class CompleteDataType { 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 network: Blockchain, val derivationPath: String? ) : CompleteData() { class Converter : BaseFieldDataConverter() { - override fun getConvertedData(): CustomBlockchain = CustomBlockchain( - collectedData[CustomTokenFieldId.Network] as Blockchain, - collectedData[CustomTokenFieldId.DerivationPath] as? String, - ) + override fun getConvertedData(): CustomBlockchain { + val network = collectedData[CustomTokenFieldId.Network] as Blockchain + val derivationPath = collectedData[CustomTokenFieldId.DerivationPath] as? String + return CustomBlockchain(network, derivationPath) + } override fun getIdToCollect(): List = listOf(CustomTokenFieldId.Network, CustomTokenFieldId.DerivationPath) } } class CustomToken( - val contractAddress: String, - val selectedNetwork: Blockchain, - val name: String, - val tokenSymbol: String, - val decimals: Int, + val token: Token, + val network: Blockchain, val derivationPath: String?, ) : CompleteData() { - class Converter : BaseFieldDataConverter() { - override fun getConvertedData(): CustomToken = CustomToken( - collectedData[CustomTokenFieldId.ContractAddress] as String, - collectedData[CustomTokenFieldId.Network] as Blockchain, - collectedData[CustomTokenFieldId.Name] as String, - collectedData[CustomTokenFieldId.Symbol] as String, - collectedData[CustomTokenFieldId.Decimals] as Int, - collectedData[CustomTokenFieldId.DerivationPath] as? String, - ) + class Converter(val tokenId: String?) : BaseFieldDataConverter() { + + override fun getConvertedData(): CustomToken { + val token = Token( + name = collectedData[CustomTokenFieldId.Name] as String, + symbol = collectedData[CustomTokenFieldId.Symbol] as String, + contractAddress = collectedData[CustomTokenFieldId.ContractAddress] as String, + decimals = (collectedData[CustomTokenFieldId.Decimals] as String).toInt(), + id = tokenId, + ) + return CustomToken( + token, + collectedData[CustomTokenFieldId.Network] as Blockchain, + collectedData[CustomTokenFieldId.DerivationPath] as? String, + ) + } override fun getIdToCollect(): List = CustomTokenFieldId.values().toList() } 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 index b651dd844b..ebf097d964 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/Errors.kt @@ -13,13 +13,13 @@ sealed class AddCustomTokenError : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add cus object NetworkIsNotSelected : AddCustomTokenError() object InvalidDecimalsCount : AddCustomTokenError() object InvalidDerivationPath : AddCustomTokenError() -} - -sealed class AddCustomTokenWarning : AnError(ERROR_CODE_ADD_CUSTOM_TOKEN, "Add custom token - warning") { - object PotentialScamToken : AddCustomTokenWarning() - object TokenAlreadyAdded : AddCustomTokenWarning() sealed class Network : AddCustomTokenWarning() { object CheckAddressRequestError : Network() } +} + +sealed class AddCustomTokenWarning : AddCustomTokenError() { + object PotentialScamToken : AddCustomTokenWarning() + object TokenAlreadyAdded : AddCustomTokenWarning() } \ 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/TangemTechServiceManager.kt similarity index 98% rename from domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt rename to domain/src/main/java/com/tangem/domain/features/addCustomToken/TangemTechServiceManager.kt index 0b49f500d7..438e75e47a 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenManager.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/TangemTechServiceManager.kt @@ -8,7 +8,7 @@ import com.tangem.network.common.AddHeaderInterceptor /** [REDACTED_AUTHOR] */ -class AddCustomTokenManager( +class TangemTechServiceManager( private val tangemTechService: TangemTechService ) { 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 index 03cb125db1..0b541fb309 100644 --- 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 @@ -6,6 +6,7 @@ import com.tangem.domain.common.form.Field 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.CompleteData import com.tangem.domain.features.addCustomToken.CustomTokenFieldId import com.tangem.network.api.tangemTech.Coins import org.rekotlin.Action @@ -14,6 +15,12 @@ import org.rekotlin.Action [REDACTED_AUTHOR] */ sealed class AddCustomTokenAction : Action { + sealed class Init : AddCustomTokenAction() { + data class SetAddedCurrencies(val addedCurrencies: AddedCurrencies) : AddCustomTokenAction() + + data class SetOnAddTokenCallback(val callback: (CompleteData) -> Unit) : AddCustomTokenAction() + } + object OnCreate : AddCustomTokenAction() { data class SetDerivationStyle(val derivationStyle: DerivationStyle?) : AddCustomTokenAction() } @@ -27,10 +34,12 @@ sealed class AddCustomTokenAction : Action { data class OnTokenSymbolChanged(val tokenSymbol: Field.Data) : AddCustomTokenAction() data class OnTokenDerivationPathChanged(val blockchainDerivationPath: Field.Data) : AddCustomTokenAction() data class OnTokenDecimalsChanged(val tokenDecimals: Field.Data) : AddCustomTokenAction() + object OnAddCustomTokenClicked : AddCustomTokenAction() // form fields data class UpdateForm(val state: AddCustomTokenState) : AddCustomTokenAction() object ClearTokenFields : AddCustomTokenAction() + data class FillTokenFields( val token: Coins.CheckAddressResponse.Token, val contract: Coins.CheckAddressResponse.Token.Contract, @@ -41,6 +50,8 @@ sealed class AddCustomTokenAction : Action { data class Remove(val id: CustomTokenFieldId) : FieldError() } + data class SetTokenId(val id: String) : AddCustomTokenAction() + // warnings sealed class Warning : AddCustomTokenAction() { data class Add(val warnings: Set) : Warning() 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 index ea1a059fe5..3e0ed80316 100644 --- 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 @@ -2,12 +2,14 @@ package com.tangem.domain.features.addCustomToken.redux import android.webkit.ValueCallback import com.tangem.blockchain.common.Blockchain -import com.tangem.common.card.Card +import com.tangem.common.extensions.guard import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.domain.DomainDialog import com.tangem.domain.DomainException import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.form.* import com.tangem.domain.features.addCustomToken.* import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.* @@ -18,7 +20,9 @@ import com.tangem.domain.redux.dispatchOnMain import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.network.api.tangemTech.Coins +import com.tangem.network.api.tangemTech.TangemTechService import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action import timber.log.Timber @@ -31,9 +35,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT private val hubState: AddCustomTokenState get() = domainStore.state.addCustomTokensState - override fun getHubState(storeState: DomainState): AddCustomTokenState { - return storeState.addCustomTokensState - } + override fun getHubState(storeState: DomainState): AddCustomTokenState = hubState override fun updateStoreState(storeState: DomainState, newHubState: AddCustomTokenState): DomainState { return storeState.copy(addCustomTokensState = newHubState) @@ -45,47 +47,51 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT cancel: ValueCallback ) { if (action !is AddCustomTokenAction) return - val card = storeState.globalState.scanResponse?.card - ?: throw IllegalStateException("ScanResponse must be set before showing the AddCustomToken screen") when (action) { + is Init.SetAddedCurrencies -> {} + is Init.SetOnAddTokenCallback -> {} is OnCreate -> { - hubState.addCustomTokenManager.attachAuthKey(card.cardPublicKey.toHexString()) - dispatchOnMain(OnCreate.SetDerivationStyle(card.derivationStyle)) + hubState.addedCurrencies.guard { + return throwUnAppropriateInitialization("addedTokens") + } } is OnDestroy -> hubScope.cancel() is OnTokenContractAddressChanged -> { + dispatchOnMain( + Screen.UpdateAddButton( + ViewStates.AddButton(!hubState.allFieldsIsEmpty()) + ) + ) val contractAddress = action.contractAddress.value - val validator: TokenContractAddressValidator = getValidator(ContractAddress, hubState) + val validator: TokenContractAddressValidator = hubState.getValidator(ContractAddress) val error = validator.validate(contractAddress) addOrRemoveError(ContractAddress, error) if (error != null || contractAddress.isEmpty()) { - dispatchOnMain(actionsUnlockTokenFields()) + dispatchOnMain(unlockTokenFields()) return } if (!action.contractAddress.isUserInput) return - val foundTokens = requestInfoAboutContractAddress(contractAddress, hubState) - manageTokenChanges(null, foundTokens) + manageTokenChanges(requestInfoAboutContractAddress(contractAddress)) } is OnTokenNetworkChanged -> { if (!action.blockchainNetwork.isUserInput) return - val contractAddress = getField(ContractAddress, hubState).data.value - val foundTokens = requestInfoAboutContractAddress(contractAddress, hubState) - manageTokenChanges(null, foundTokens) + val contractAddress = hubState.getField(ContractAddress).data.value + manageTokenChanges(requestInfoAboutContractAddress(contractAddress)) } is OnTokenNameChanged -> { - val validator: TokenNameValidator = getValidator(Name, hubState) + val validator: TokenNameValidator = hubState.getValidator(Name) addOrRemoveError(Name, validator.validate(action.tokenName.value)) } is OnTokenSymbolChanged -> { - val validator: TokenSymbolValidator = getValidator(Symbol, hubState) + val validator: TokenSymbolValidator = hubState.getValidator(Symbol) addOrRemoveError(Symbol, validator.validate(action.tokenSymbol.value)) } is OnTokenDecimalsChanged -> { - val validator: TokenDecimalsValidator = getValidator(Decimals, hubState) + val validator: TokenDecimalsValidator = hubState.getValidator(Decimals) addOrRemoveError(Decimals, validator.validate(action.tokenDecimals.value)) } // is OnTokenDerivationPathChanged -> { @@ -93,9 +99,9 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT // addOrRemoveError(DerivationPath, validator.validate(action.value.value)) // } is ClearTokenFields -> { - val nameField = getField(Name, hubState) - val symbolField = getField(Symbol, hubState) - val decimalsField = getField(Decimals, hubState) + val nameField = hubState.getField(Name) + val symbolField = hubState.getField(Symbol) + val decimalsField = hubState.getField(Decimals) nameField.data = Field.Data("", false) symbolField.data = Field.Data("", false) @@ -104,14 +110,14 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT dispatchOnMain(UpdateForm(hubState)) } is FillTokenFields -> { - val networkField = getField(Network, hubState) - val nameField = getField(Name, hubState) - val symbolField = getField(Symbol, hubState) - val decimalsField = getField(Decimals, hubState) + val networkField = hubState.getField(Network) + val nameField = hubState.getField(Name) + val symbolField = hubState.getField(Symbol) + val decimalsField = hubState.getField(Decimals) val token = action.token val contract = action.contract - val blockchain = Blockchain.fromNetworkId(contract.networkId) + val blockchain = Blockchain.fromNetworkId(contract.networkId) ?: Blockchain.Unknown networkField.data = Field.Data(blockchain, false) nameField.data = Field.Data(token.name, false) symbolField.data = Field.Data(token.symbol, false) @@ -119,23 +125,46 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT dispatchOnMain(UpdateForm(hubState)) } + is OnAddCustomTokenClicked -> { +// if (hubState.allFieldsIsEmpty()) { + dispatchOnMain( + DomainGlobalAction.ShowDialog(DomainDialog.DialogError( + AddCustomTokenError.FieldIsEmpty + ))) + return +// } + when { + !hubState.customTokensFieldsIsEmpty() && !hubState.networkIsEmpty() -> { + hubState.getCompleteData(CompleteDataType.Token) + } +// !hubState.customTokensFieldsIsEmpty() && -> { +// } + } +// if (true) { +// dispatchOnMain(NavigationAction.PopBackTo()) +// hubState.onTokenAddCallback?.invoke() +// } + } else -> {} } } private suspend fun requestInfoAboutContractAddress( contractAddress: String, - hubState: AddCustomTokenState ): List { + val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager) dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true)))) - val tokenManager = hubState.addCustomTokenManager - val field = getField(Network, hubState) + val field = hubState.getField(Network) val selectedNetworkId: String? = field.data.value.let { if (it == Blockchain.Unknown) null else it }?.toNetworkId() -// delay(1000) - val result = when (val foundTokensResult = tokenManager.checkAddress(contractAddress, selectedNetworkId)) { + // simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress() + // got the result faster than 500ms and the delay would only be the difference between them. + delay(500) + + val foundTokensResult = tangemTechServiceManager.checkAddress(contractAddress, selectedNetworkId) + val result = when (foundTokensResult) { is Result.Success -> foundTokensResult.data is Result.Failure -> { // val warning = AddCustomTokenWarning.Network.CheckAddressRequestError @@ -147,10 +176,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT return result } - private suspend fun manageTokenChanges( - card: Card?, - foundTokens: List, - ) { + private suspend fun manageTokenChanges(foundTokens: List) { val toAddWarnings = mutableSetOf() val toRemoveWarnings = mutableSetOf() @@ -159,7 +185,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken) toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) dispatchOnMain(ClearTokenFields) - dispatchOnMain(actionsUnlockTokenFields()) + dispatchOnMain(unlockTokenFields()) } else -> { val token = foundTokens[0] @@ -178,7 +204,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken) dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(false))) - dispatchOnMain(actionsLockTokenFields()) + dispatchOnMain(lockTokenFields()) } else { toRemoveWarnings.add(AddCustomTokenWarning.TokenAlreadyAdded) dispatchOnMain(Screen.UpdateAddButton(ViewStates.AddButton(true))) @@ -188,11 +214,11 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT if (tokenContract.active && isStandardDerivation) { toRemoveWarnings.add(AddCustomTokenWarning.PotentialScamToken) dispatchOnMain(FillTokenFields(token, contract)) - dispatchOnMain(actionsLockTokenFields()) + dispatchOnMain(lockTokenFields()) } else { toAddWarnings.add(AddCustomTokenWarning.PotentialScamToken) dispatchOnMain(ClearTokenFields) - dispatchOnMain(actionsUnlockTokenFields()) + dispatchOnMain(unlockTokenFields()) } } } @@ -201,7 +227,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT items = contracts, networkIdConverter = { networkId -> val blockchain = Blockchain.fromNetworkId(networkId) - if (blockchain == Blockchain.Unknown) { + if (blockchain == null || blockchain == Blockchain.Unknown) { throw DomainException.SelectTokeNetworkException(networkId) } hubState.convertBlockchainName(blockchain, "") @@ -210,7 +236,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT hubScope.launch { // find how to connect to the upper coroutineContext and dispatch through them dispatchOnMain(FillTokenFields(token, selectedContract)) - dispatchOnMain(actionsLockTokenFields()) + dispatchOnMain(lockTokenFields()) } }, ) @@ -238,7 +264,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT } } - private fun actionsLockTokenFields(): Action { + private fun lockTokenFields(): Action { val state = hubState return Screen.UpdateTokenFields(listOf( Network to state.screenState.network.copy(isEnabled = false), @@ -248,7 +274,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT )) } - private fun actionsUnlockTokenFields(): Action { + private fun unlockTokenFields(): Action { val state = hubState return Screen.UpdateTokenFields(listOf( Network to state.screenState.network.copy(isEnabled = true), @@ -258,46 +284,54 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT )) } - private inline fun getField(id: FieldId, state: AddCustomTokenState): T { - return state.form.getField(id) as T - } - - private inline fun getValidator(id: FieldId, state: AddCustomTokenState): T { - return state.getValidator(id) as T - } - override fun reduceAction(action: Action, state: AddCustomTokenState): AddCustomTokenState { return when (action) { + is Init.SetAddedCurrencies -> { + state.copy(addedCurrencies = action.addedCurrencies) + } + is Init.SetOnAddTokenCallback -> { + state.copy(onTokenAddCallback = action.callback) + } + is OnCreate -> { + val card = requireNotNull(globalState.scanResponse?.card) + val tangemTechServiceManager = TangemTechServiceManager(TangemTechService()) + tangemTechServiceManager.attachAuthKey(card.cardPublicKey.toHexString()) + state.copy( + derivationStyle = card.derivationStyle, + tangemTechServiceManager = tangemTechServiceManager + ) + } + is OnDestroy -> state.reset() is UpdateForm -> { updateFormState(action.state) } is OnTokenContractAddressChanged -> { - val field: TokenField = getField(ContractAddress, state) + val field: TokenField = state.getField(ContractAddress) field.data = action.contractAddress updateFormState(state) } is OnTokenNetworkChanged -> { - val field: TokenBlockchainField = getField(Network, state) + val field: TokenBlockchainField = state.getField(Network) field.data = action.blockchainNetwork updateFormState(state) } is OnTokenNameChanged -> { - val field: TokenField = getField(Name, state) + val field: TokenField = state.getField(Name) field.data = action.tokenName updateFormState(state) } is OnTokenSymbolChanged -> { - val field: TokenField = getField(Symbol, state) + val field: TokenField = state.getField(Symbol) field.data = action.tokenSymbol updateFormState(state) } is OnTokenDecimalsChanged -> { - val field: TokenField = getField(Decimals, state) + val field: TokenField = state.getField(Decimals) field.data = action.tokenDecimals updateFormState(state) } is OnTokenDerivationPathChanged -> { - val field: TokenDerivationPathField = getField(DerivationPath, state) + val field: TokenDerivationPathField = state.getField(DerivationPath) field.data = action.blockchainDerivationPath updateFormState(state) } @@ -309,6 +343,9 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT val newMap = state.formErrors.toMutableMap().apply { remove(action.id) } state.copy(formErrors = newMap) } + is SetTokenId -> { + state.copy(tokenId = action.id) + } is Warning.Add -> { val newList = state.warnings.toMutableSet().apply { addAll(action.warnings) } state.copy(warnings = newList.toSet()) @@ -391,62 +428,11 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT private fun updateFormState(state: AddCustomTokenState): AddCustomTokenState { return state.copy(form = Form(state.form.fieldList)) } -} -//TODO: refactoring: replace by Blockchain.Companion.fromNetworkId -fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain { - return when (networkId) { - "avalanche" -> Blockchain.Avalanche - "binancecoin" -> Blockchain.Binance - "binance-smart-chain" -> Blockchain.BSC - "ethereum" -> Blockchain.Ethereum - "polygon-pos" -> Blockchain.Polygon - "solana" -> Blockchain.Solana - "fantom" -> Blockchain.Fantom - "bitcoin" -> Blockchain.Bitcoin - "bitcoin-cash" -> Blockchain.BitcoinCash - "cardano" -> Blockchain.CardanoShelley - "dogecoin" -> Blockchain.Dogecoin - "ducatus" -> Blockchain.Ducatus - "litecoin" -> Blockchain.Litecoin - "rsk" -> Blockchain.RSK - "stellar" -> Blockchain.Stellar - "tezos" -> Blockchain.Tezos - "ripple" -> Blockchain.XRP - else -> Blockchain.Unknown - } -} - -fun Blockchain.toNetworkId(): String? { - return when (this) { - Blockchain.Unknown -> null - Blockchain.Avalanche -> "avalanche" - Blockchain.AvalancheTestnet -> "avalanche" - Blockchain.Binance -> "binancecoin" - Blockchain.BinanceTestnet -> "binancecoin" - Blockchain.BSC -> "binance-smart-chain" - Blockchain.BSCTestnet -> "binance-smart-chain" - Blockchain.Bitcoin -> "bitcoin" - Blockchain.BitcoinTestnet -> "bitcoin" - Blockchain.BitcoinCash -> "bitcoin-cash" - Blockchain.BitcoinCashTestnet -> "bitcoin-cash" - Blockchain.Cardano -> "cardano" - Blockchain.CardanoShelley -> "cardano" - Blockchain.Dogecoin -> "dogecoin" - Blockchain.Ducatus -> "ducatus" - Blockchain.Ethereum -> "ethereum" - Blockchain.EthereumTestnet -> "ethereum" - Blockchain.Fantom -> "fantom" - Blockchain.FantomTestnet -> "fantom" - Blockchain.Litecoin -> "litecoin" - Blockchain.Polygon -> "matic-network" - Blockchain.PolygonTestnet -> "matic-networks" - Blockchain.RSK -> "rootstock" - Blockchain.Stellar -> "stellar" - Blockchain.StellarTestnet -> "stellar" - Blockchain.Solana -> "solana" - Blockchain.SolanaTestnet -> "solana" - Blockchain.Tezos -> "tezos" - Blockchain.XRP -> "ripple" + @Throws + private fun throwUnAppropriateInitialization(objName: String) { + throw DomainException.UnAppropriateInitializationException( + "AddCustomTokenHub", "$objName must be not NULL" + ) } } \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index cfcdf6da1b..13acabb71e 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -5,35 +5,39 @@ import com.tangem.blockchain.common.DerivationStyle 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 AddCustomTokenState( + val addedCurrencies: AddedCurrencies? = null, + val onTokenAddCallback: ((CompleteData) -> Unit)? = null, + val derivationStyle: DerivationStyle? = null, val form: Form = Form(createFormFields()), - val formValidators: Map> = createFormValidators(), + val formValidators: Map> = createFormValidators(), val formErrors: Map = emptyMap(), + val tokenId: String? = null, val warnings: Set = emptySet(), val screenState: ScreenState = createInitialScreenState(), - val addCustomTokenManager: AddCustomTokenManager = AddCustomTokenManager(TangemTechService()), - val derivationStyle: DerivationStyle? = null + val tangemTechServiceManager: TangemTechServiceManager? = null ) : StateType { - val completeDataType: CompleteDataType - get() = calculateDataType() + inline fun getField(id: FieldId): T = form.getField(id) as T + + inline fun getValidator(id: FieldId): T = formValidators[id] as T + + fun getError(id: FieldId): AddCustomTokenError? = formErrors[id] + + fun hasError(id: FieldId): Boolean = formErrors[id] != null + + fun getCompleteData(type: CompleteDataType): CompleteData = when (type) { + CompleteDataType.Token -> getToken() + CompleteDataType.Blockchain -> getBlockchain() + } inline fun visitDataConverter(converter: FieldDataConverter): T { form.visitDataConverter(converter) return converter.getConvertedData() } - fun getValidator(id: FieldId): CustomTokenValidator<*> = formValidators[id]!! - - fun hasError(id: FieldId): Boolean = formErrors[id] != null - - fun getError(id: FieldId): AddCustomTokenError? { - return formErrors[id] - } - fun convertBlockchainName(blockchain: Blockchain, unknown: String): String = when (blockchain) { Blockchain.Unknown -> unknown else -> blockchain.fullName @@ -43,18 +47,49 @@ data class AddCustomTokenState( return blockchain.derivationPath(derivationStyle)?.rawPath ?: unknown } - private fun calculateDataType(): CompleteDataType { + fun reset(): AddCustomTokenState { + return this.copy( + addedCurrencies = null, + onTokenAddCallback = null, + derivationStyle = null, + form = Form(createFormFields()), + formErrors = emptyMap(), + tokenId = null, + warnings = emptySet(), + screenState = createInitialScreenState(), + tangemTechServiceManager = null, + ) + } + + fun networkIsEmpty(): Boolean { + val network = getField(Network) + return network.data.value != Blockchain.Unknown + } + + fun customTokensFieldsIsEmpty(): Boolean { 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 + val validator = StringIsEmptyValidator() +// val errors = mutableMapOf<>() + fieldsToCheck.forEach { field -> + val error = validator.validate(field.data.value?.toString()) + if (error != null) return true } + return false + } - return CompleteDataType.Blockchain + fun allFieldsIsEmpty(): Boolean = networkIsEmpty() && customTokensFieldsIsEmpty() + + private fun getToken(): CompleteData.CustomToken { + return CompleteData.CustomToken.Converter(tokenId) + .apply { visitDataConverter(this) } + .getConvertedData() + } + + private fun getBlockchain(): CompleteData.CustomBlockchain { + return CompleteData.CustomBlockchain.Converter() + .apply { visitDataConverter(this) } + .getConvertedData() } companion object { @@ -69,7 +104,7 @@ data class AddCustomTokenState( ) } - private fun createFormValidators(): Map> { + private fun createFormValidators(): Map> { return mapOf( ContractAddress to TokenContractAddressValidator(), Network to TokenNetworkValidator(), diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt index 3dba21a577..1d9bc38a7a 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt @@ -1,5 +1,8 @@ package com.tangem.domain.features.addCustomToken.redux +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.DomainWrapped + /** [REDACTED_AUTHOR] */ @@ -24,4 +27,9 @@ sealed class ViewStates { data class AddButton( val isEnabled: Boolean = true ) : ViewStates() -} \ No newline at end of file +} + +data class AddedCurrencies( + val addedTokens: List, + val addedBlockchains: List +) \ No newline at end of file diff --git a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt index 183599aa4a..9de3cedcf3 100644 --- a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt +++ b/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt @@ -4,6 +4,7 @@ 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 com.tangem.domain.redux.global.DomainGlobalState import kotlinx.coroutines.* import org.rekotlin.Action import org.rekotlin.DispatchFunction @@ -32,14 +33,20 @@ internal interface HubReducer { * All action went from the middleware must be dispatched through ReStoreHub.dispatchOnMain(Actions) to prevent * concurrent modification in the Store * Only the changed hub State will change its state in the DomainState + * Do not implement other states like as DomainGlobalState. Because it can dilute the responsibility of + * states. * @param name - name of the Hub * @param dispatcher - main coroutine dispatcher for actions + * @property globalState - state witch produce accessibility to global variables */ internal abstract class BaseStoreHub( private val name: String, private val dispatcher: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() ) : ReStoreHub { + val globalState: DomainGlobalState + get() = domainStore.state.globalState + val hubScope = CoroutineScope( Job() + dispatcher + CoroutineName(name) + FeatureCoroutineExceptionHandler.create(name) ) From c7720e2ff47aa333496f50f5f2aa856443c18f83 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Apr 2022 17:36:28 +0300 Subject: [PATCH 28/28] Updated on 2026-08-14 --- .../java/com/tangem/tap/features/tokens/redux/TokensAction.kt | 2 +- .../java/com/tangem/tap/features/tokens/redux/TokensState.kt | 2 +- .../tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt index d065b94ea5..e95dc24e86 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.tokens.redux import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle -import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.domain.common.ScanResponse import com.tangem.tap.domain.tokens.Currency import com.tangem.tap.features.wallet.redux.WalletData import org.rekotlin.Action diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt index 265d8ab3a9..7fc19bf751 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.tokens.redux import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.Token +import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.tokens.Currency import com.tangem.tap.features.wallet.redux.WalletData import org.rekotlin.StateType diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt index 9820b0d20d..983f0aaeed 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt @@ -17,11 +17,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.tap.common.compose.Keyboard import com.tangem.tap.common.compose.keyboardAsState import com.tangem.tap.common.extensions.pixelsToDp -import com.tangem.tap.domain.TapWorkarounds.useOldStyleDerivation import com.tangem.tap.domain.tokens.Currency import com.tangem.tap.features.tokens.redux.ContractAddress import com.tangem.tap.features.tokens.redux.TokenWithBlockchain