diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df6d24b384..8b1db8e62c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,11 +25,13 @@ dependencies { implementation(project(":domain:legacy")) implementation(project(":domain:models")) implementation(project(":domain:core")) - implementation(project(":domain:card")) + implementation(projects.domain.card) implementation(project(":domain:wallets")) implementation(project(":domain:wallets:models")) + implementation(projects.domain.tokens) implementation(project(":common")) implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:navigation")) implementation(project(":core:featuretoggles")) implementation(project(":core:res")) @@ -39,6 +41,9 @@ dependencies { implementation(project(":libs:crypto")) implementation(project(":libs:auth")) implementation(project(":data:source:preferences")) + implementation(projects.data.card) + implementation(projects.data.tokens) + implementation(projects.data.common) /** Features */ implementation(project(":features:onboarding")) @@ -119,13 +124,6 @@ dependencies { implementation(deps.appsflyer) implementation(deps.amplitude) implementation(deps.kotsonGson) - //TODO: refactoring: remove it when all network services moved to the datasource module - implementation(deps.retrofit) - implementation(deps.retrofit.moshi) - implementation(deps.moshi) - implementation(deps.moshi.kotlin) - implementation(deps.okHttp) - implementation(deps.okHttp.logging) implementation(deps.zendesk.chat) implementation(deps.zendesk.messaging) implementation(deps.spongecastle.core) @@ -144,6 +142,7 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.walletConnectCore) implementation(deps.walletConnectWeb3) + implementation(deps.prettyLogger) /** Testing libraries */ testImplementation(deps.test.junit) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 186fe79feb..0d64c958e4 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -10,10 +10,11 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsControllerCompat import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.snackbar.Snackbar -import com.tangem.TangemSdk import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.data.card.sdk.CardSdkLifecycleObserver import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -56,7 +57,6 @@ import java.lang.ref.WeakReference import javax.inject.Inject import kotlin.coroutines.CoroutineContext -lateinit var tangemSdk: TangemSdk lateinit var tangemSdkManager: TangemSdkManager lateinit var backupService: BackupService internal var lockUserWalletsTimer: LockUserWalletsTimer? = null @@ -88,7 +88,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lateinit var testerRouter: TesterRouter @Inject - lateinit var injectedTangemSdk: TangemSdk + lateinit var cardSdkLifecycleObserver: CardSdkLifecycleObserver + + @Inject + lateinit var cardSdkConfigRepository: CardSdkConfigRepository @Inject lateinit var injectedTangemSdkManager: TangemSdkManager @@ -122,11 +125,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac systemActions() store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) - tangemSdk = injectedTangemSdk + cardSdkLifecycleObserver.onCreate(context = this) + tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager - appStateHolder.tangemSdk = tangemSdk - backupService = BackupService.init(tangemSdk, this) + backupService = BackupService.init(cardSdkConfigRepository.sdk, this) lockUserWalletsTimer = LockUserWalletsTimer(owner = this) initUserWalletsListManager() @@ -144,10 +147,37 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac walletRouter = walletRouter, walletConnectInteractor = walletConnectInteractor, tokenDetailsRouter = tokenDetailsRouter, + cardSdkConfigRepository = cardSdkConfigRepository, ), ) } + override fun onStart() { + super.onStart() + dialogManager.onStart(this) + } + + override fun onResume() { + super.onResume() + // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop + notificationsHandler = NotificationsHandler(binding.fragmentContainer) + + navigateToInitialScreenIfNeededOnResume(intent) + } + + override fun onStop() { + notificationsHandler = null + dialogManager.onStop() + super.onStop() + } + + override fun onDestroy() { + store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this))) + intentProcessor.removeAll() + cardSdkLifecycleObserver.onDestroy() + super.onDestroy() + } + private fun initIntentHandlers() { val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true } intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider)) @@ -183,13 +213,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } - override fun onResume() { - super.onResume() - notificationsHandler = NotificationsHandler(binding.fragmentContainer) - - navigateToInitialScreenIfNeededOnResume(intent) - } - override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) scope.launch { @@ -197,23 +220,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - override fun onStart() { - super.onStart() - dialogManager.onStart(this) - } - - override fun onStop() { - notificationsHandler = null - dialogManager.onStop() - super.onStop() - } - - override fun onDestroy() { - store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this))) - intentProcessor.removeAll() - super.onDestroy() - } - override fun showSnackbar(text: Int, buttonTitle: Int?, action: View.OnClickListener?) { if (snackbar != null) return diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 63dfcc0729..00e2ae0a86 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -5,6 +5,8 @@ import android.content.Context import android.content.pm.PackageManager import coil.ImageLoader import coil.ImageLoaderFactory +import com.orhanobut.logger.AndroidLogAdapter +import com.orhanobut.logger.Logger import com.tangem.Log import com.tangem.LogFormat import com.tangem.blockchain.common.BlockchainSdkConfig @@ -14,12 +16,14 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.models.Config import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.DomainLayer +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.LogConfig import com.tangem.domain.wallets.legacy.WalletManagersRepository import com.tangem.feature.learn2earn.domain.api.Learn2earnInteractor @@ -59,7 +63,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.BuildConfig import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.runBlocking -import okhttp3.logging.HttpLoggingInterceptor import org.rekotlin.Store import timber.log.Timber import javax.inject.Inject @@ -151,6 +154,9 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var tokenDetailsFeatureToggles: TokenDetailsFeatureToggles + @Inject + lateinit var scanCardProcessor: ScanCardProcessor + override fun onCreate() { super.onCreate() @@ -168,12 +174,20 @@ class TapApplication : Application(), ImageLoaderFactory { walletConnectRepository = walletConnect2Repository, walletConnectSessionsRepository = walletConnectSessionsRepository, tokenDetailsFeatureToggles = tokenDetailsFeatureToggles, + scanCardProcessor = scanCardProcessor, ), ), ) if (BuildConfig.DEBUG) { - Timber.plant(Timber.DebugTree()) + Logger.addLogAdapter(AndroidLogAdapter()) + Timber.plant( + object : Timber.DebugTree() { + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + Logger.log(priority, tag, message, t) + } + }, + ) } foregroundActivityObserver = ForegroundActivityObserver() @@ -192,7 +206,7 @@ class TapApplication : Application(), ImageLoaderFactory { if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( - HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }, + createNetworkLoggingInterceptor(), ) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt index e5b89b30e5..2782ceac8f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Basic.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt index 96fa307105..f4b9dbc61f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Chat.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt index a0d34306c9..775f44a0d8 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/DetailsScreen.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt index 602365c0c6..b5ce0042ae 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/IntroductionProcess.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt index 7a36f35d1e..fbbc88c099 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] @@ -13,7 +13,6 @@ sealed class MainScreen( class ScreenOpened : MainScreen("Screen opened") class ButtonScanCard : MainScreen("Button - Scan Card") class ButtonMyWallets : MainScreen("Button - My Wallets") - class ButtonBuy : MainScreen("Button - Buy") class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen( event = "Enable Biometric", diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt index 8e0b8cc5b9..969d1d47fb 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.tap.common.extensions.filterNotNull diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt index 65ab7937c2..87ba002047 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/MyWallets.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent sealed class MyWallets( event: String, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt index 98dd408ba8..9612f2b972 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Onboarding.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] @@ -72,26 +72,8 @@ sealed class Onboarding( class SetupFinished : Twins("Twin Setup Finished") } - class PinScreenOpened : Onboarding("Onboarding", "PIN screen opened") - class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code") - class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened") - class ButtonConnect : Onboarding("Onboarding", "Button - Connect") - class PinCodeSet : Onboarding("Onboarding", "PIN Code Set") - - class KYCScreenOpened : Onboarding("Onboarding", "KYC screen opened") - class KYCStarted : Onboarding("Onboarding", "KYC Started") - class KYCInProgress : Onboarding("Onboarding", "KYC In Progress") - class KYCRejected : Onboarding("Onboarding", "KYC Rejected") - - class ClaimScreenOpened : Onboarding("Onboarding", "Claim Screen Opened") - class ButtonClaim : Onboarding("Onboarding", "Button - Claim") - class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim Was Successfully") - class ButtonChat : Onboarding("Onboarding", "Button - Chat") - class NotEnoughGasError : Onboarding("Onboarding", "Not Enough Gas Error") - class CardNotPassedError : Onboarding("Onboarding", "Card Not Passed Error") - class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding( category = "Onboarding / Biometric", event = "Enable Biometric", diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt index 71383c9f9a..3e961883b7 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Portfolio.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt index 5a9226bec4..e71eda15ad 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.features.details.ui.details.SocialNetwork /** diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt index 0ab4dd9361..4076de8685 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Shop.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.extensions.filterNotNull /** diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt index a5417d775b..de174fd193 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/SignIn.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index 837c9bce3c..9b8cecc417 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.events.AnalyticsParam.CurrencyType /** diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt index 34047928e4..49c49b912a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/WalletConnect.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.events -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt index d1b3189ab1..c4b12fc278 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsFlyer/AppsFlyerAnalyticsHandler.kt @@ -1,8 +1,8 @@ package com.tangem.tap.common.analytics.handlers.appsFlyer import com.appsflyer.AFInAppEventType -import com.tangem.core.analytics.AnalyticsEvent import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.events.Shop diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index 7430fcc17f..e445597e8f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -1,9 +1,9 @@ package com.tangem.tap.common.analytics.handlers.firebase import com.google.firebase.analytics.FirebaseAnalytics -import com.tangem.core.analytics.AnalyticsEvent import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.ErrorEventHandler +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter import com.tangem.tap.common.analytics.events.Shop diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 864c9dadbb..0db51098a3 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.analytics.paramsInterceptor -import com.tangem.core.analytics.AnalyticsEvent import com.tangem.core.analytics.api.ParamsInterceptor +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt index 94c006d699..25b67c3ee0 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt @@ -1,14 +1,14 @@ package com.tangem.tap.common.analytics.paramsInterceptor -import com.tangem.core.analytics.AnalyticsEvent import com.tangem.core.analytics.api.ParamsInterceptor +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.models.scan.ScanResponse /** [REDACTED_AUTHOR] */ class LinkedCardContextInterceptor( - private val scanResponse: ScanResponse, + scanResponse: ScanResponse, val parent: LinkedCardContextInterceptor? = null, ) : ParamsInterceptor { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt index bdd315bc3d..ba23fa52dd 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt @@ -9,6 +9,7 @@ import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.analytics.converters.TopUpEventConverter @@ -17,7 +18,6 @@ import com.tangem.tap.common.extensions.copy import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager import com.tangem.tap.domain.walletStores.WalletStoresManager import com.tangem.tap.features.wallet.models.Currency diff --git a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt index 182aa7350b..56ded693ad 100644 --- a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt @@ -12,6 +12,7 @@ import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener import com.tangem.tap.common.redux.AppState import org.rekotlin.Store +import java.io.File class ChatManager( private val preferencesStorage: PreferencesDataSource, @@ -20,7 +21,7 @@ class ChatManager( ) { private val openers = mutableMapOf() - fun open(config: ChatConfig, feedbackDataBuilder: (Context) -> String) { + fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) { val opener = openers.getOrPut(config) { when (config) { is SprinklrConfig -> SprinklrChatOpener(getSprinklrUserId(), config, store, foregroundActivityObserver) @@ -28,7 +29,10 @@ class ChatManager( } } - opener.open(feedbackDataBuilder) + opener.open( + createFeedbackFile = createFeedbackFile, + createLogsFile = createLogsFile, + ) } private fun getZendeskUserId(): String { diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt index e3757286e7..aff21cad05 100644 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt @@ -1,7 +1,8 @@ package com.tangem.tap.common.chat.opener import android.content.Context +import java.io.File internal interface ChatOpener { - fun open(feedbackDataBuilder: (Context) -> String) + fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt index c9b8fdb4f9..f8936d6abd 100644 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt @@ -2,14 +2,15 @@ package com.tangem.tap.common.chat.opener.implementation import android.content.Context import android.content.Intent +import com.tangem.datasource.config.models.SprinklrConfig import com.tangem.tap.ForegroundActivityObserver import com.tangem.tap.common.chat.opener.ChatOpener -import com.tangem.datasource.config.models.SprinklrConfig import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.sprinklr.redux.SprinklrAction import com.tangem.tap.features.sprinklr.ui.SprinklrActivity import com.tangem.tap.withForegroundActivity import org.rekotlin.Store +import java.io.File internal class SprinklrChatOpener( private val userId: String, @@ -17,7 +18,7 @@ internal class SprinklrChatOpener( private val store: Store, private val foregroundActivityObserver: ForegroundActivityObserver, ) : ChatOpener { - override fun open(feedbackDataBuilder: (Context) -> String) { + override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) { store.dispatch(SprinklrAction.Init(userId, config)) foregroundActivityObserver.withForegroundActivity { activity -> val intent = Intent(activity, SprinklrActivity::class.java) diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt index 0e5d38b0da..1c6198806c 100644 --- a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/ZendeskChatOpener.kt @@ -2,34 +2,34 @@ package com.tangem.tap.common.chat.opener.implementation import android.content.Context import com.tangem.core.analytics.Analytics +import com.tangem.datasource.config.models.ZendeskConfig import com.tangem.domain.common.LogConfig import com.tangem.tap.ForegroundActivityObserver import com.tangem.tap.common.chat.opener.ChatOpener -import com.tangem.datasource.config.models.ZendeskConfig import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R import com.zendesk.logger.Logger -import zendesk.chat.Chat -import zendesk.chat.ChatConfiguration -import zendesk.chat.ChatEngine -import zendesk.chat.ChatProvidersConfiguration -import zendesk.chat.VisitorInfo +import timber.log.Timber +import zendesk.chat.* import zendesk.configurations.Configuration import zendesk.messaging.MessagingActivity +import java.io.File internal class ZendeskChatOpener( private val userId: String, private val config: ZendeskConfig, private val foregroundActivityObserver: ForegroundActivityObserver, ) : ChatOpener { + private var isInitialized = false - override fun open(feedbackDataBuilder: (Context) -> String) { + override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) { foregroundActivityObserver.withForegroundActivity { activity -> initZendeskIfNeeded(activity.applicationContext) setChatVisitorInfo() - setChatVisitorNote(feedbackDataBuilder(activity)) showMessagingActivity(activity) + sendFeedbackFile(createFeedbackFile(activity)) + sendLogsFile(createLogsFile(activity)) } } @@ -48,8 +48,20 @@ internal class ZendeskChatOpener( ChatProvidersConfiguration.builder().withVisitorInfo(visitorInfo).build() } - private fun setChatVisitorNote(note: String) { - Chat.INSTANCE.providers()?.profileProvider()?.setVisitorNote(note) + private fun sendFeedbackFile(feedbackFile: File?) { + if (isInitialized && feedbackFile != null) { + Chat.INSTANCE.providers()?.chatProvider()?.sendFile(feedbackFile) { _, bytesUploaded, _ -> + Timber.d("Log file sent", "bytesUploaded: $bytesUploaded") + } + } + } + + private fun sendLogsFile(logsFile: File?) { + if (isInitialized && logsFile != null) { + Chat.INSTANCE.providers()?.chatProvider()?.sendFile(logsFile) { _, bytesUploaded, _ -> + Timber.d("Log file sent", "bytesUploaded: $bytesUploaded") + } + } } private fun showMessagingActivity(context: Context) { diff --git a/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt index f945462b83..bcf08adf87 100644 --- a/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.di.domain.wallets import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,4 +18,10 @@ object WalletsDomainModule { fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase { return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) } + + @Provides + @ViewModelScoped + fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase { + return SaveWalletUseCase(walletsStateHolder = walletsStateHolder) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index bea9728880..5f259977e0 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -24,13 +24,17 @@ fun BigDecimal.toFormattedString( df.decimalFormatSymbols = symbols df.maximumFractionDigits = decimals df.minimumFractionDigits = 0 - df.isGroupingUsed = false + df.isGroupingUsed = true df.roundingMode = roundingMode return df.format(this) } +/** + * To formatted crypto currency string + * Specific method because there is no crypto currency codes in Locale + */ @Suppress("MagicNumber") -fun BigDecimal.toFormattedCurrencyString( +fun BigDecimal.toFormattedCryptoCurrencyString( decimals: Int, currency: String, roundingMode: RoundingMode = RoundingMode.DOWN, @@ -41,16 +45,34 @@ fun BigDecimal.toFormattedCurrencyString( } else { decimals } + try { + val locale = Locale.getDefault() + val formatter = NumberFormat.getCurrencyInstance(locale) + // first create currency instance for "USD" with Locale default to replace currency to crypto later + Currency.getInstance("USD")?.let { currencyTmp -> + formatter.currency = currencyTmp + formatter.maximumFractionDigits = decimalsForRounding + formatter.minimumFractionDigits = 0 + formatter.isGroupingUsed = true + formatter.roundingMode = roundingMode + // cause formatter created for USD, replace Currency with Crypto symbol on right by Locale place + return formatter.format(this).replace(currencyTmp.getSymbol(locale), "$currency ") + } + } catch (e: IllegalArgumentException) { + Timber.e(e, "can't parse currency") + } + // if something went wrong - use old way to format val formattedAmount = this.toFormattedString( decimals = decimalsForRounding, roundingMode = roundingMode, + locale = Locale.getDefault(), ) - return "$formattedAmount $currency" + return "$formattedAmount $currency " } fun BigDecimal.toFiatRateString(fiatCurrencyName: String, fiatCode: String): String { try { - val formatter = NumberFormat.getCurrencyInstance() + val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault()) Currency.getInstance(fiatCode)?.let { currency -> formatter.currency = currency formatter.maximumFractionDigits = 2 @@ -91,7 +113,7 @@ fun BigDecimal.toFormattedFiatValue( formatWithSpaces: Boolean = false, ): String { try { - val formatter = NumberFormat.getCurrencyInstance() + val formatter = NumberFormat.getCurrencyInstance(Locale.getDefault()) Currency.getInstance(fiatCode)?.let { currency -> formatter.currency = currency formatter.maximumFractionDigits = 2 diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt index 7d138f1610..947691b26f 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -1,17 +1,12 @@ package com.tangem.tap.common.feedback import android.os.Build -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.common.extensions.stripZeroPlainString -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder class AdditionalFeedbackInfo { class EmailWalletInfo( diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt index 6fddf99607..c1e9ae43b1 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -1,9 +1,9 @@ package com.tangem.tap.common.feedback import android.content.Context +import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.common.TapWorkarounds import com.tangem.tap.common.chat.ChatManager -import com.tangem.datasource.config.models.ChatConfig import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.foregroundActivityObserver @@ -22,32 +22,61 @@ class FeedbackManager( private val chatManager: ChatManager, ) { + private var sessionFeedbackFile: File? = null + private var sessionLogsFile: File? = null + fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { feedbackData.prepare(infoHolder) foregroundActivityObserver.withForegroundActivity { activity -> - val fileLog = createLogFile(activity) activity.sendEmail( email = getSupportEmail(), subject = activity.getString(feedbackData.subjectResId), message = feedbackData.joinTogether(activity, infoHolder), - file = fileLog, + file = getLogFile(activity), onFail = onFail, ) } } fun openChat(config: ChatConfig, feedbackData: FeedbackData) { - chatManager.open(config) { context -> - feedbackData.run { + chatManager.open( + config = config, + createLogsFile = ::getLogFile, + createFeedbackFile = { context -> getFeedbackFile(context, feedbackData) }, + ) + } + + private fun getFeedbackFile(context: Context, feedbackData: FeedbackData): File? { + return try { + if (sessionFeedbackFile != null) { + return sessionFeedbackFile + } + val file = File(context.filesDir, FEEDBACK_FILE) + file.delete() + file.createNewFile() + + val feedback = feedbackData.run { prepare(infoHolder) joinTogether(context, infoHolder) } + val fileWriter = FileWriter(file) + fileWriter.write(feedback) + fileWriter.close() + + sessionFeedbackFile = file + sessionFeedbackFile + } catch (ex: Exception) { + Timber.e(ex, "Can't create the logs file") + null } } - private fun createLogFile(context: Context): File? { + private fun getLogFile(context: Context): File? { return try { - val file = File(context.filesDir, "logs.txt") + if (sessionLogsFile != null) { + return sessionLogsFile + } + val file = File(context.filesDir, LOGS_FILE) file.delete() file.createNewFile() @@ -57,7 +86,8 @@ class FeedbackManager( fileWriter.write(stringWriter.toString()) fileWriter.close() logCollector.clearLogs() - file + sessionLogsFile = file + sessionLogsFile } catch (ex: Exception) { Timber.e(ex, "Can't create the logs file") null @@ -75,5 +105,7 @@ class FeedbackManager( companion object { const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com" const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com" + const val FEEDBACK_FILE = "feedback.txt" + const val LOGS_FILE = "logs.txt" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/images/Coil.kt b/app/src/main/java/com/tangem/tap/common/images/Coil.kt index b7305e86dc..0d6d4de675 100644 --- a/app/src/main/java/com/tangem/tap/common/images/Coil.kt +++ b/app/src/main/java/com/tangem/tap/common/images/Coil.kt @@ -4,8 +4,8 @@ import android.content.Context import android.util.Log import coil.ImageLoader import coil.util.Logger +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import timber.log.Timber private const val COIL_LOG_TAG = "COIL" @@ -18,14 +18,7 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL logger(CoilTimberLogger()) okHttpClient { OkHttpClient.Builder() - .addNetworkInterceptor( - HttpLoggingInterceptor { message -> - Timber.tag(COIL_LOG_TAG).d(message) - } - .apply { - level = HttpLoggingInterceptor.Level.BODY - }, - ) + .addNetworkInterceptor(createNetworkLoggingInterceptor()) .build() } } diff --git a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt index f132cd319b..be7a6e4330 100644 --- a/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt +++ b/app/src/main/java/com/tangem/tap/common/qrCodeScan/ScanQrCodeActivity.kt @@ -8,6 +8,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import com.google.zxing.BarcodeFormat import com.google.zxing.Result import com.otaliastudios.cameraview.CameraView.PERMISSION_REQUEST_CODE import me.dm7.barcodescanner.zxing.ZXingScannerView @@ -16,11 +17,15 @@ import me.dm7.barcodescanner.zxing.ZXingScannerView [REDACTED_AUTHOR] */ class ScanQrCodeActivity : AppCompatActivity(), ZXingScannerView.ResultHandler { + private lateinit var mScannerView: ZXingScannerView override fun onCreate(state: Bundle?) { super.onCreate(state) - mScannerView = ZXingScannerView(this) + + mScannerView = ZXingScannerView(this).apply { + setFormats(listOf(BarcodeFormat.QR_CODE)) + } setContentView(mScannerView) if (!permissionIsGranted()) requestPermission() diff --git a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt index a96f1c1e06..c385b121ae 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt @@ -3,7 +3,8 @@ package com.tangem.tap.common.redux import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.preferencesStorage -import com.tangem.tap.tangemSdkManager +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.store import org.rekotlin.Middleware class AccessCodeRequestPolicyMiddleware { @@ -19,9 +20,8 @@ class AccessCodeRequestPolicyMiddleware { } private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) { - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && - scanResponse.card.isAccessCodeSet, + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 96639f64e4..a894d7c4fe 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,42 +1,43 @@ package com.tangem.tap.di import android.content.Context -import androidx.fragment.app.FragmentActivity -import com.tangem.TangemSdk import com.tangem.domain.card.ScanCardUseCase -import com.tangem.sdk.extensions.initWithBiometrics +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository import com.tangem.tap.userTokensRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.qualifiers.ActivityContext -import dagger.hilt.android.scopes.ActivityScoped +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton @Module -@InstallIn(ActivityComponent::class) +@InstallIn(SingletonComponent::class) internal object ActivityModule { @Provides - @ActivityScoped - fun provideTangemSdk(@ActivityContext context: Context): TangemSdk { - return TangemSdk.initWithBiometrics(context as FragmentActivity, TangemSdkManager.config) + @Singleton + fun provideTangemSdkManager( + @ApplicationContext context: Context, + cardSdkConfigRepository: CardSdkConfigRepository, + ): TangemSdkManager { + return TangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources) } @Provides - @ActivityScoped - fun provideTangemSdkManager(@ActivityContext context: Context, tangemSdk: TangemSdk): TangemSdkManager { - return TangemSdkManager(tangemSdk, context) - } - - @Provides - @ActivityScoped - fun provideScanCardUseCase(tangemSdk: TangemSdk, tangemSdkManager: TangemSdkManager): ScanCardUseCase { + @Singleton + fun provideScanCardUseCase( + tangemSdkManager: TangemSdkManager, + cardSdkConfigRepository: CardSdkConfigRepository, + ): ScanCardUseCase { return ScanCardUseCase( - tangemSdk = tangemSdk, - scanCardRepository = DefaultScanCardRepository(userTokensRepository, tangemSdkManager), + cardSdkConfigRepository = cardSdkConfigRepository, + scanCardRepository = DefaultScanCardRepository( + userTokensRepository = userTokensRepository, + tangemSdkManager = tangemSdkManager, + ), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt new file mode 100644 index 0000000000..4af9f75d50 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -0,0 +1,46 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase +import com.tangem.domain.card.GetBiometricsStatusUseCase +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CardDomainModule { + + @Provides + @Singleton + fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor() + + @Provides + @Singleton + fun provideGetBiometricsStatusUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): GetBiometricsStatusUseCase { + return GetBiometricsStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } + + @Provides + @Singleton + fun provideSetAccessCodeRequestPolicyUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): SetAccessCodeRequestPolicyUseCase { + return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } + + @Provides + @Singleton + fun provideGetAccessCodeSavingStatusUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): GetAccessCodeSavingStatusUseCase { + return GetAccessCodeSavingStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt new file mode 100644 index 0000000000..a43ebfeb6c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -0,0 +1,34 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.scopes.ViewModelScoped + +@Module +@InstallIn(ViewModelComponent::class) +internal object TokensDomainModule { + + @Provides + @ViewModelScoped + fun provideGetTokenListUseCase( + tokensRepository: TokensRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetTokenListUseCase { + return GetTokenListUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { + return ToggleTokenListSortingUseCase(dispatchers) + } +} \ No newline at end of file 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 ec8103cbe2..5ab4d720a3 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -1,27 +1,25 @@ package com.tangem.tap.domain -import android.content.Context +import android.content.res.Resources import androidx.annotation.StringRes import com.tangem.Message import com.tangem.TangemSdk import com.tangem.blockchain.common.Blockchain import com.tangem.common.* import com.tangem.common.biometric.BiometricManager -import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.* import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.ScanTask import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask -import com.tangem.operations.pins.CheckUserCodesCommand -import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.tap.common.analytics.events.Basic @@ -38,7 +36,10 @@ import kotlinx.coroutines.withContext import kotlin.coroutines.resume @Suppress("TooManyFunctions") -class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) { +class TangemSdkManager(private val cardSdkConfigRepository: CardSdkConfigRepository, private val resources: Resources) { + + private val tangemSdk: TangemSdk + get() = cardSdkConfigRepository.sdk private val userCodeRepository by lazy { UserCodeRepository( @@ -63,7 +64,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co messageRes: Int? = null, allowsRequestAccessCodeFromRepository: Boolean = false, ): CompletionResult { - val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header)) + val message = Message(resources.getString(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( runnable = ScanProductTask( card = null, @@ -80,7 +81,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsync( CreateProductWalletTask(scanResponse.cardTypesResolver), scanResponse.card.cardId, - Message(context.getString(R.string.initial_message_create_wallet_body)), + Message(resources.getString(R.string.initial_message_create_wallet_body)), ) } @@ -92,7 +93,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co is CompletionResult.Success -> runTaskAsync( CreateProductWalletTask(scanResponse.cardTypesResolver, seedResult.data), scanResponse.card.cardId, - Message(context.getString(R.string.initial_message_create_wallet_body)), + Message(resources.getString(R.string.initial_message_create_wallet_body)), ) is CompletionResult.Failure -> CompletionResult.Failure(seedResult.error) @@ -111,7 +112,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( CreateWalletAndRescanTask(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body)), + initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)), ) .map { CardDTO(it) } } @@ -127,7 +128,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( runnable = ResetToFactorySettingsTask(), cardId = cardId, - initialMessage = Message(context.getString(R.string.card_settings_reset_card_to_factory)), + initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)), ) .map { CardDTO(it) } } @@ -154,7 +155,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( SetUserCodeCommand.changePasscode(null), cardId, - initialMessage = Message(context.getString(R.string.initial_message_change_passcode_body)), + initialMessage = Message(resources.getString(R.string.initial_message_change_passcode_body)), ) } @@ -162,7 +163,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( SetUserCodeCommand.changeAccessCode(null), cardId, - initialMessage = Message(context.getString(R.string.initial_message_change_access_code_body)), + initialMessage = Message(resources.getString(R.string.initial_message_change_access_code_body)), ) } @@ -170,15 +171,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( SetUserCodeCommand.resetUserCodes(), cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header)), - ) - } - - suspend fun checkUserCodes(cardId: String?): CompletionResult { - return runTaskAsyncReturnOnMain( - CheckUserCodesCommand(), - cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), ) } @@ -186,7 +179,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( SetUserCodeRecoveryAllowedTask(enabled), cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), ) } @@ -197,7 +190,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co return runTaskAsyncReturnOnMain( runnable = ScanTask(allowRequestAccessCodeFromRepository), cardId = cardId, - initialMessage = Message(context.getString(R.string.initial_message_tap_header)), + initialMessage = Message(resources.getString(R.string.initial_message_tap_header)), ) .map { CardDTO(it) } } @@ -233,8 +226,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co } } + @Deprecated("TangemSdkManager shouldn't returns a string from resources") fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String { - return context.getString(stringResId, *formatArgs) + return resources.getString(stringResId, *formatArgs) } fun setAccessCodeRequestPolicy(useBiometricsForAccessCode: Boolean) { 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 f963c6f982..363565bf3e 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 @@ -1,24 +1,17 @@ package com.tangem.tap.domain.extensions -import com.tangem.common.card.FirmwareVersion import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.userwallets.Artwork import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.tap.features.wallet.redux.Artwork val CardDTO.remainingSignatures: Int? get() = this.wallets.firstOrNull()?.remainingSignatures -val CardDTO.isWalletDataSupported: Boolean - get() = this.firmwareVersion.major >= 4 - -val CardDTO.isFirmwareMultiwalletAllowed: Boolean - get() = firmwareVersion >= FirmwareVersion.MultiWalletAvailable && settings.maxWalletsCount > 1 - val CardDTO.isHdWalletAllowedByApp: Boolean get() = settings.isHDWalletAllowed @@ -58,16 +51,4 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result ifAnyError() } -} - -fun CardDTO.getArtworkUrl(artworkId: String?): String? { - return when { - artworkId != null -> { - OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) - } - - cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL - cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL - else -> null - } } \ 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 c622163be0..7253b1fccf 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 @@ -31,7 +31,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( val seedKey = wallet.extendedPublicKey return when { scanResponse.cardTypesResolver.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> { - makeTwinWalletManager( + createTwinWalletManager( walletPublicKey = wallet.publicKey, pairPublicKey = scanResponse.secondTwinPublicKey!!.hexToBytes(), blockchain = environmentBlockchain, @@ -47,7 +47,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( val derivedKey = derivedKeys?.get(derivationPath) ?: return null - makeWalletManager( + createWalletManager( blockchain = environmentBlockchain, seedKey = wallet.publicKey, derivedKey = derivedKey, @@ -55,7 +55,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( ) } else -> { - makeWalletManager( + createLegacyWalletManager( blockchain = environmentBlockchain, walletPublicKey = wallet.publicKey, curve = wallet.curve, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt similarity index 73% rename from app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt rename to app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index 0546a79db5..7c21a8a2f2 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -2,21 +2,22 @@ package com.tangem.tap.domain.scanCard import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store // TODO: Remove this object after feature toggle was removed and use ScanCardUseCase instead -internal object ScanCardProcessor { +internal class DefaultScanCardProcessor : ScanCardProcessor { private val isNewCardScanningEnabled: Boolean get() = store.state.daggerGraphState .get(DaggerGraphState::customTokenFeatureToggles) .isNewCardScanningEnabled - suspend fun scan( - cardId: String? = null, - allowsRequestAccessCodeFromRepository: Boolean = false, + override suspend fun scan( + cardId: String?, + allowsRequestAccessCodeFromRepository: Boolean, ): CompletionResult { return if (isNewCardScanningEnabled) { UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository) @@ -26,15 +27,15 @@ internal object ScanCardProcessor { } @Suppress("LongParameterList") - suspend fun scan( - analyticsEvent: AnalyticsEvent? = null, - cardId: String? = null, - onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, - onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, - onWalletNotCreated: suspend () -> Unit = {}, - disclaimerWillShow: () -> Unit = {}, - onFailure: suspend (error: TangemError) -> Unit = {}, - onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {}, + override suspend fun scan( + analyticsEvent: AnalyticsEvent?, + cardId: String?, + onProgressStateChange: suspend (showProgress: Boolean) -> Unit, + onScanStateChange: suspend (scanInProgress: Boolean) -> Unit, + onWalletNotCreated: suspend () -> Unit, + disclaimerWillShow: () -> Unit, + onFailure: suspend (error: TangemError) -> Unit, + onSuccess: suspend (scanResponse: ScanResponse) -> Unit, ) { if (isNewCardScanningEnabled) { UseCaseScanProcessor.scan( diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index b2b40dd547..ec38e494bc 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -6,7 +6,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.canSkipBackup diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index ae4e286e5f..002b9bc7ff 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.scanCard import arrow.fx.coroutines.resourceScope import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.card.ScanCardException diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt index febc6a1525..8f72ce92f8 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/AnalyticsChain.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.scanCard.chains import arrow.core.Either import arrow.core.right import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.card.ScanCardException import com.tangem.domain.core.chain.Chain import com.tangem.domain.models.scan.ScanResponse diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt index 1ab2b50b69..a2622b218d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt @@ -31,8 +31,7 @@ class ResetToFactorySettingsTask : CardSessionRunnable { } private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val backupStatus = session.environment.card?.backupStatus - if (backupStatus == null || backupStatus == Card.BackupStatus.NoBackup) { + if (session.environment.card?.backupStatus?.isActive != true) { callback(CompletionResult.Success(session.environment.card!!)) return } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index c4c01684bf..6842f8a94c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -10,7 +10,7 @@ import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.files.AndroidFileReader import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.models.scan.CardDTO -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.domain.tokens.converters.CurrencyConverter import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.wallet.models.Currency diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index 4ba9c2f058..a86a86719a 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.analytics.events.WalletConnect @@ -36,7 +37,6 @@ import okhttp3.Interceptor import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response -import okhttp3.logging.HttpLoggingInterceptor import timber.log.Timber import java.util.* import java.util.concurrent.TimeUnit @@ -56,8 +56,9 @@ class WalletConnectManager { .addInterceptor(RetryInterceptor()) .build() } + private val interceptor by lazy { - HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } + createNetworkLoggingInterceptor() } private var sessions: MutableMap = mutableMapOf() diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index 134a915fbd..03f8cf7d40 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.hexToBigDecimal @@ -35,8 +36,8 @@ import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTrans import com.tangem.tap.features.details.redux.walletconnect.* import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store -import com.tangem.tap.tangemSdk import com.tangem.tap.tangemSdkManager import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.* import timber.log.Timber @@ -76,7 +77,8 @@ class WalletConnectSdkHelper { val transactionData = TransactionData( amount = Amount(value, wallet.blockchain), - fee = Amount(fee, wallet.blockchain), + // TODO refactoring + fee = Fee.Common(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, destinationAddress = transaction.to!!, extras = EthereumTransactionExtras( @@ -161,7 +163,10 @@ class WalletConnectSdkHelper { private suspend fun sendTransaction(data: WcTransactionData, cardId: String?): String? { val result = (data.walletManager as TransactionSender).send( transactionData = data.transaction, - signer = CommonSigner(tangemSdk, cardId), + signer = CommonSigner( + tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, + cardId = cardId, + ), ) return when (result) { SimpleResult.Success -> { @@ -182,7 +187,6 @@ class WalletConnectSdkHelper { transactionData = data.transaction, nonce = null, blockchain = data.walletManager.wallet.blockchain, - gasLimit = null, ) ?: return null val command = SignHashCommand( 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 cccc4167ae..ef011c6dc1 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 @@ -6,6 +6,8 @@ import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.toBlockchainSdkError +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.CompletionResult @@ -84,13 +86,13 @@ object DemoHelper { class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender { - override suspend fun getFee(amount: Amount, destination: String): Result> { + override suspend fun getFee(amount: Amount, destination: String): Result { val blockchain = walletManager.wallet.blockchain return Result.Success( - listOf( - Amount(0.0001.toBigDecimal(), blockchain), - Amount(0.0002.toBigDecimal(), blockchain), - Amount(0.0003.toBigDecimal(), blockchain), + TransactionFee.Choosable( + minimum = Fee.Common(Amount(minimumFee, blockchain)), + normal = Fee.Common(Amount(normalFee, blockchain)), + priority = Fee.Common(Amount(priorityFee, blockchain)), ), ) } @@ -120,5 +122,9 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact companion object { val ID = DemoTransactionSender::class.java.simpleName + + private val minimumFee = 0.0001.toBigDecimal() + private val normalFee = 0.0002.toBigDecimal() + private val priorityFee = 0.0003.toBigDecimal() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index a992489911..478d0cb1bd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -12,6 +12,8 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -23,9 +25,6 @@ import com.tangem.tap.common.extensions.onUserWalletSelected 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.domain.model.builders.UserWalletBuilder -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation import com.tangem.tap.domain.userWalletList.isLockedSync @@ -33,6 +32,7 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.tangemSdkManager import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers @@ -74,7 +74,8 @@ class DetailsMiddleware { is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) DetailsAction.ScanCard -> { scope.launch { - ScanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + .scan(allowsRequestAccessCodeFromRepository = true) .doOnSuccess { scanResponse -> // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) @@ -371,9 +372,9 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On)) preferencesStorage.shouldSaveAccessCodes = true - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = scanResponse?.card?.isAccessCodeSet == true, - ) + store.state.daggerGraphState + .get(DaggerGraphState::cardSdkConfigRepository) + .setAccessCodeRequestPolicy(isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true) return CompletionResult.Success(Unit) } @@ -384,9 +385,9 @@ class DetailsMiddleware { Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off)) preferencesStorage.shouldSaveAccessCodes = false - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = false, - ) + store.state.daggerGraphState + .get(DaggerGraphState::cardSdkConfigRepository) + .setAccessCodeRequestPolicy(isBiometricsRequestPolicy = false) } .doOnFailure { error -> Timber.e(error, "Unable to delete saved access codes") diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt index d67c7a262d..ccfe000c5c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/QrScanFragment.kt @@ -11,6 +11,7 @@ import androidx.activity.OnBackPressedCallback import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.fragment.app.Fragment +import com.google.zxing.BarcodeFormat import com.google.zxing.Result import com.otaliastudios.cameraview.CameraView import com.tangem.core.navigation.NavigationAction @@ -38,10 +39,19 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { if (!permissionIsGranted()) requestPermission() - scannerView = ZXingScannerView(activity) + scannerView = ZXingScannerView(activity).apply { + setFormats(listOf(BarcodeFormat.QR_CODE)) + } + return scannerView } + override fun onResume() { + super.onResume() + scannerView?.setResultHandler(this) + scannerView?.startCamera() + } + override fun onPause() { super.onPause() scannerView?.stopCamera() @@ -52,12 +62,6 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler { setFitSystemWindows(fit = false) } - override fun onResume() { - super.onResume() - scannerView?.setResultHandler(this) - scannerView?.startCamera() - } - override fun handleResult(result: Result) { store.dispatch(NavigationAction.PopBackTo()) setFitSystemWindows(fit = false) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index 86b139defb..93479d1767 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -1,6 +1,6 @@ package com.tangem.tap.features.home.redux -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.entities.IndeterminateProgressButton 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 7a77fcaa60..15a3f5c172 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 @@ -5,11 +5,11 @@ import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.* +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop @@ -20,13 +20,16 @@ import com.tangem.tap.common.extensions.eraseContext import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.model.builders.UserWalletBuilder -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.home.redux.HomeMiddleware.BUY_WALLET_URL import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.features.signin.redux.SignInAction +import com.tangem.tap.preferencesStorage +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope +import com.tangem.tap.store +import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.rekotlin.Action @@ -74,11 +77,11 @@ private fun handleHomeAction(action: Action) { private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch { delay(timeMillis = 200) - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) - ScanCardProcessor.scan( + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = analyticsEvent, onProgressStateChange = { showProgress -> if (showProgress) { 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 624ecfb185..78f8cc873e 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 @@ -11,12 +11,12 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.extensions.removeContext import com.tangem.tap.common.extensions.setContext -import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import kotlinx.coroutines.delay import kotlinx.coroutines.launch 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 af0338bb49..b768c53091 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 @@ -9,6 +9,7 @@ import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding @@ -19,7 +20,6 @@ import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.extensions.makePrimaryWalletManager -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.userWalletList.isLockedSync import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE 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 77bc7f7310..2fa0a10909 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 @@ -17,15 +17,12 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.ShareElement import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.datasource.asset.AssetReader -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.Artwork import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.beginDelayedTransition -import com.tangem.tap.common.extensions.getDrawableCompat -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.domain.twins.TwinsCardWidget @@ -35,7 +32,6 @@ import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletM import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep -import com.tangem.tap.features.wallet.redux.Artwork import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding 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 1819011b64..2ce6725c37 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 @@ -14,6 +14,7 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.Artwork import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.operations.backup.BackupService @@ -30,10 +31,9 @@ import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.wallet.models.toCurrencies -import com.tangem.tap.features.wallet.redux.Artwork +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch import org.rekotlin.Action -import org.rekotlin.DispatchFunction import org.rekotlin.Middleware object OnboardingWalletMiddleware { @@ -44,8 +44,8 @@ private val onboardingWalletMiddleware: Middleware = { dispatch, state { next -> { action -> when (action) { - is OnboardingWallet2Action -> handleWallet2Action(action, state) - else -> handleWalletAction(action, state, dispatch) + is OnboardingWallet2Action -> handleWallet2Action(action) + else -> handleWalletAction(action) } next(action) } @@ -53,7 +53,7 @@ private val onboardingWalletMiddleware: Middleware = { dispatch, state } @Suppress("LongMethod", "ComplexMethod") -private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: DispatchFunction) { +private fun handleWalletAction(action: Action) { if (action !is OnboardingWalletAction) return val globalState = store.state.globalState @@ -179,7 +179,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch: } @Suppress("LongMethod", "ComplexMethod") -private fun handleWallet2Action(action: OnboardingWallet2Action, state: () -> AppState?) { +private fun handleWallet2Action(action: OnboardingWallet2Action) { val globalState = store.state.globalState val onboardingManager = globalState.onboardingState.onboardingManager @@ -330,7 +330,9 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) is BackupAction.AddBackupCard -> { backupService.addBackupCard { result -> backupService.skipCompatibilityChecks = false - tangemSdk.config.filter.cardIdFilter = null + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk + .config.filter.cardIdFilter = null + when (result) { is CompletionResult.Success -> { store.dispatchOnMain(BackupAction.AddBackupCard.Success) diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 945fc90afd..8261f5bef3 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -9,6 +9,7 @@ import com.tangem.common.flatMap import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -18,10 +19,10 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.model.builders.UserWalletBuilder import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation import com.tangem.tap.domain.userWalletList.isLockable import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -110,9 +111,10 @@ internal class SaveWalletMiddleware { // Enable saving access codes only if this is the first time user save the wallet if (isFirstSavedWallet) { preferencesStorage.shouldSaveAccessCodes = true - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = userWallet.hasAccessCode, - ) + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + .setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) } val savedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 5f92c3c10b..cd9ad2160d 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -4,6 +4,7 @@ import com.tangem.Message import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.core.TangemSdkError import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered import com.tangem.tap.common.redux.StateDialog @@ -114,7 +115,7 @@ sealed class FeeAction : SendScreenAction { object RequestFee : FeeAction() sealed class FeeCalculation : FeeAction() { - data class SetFeeResult(val fee: List) : FeeCalculation() + data class SetFeeResult(val fee: TransactionFee) : FeeCalculation() object ClearResult : FeeCalculation() } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt index 15522e87d2..0564218bcd 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/AmountMiddleware.kt @@ -67,7 +67,7 @@ class AmountMiddleware { } val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend(inputCrypto)) - val transactionErrors = walletManager.validateTransaction(amountToSend, sendState.feeState.currentFee) + val transactionErrors = walletManager.validateTransaction(amountToSend, sendState.feeState.currentFee?.amount) val amountFieldErrors = filterErrorsForAmountField(transactionErrors) if (amountFieldErrors.isEmpty()) { dispatch(AmountAction.SetAmountError(null)) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index 3cbd54e044..efdd59f799 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.features.send.redux.middlewares import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero import com.tangem.tap.common.redux.AppState @@ -53,15 +54,18 @@ class RequestFeeMiddleware { val result = feeResult.data // val result = FeeMock.getFee(walletManager.wallet.blockchain) dispatch(FeeAction.FeeCalculation.SetFeeResult(result)) - if (result.size == 1) { - val fee = result[0].value ?: BigDecimal.ZERO - if (fee.isZero()) { - dispatch(FeeAction.ChangeLayoutVisibility(main = false)) - } else { - dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false)) + when (result) { + is TransactionFee.Single -> { + val fee = result.normal.amount.value ?: BigDecimal.ZERO + if (fee.isZero()) { + dispatch(FeeAction.ChangeLayoutVisibility(main = false)) + } else { + dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = false)) + } + } + is TransactionFee.Choosable -> { + dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = true)) } - } else { - dispatch(FeeAction.ChangeLayoutVisibility(main = true, chipGroup = true)) } } is Result.Failure -> { 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 af8480549a..a76724c7c3 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 @@ -7,13 +7,14 @@ import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactionExtras -import com.tangem.core.navigation.NavigationAction import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics +import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO @@ -38,6 +39,7 @@ import com.tangem.tap.features.send.redux.FeeAction.RequestFee import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -97,11 +99,11 @@ private fun verifyAndSendTransaction( val card = appState.globalState.scanResponse?.card ?: return val destinationAddress = sendState.addressState.destinationWalletAddress ?: return val typedAmount = sendState.amountState.amountToExtract ?: return - val feeAmount = sendState.feeState.currentFee ?: return + val fee = sendState.feeState.currentFee ?: return val amountToSend = Amount(typedAmount, sendState.getTotalAmountToSend()) - val transactionErrors = walletManager.validateTransaction(amountToSend, feeAmount) + val transactionErrors = walletManager.validateTransaction(amountToSend, fee.amount) when { transactionErrors.contains(TransactionError.TezosSendAll) -> { val reduceAmount = walletManager.wallet.blockchain.minimalAmount() @@ -116,7 +118,7 @@ private fun verifyAndSendTransaction( action = action, walletManager = walletManager, amountToSend = amountToSend, - feeAmount = feeAmount, + fee = fee, feeType = sendState.feeState.selectedFeeType, destinationAddress = destinationAddress, transactionExtras = sendState.transactionExtrasState, @@ -135,7 +137,7 @@ private fun verifyAndSendTransaction( action = action, walletManager = walletManager, amountToSend = amountToSend, - feeAmount = feeAmount, + fee = fee, feeType = sendState.feeState.selectedFeeType, destinationAddress = destinationAddress, transactionExtras = sendState.transactionExtrasState, @@ -153,7 +155,7 @@ private fun sendTransaction( action: SendActionUi.SendAmountToRecipient, walletManager: WalletManager, amountToSend: Amount, - feeAmount: Amount, + fee: Fee, feeType: FeeType, destinationAddress: String, transactionExtras: TransactionExtrasState, @@ -163,7 +165,7 @@ private fun sendTransaction( dispatch: (Action) -> Unit, ) { dispatch(SendAction.ChangeSendButtonState(ButtonState.PROGRESS)) - var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress) + var txData = walletManager.createTransaction(amountToSend, fee, destinationAddress) transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) } transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) } @@ -181,7 +183,7 @@ private fun sendTransaction( updateFeedbackManagerInfo( walletManager = walletManager, amountToSend = amountToSend, - feeAmount = feeAmount, + feeAmount = fee.amount, destinationAddress = destinationAddress, ) dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error = error)) @@ -200,6 +202,7 @@ private fun sendTransaction( return@launch } + val tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk val linkedTerminalState = tangemSdk.config.linkedTerminal if (card.isStart2Coin) { tangemSdk.config.linkedTerminal = false @@ -276,7 +279,7 @@ private fun sendTransaction( updateFeedbackManagerInfo( walletManager = walletManager, amountToSend = amountToSend, - feeAmount = feeAmount, + feeAmount = fee.amount, destinationAddress = destinationAddress, ) val error = sendResult.error as? BlockchainSdkError ?: return@withMainContext diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt index d0548132b9..7e2e65171e 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/FeeReducer.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.send.redux.reducers -import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.tap.features.send.redux.FeeAction import com.tangem.tap.features.send.redux.FeeActionUi import com.tangem.tap.features.send.redux.SendScreenAction @@ -13,6 +14,7 @@ import com.tangem.tap.features.wallet.redux.ProgressState [REDACTED_AUTHOR] */ class FeeReducer : SendInternalReducer { + override fun handle(action: SendScreenAction, sendState: SendState): SendState = when (action) { is FeeActionUi -> handleUiAction(action, sendState, sendState.feeState) is FeeAction -> handleAction(action, sendState, sendState.feeState) @@ -25,7 +27,9 @@ class FeeReducer : SendInternalReducer { state.copy(controlsLayoutIsVisible = !state.controlsLayoutIsVisible) } is FeeActionUi.ChangeSelectedFee -> { - val currentFee = createValueOfFeeAmount(action.feeType, state.feeList) + val currentFee = state.fees?.let { + createValueOfFeeAmount(action.feeType, it) + } state.copy( selectedFeeType = action.feeType, currentFee = currentFee, @@ -50,34 +54,36 @@ class FeeReducer : SendInternalReducer { ) } is FeeAction.FeeCalculation.SetFeeResult -> { - val fees = action.fee - if (fees.size == 1) { - val feeType = FeeType.SINGLE - val currentFee = createValueOfFeeAmount(feeType, fees) + when (val fees = action.fee) { + is TransactionFee.Single -> { + val feeType = FeeType.SINGLE + val currentFee = createValueOfFeeAmount(feeType, fees) - state.copy( - selectedFeeType = feeType, - feeList = fees, - currentFee = currentFee, - feeIsApproximate = isFeeApproximate(sendState), - ) - } else { - val feeType = getCurrentFeeType(state) - val currentFee = createValueOfFeeAmount(feeType, fees) + state.copy( + selectedFeeType = feeType, + fees = fees, + currentFee = currentFee, + feeIsApproximate = isFeeApproximate(sendState), + ) + } + is TransactionFee.Choosable -> { + val feeType = getCurrentFeeType(state) + val currentFee = createValueOfFeeAmount(feeType, fees) - state.copy( - selectedFeeType = feeType, - feeList = fees, - currentFee = currentFee, - feeIsApproximate = isFeeApproximate(sendState), - ) + state.copy( + selectedFeeType = feeType, + fees = fees, + currentFee = currentFee, + feeIsApproximate = isFeeApproximate(sendState), + ) + } }.copy( progressState = ProgressState.Done, ) } FeeAction.FeeCalculation.ClearResult -> { state.copy( - feeList = null, + fees = null, currentFee = null, progressState = ProgressState.Done, ) @@ -87,17 +93,18 @@ class FeeReducer : SendInternalReducer { return updateLastState(sendState.copy(feeState = result), result) } - private fun createValueOfFeeAmount(feeType: FeeType, list: List?): Amount? { - if (list == null || list.isEmpty()) return null - - return if (list.size == 1) { - list[0] - } else { - when (feeType) { - FeeType.SINGLE -> list[1] - FeeType.LOW -> list[0] - FeeType.NORMAL -> list[1] - FeeType.PRIORITY -> list[2] + private fun createValueOfFeeAmount(feeType: FeeType, transactionFee: TransactionFee): Fee { + return when (transactionFee) { + is TransactionFee.Single -> { + transactionFee.normal + } + is TransactionFee.Choosable -> { + when (feeType) { + FeeType.SINGLE -> transactionFee.normal + FeeType.LOW -> transactionFee.minimum + FeeType.NORMAL -> transactionFee.normal + FeeType.PRIORITY -> transactionFee.priority + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt index 65067fc0f8..f7a3a5e41b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/FeeState.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.send.redux.states -import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.features.wallet.redux.ProgressState import java.math.BigDecimal @@ -23,8 +24,8 @@ fun FeeType.convertToAnalyticsFeeType(): AnalyticsParam.FeeType { data class FeeState( val selectedFeeType: FeeType = FeeType.NORMAL, - val feeList: List? = null, - val currentFee: Amount? = null, + val fees: TransactionFee? = null, + val currentFee: Fee? = null, val feeIsIncluded: Boolean = false, val feeIsApproximate: Boolean = false, val mainLayoutIsVisible: Boolean = false, @@ -38,5 +39,5 @@ data class FeeState( fun isReady(): Boolean = currentFee != null - fun getCurrentFeeValue(): BigDecimal = currentFee?.value ?: BigDecimal.ZERO + fun getCurrentFeeValue(): BigDecimal = currentFee?.amount?.value ?: BigDecimal.ZERO } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt index 8af509f84f..417a308de0 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel import androidx.transition.TransitionInflater +import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel @@ -40,6 +41,10 @@ internal class TokensListFragment : Fragment() { } TangemTheme { + val statusBarColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(color = statusBarColor) + } TokensListScreen( modifier = Modifier.systemBarsPadding(), stateHolder = viewModel.uiState, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 3f73a94c11..10803cf57f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -3,7 +3,7 @@ package com.tangem.tap.features.wallet.redux import android.content.Context import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.address.AddressType -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWallet 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 2135a4ab7d..0048c80636 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 @@ -1,11 +1,11 @@ package com.tangem.tap.features.wallet.redux -import android.graphics.Bitmap import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.userwallets.Artwork import com.tangem.tap.common.entities.Button import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.toggleWidget.WidgetState @@ -118,19 +118,4 @@ enum class ErrorType { sealed class WalletMainButton(enabled: Boolean) : Button(enabled) { class SendButton(enabled: Boolean) : WalletMainButton(enabled) -} - -data class Artwork( - val artworkId: String, - val artwork: Bitmap? = null, -) { - companion object { - const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png" - const val SERGIO_CARD_URL = "https://app.tangem.com/cards/card_tg059.png" - const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png" - const val SERGIO_CARD_ID = "BC01" - const val MARTA_CARD_ID = "BC02" - const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png" - const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index d008d1b2e0..3f080d645a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -15,10 +15,10 @@ import com.tangem.tap.common.extensions.dispatchErrorNotification import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.models.WalletDialog +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.userWalletsListManager @@ -102,7 +102,8 @@ class MultiWalletMiddleware { } private fun scanAndUpdateCard(selectedUserWallet: UserWallet) = scope.launch(Dispatchers.Default) { - ScanCardProcessor.scan(selectedUserWallet.cardId, allowsRequestAccessCodeFromRepository = true) + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor) + .scan(cardId = selectedUserWallet.cardId, allowsRequestAccessCodeFromRepository = true) .flatMap { scanResponse -> userWalletsListManager.update( userWalletId = selectedUserWallet.walletId, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index fbb76f391a..65be537ac8 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -72,7 +72,7 @@ class TradeCryptoMiddleware { } scope.launch { - exchangeManager.buyErc20TestnetTokens( + buyErc20TestnetTokens( card = card, walletManager = walletManager, token = currency.token, 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 2bdd72972f..a96e7e6197 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 @@ -12,6 +12,7 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.domain.userwallets.GetCardImageUseCase import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic @@ -23,7 +24,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel -import com.tangem.tap.domain.userWalletList.GetCardImageUseCase import com.tangem.tap.domain.userWalletList.lockIfLockable import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction 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 a196a6146b..e91e0a1194 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 @@ -1,14 +1,19 @@ package com.tangem.tap.features.wallet.redux.reducers import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.userwallets.Artwork import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TapError import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.features.wallet.models.Currency -import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.ErrorType +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.userWalletsListManager import org.rekotlin.Action @@ -114,7 +119,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS if (selectedUserWallet == action.userWalletId) { newState = newState.copy( - cardImage = Artwork(artworkId = action.url, artwork = newState.cardImage?.artwork), + cardImage = Artwork(artworkId = action.url), ) } } @@ -151,7 +156,7 @@ fun Wallet.createAddressesData(): List { getShareUri(it.value), getExploreUrl(it.value), ) - if (it.type == blockchain.defaultAddressType()) { + if (it.type == AddressType.Default) { listOfAddressData.add(0, addressData) } else { listOfAddressData.add(addressData) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 23aa7d9d6e..3cab6df274 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -4,7 +4,7 @@ import androidx.annotation.IdRes import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.model.WalletDataModel -import com.tangem.tap.features.wallet.ui.utils.getFormattedAmount +import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount import com.tangem.tap.store import com.tangem.wallet.R @@ -102,8 +102,8 @@ class BalanceWidget( groupBaseCurrency.show() tvCurrency.text = tokenWalletData?.currency?.currencySymbol tvBaseCurrency.text = data.currency.currencyName - tvAmount.text = if (showAmount) tokenWalletData?.getFormattedAmount() else "" - tvBaseAmount.text = if (showAmount) data.getFormattedAmount() else "" + tvAmount.text = if (showAmount) tokenWalletData?.getFormattedCryptoAmount() else "" + tvBaseAmount.text = if (showAmount) data.getFormattedCryptoAmount() else "" if (showAmount) { tvFiatAmount.show() tvFiatAmount.text = tokenWalletData?.getFormattedFiatAmount(store.state.globalState.appCurrency) @@ -113,7 +113,7 @@ class BalanceWidget( private fun showBalanceWithoutToken(data: WalletDataModel, showAmount: Boolean) = with(binding.lBalance) { groupBaseCurrency.hide() tvCurrency.text = data.currency.currencyName - tvAmount.text = if (showAmount) data.getFormattedAmount() else "" + tvAmount.text = if (showAmount) data.getFormattedCryptoAmount() else "" if (showAmount) { tvFiatAmount.show() tvFiatAmount.text = data.getFormattedFiatAmount(store.state.globalState.appCurrency) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt index e82c2f9547..c61cb57c3f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -1,44 +1,38 @@ package com.tangem.tap.features.wallet.ui import android.view.View -import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType -import com.tangem.blockchain.blockchains.cardano.CardanoAddressType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.wallet.R object MultipleAddressUiHelper { - fun typeToId(type: AddressType): Int { - return when (type) { - is BitcoinAddressType.Legacy -> R.id.chip_legacy - is BitcoinAddressType.Segwit -> R.id.chip_default - is CardanoAddressType.Byron -> R.id.chip_legacy - is CardanoAddressType.Shelley -> R.id.chip_default - else -> View.NO_ID + + private val blockchainsSupportingSplit = listOf( + Blockchain.Bitcoin, + Blockchain.BitcoinTestnet, + Blockchain.Litecoin, + Blockchain.BitcoinCash, + Blockchain.CardanoShelley, + ) + + fun typeToId(type: AddressType, blockchain: Blockchain): Int { + return if (blockchain in blockchainsSupportingSplit) { + if (type == AddressType.Legacy) { + R.id.chip_legacy + } else { + R.id.chip_default + } + } else { + View.NO_ID } } - fun idToType(id: Int, blockchain: Blockchain?): AddressType? { - return when (id) { - R.id.chip_default -> { - when (blockchain) { - Blockchain.Bitcoin, - Blockchain.BitcoinTestnet, - Blockchain.Litecoin, - Blockchain.BitcoinCash, - -> BitcoinAddressType.Segwit - Blockchain.CardanoShelley -> CardanoAddressType.Shelley - else -> null - } - } - R.id.chip_legacy -> { - when (blockchain) { - Blockchain.Bitcoin, - Blockchain.BitcoinTestnet, - Blockchain.Litecoin, - Blockchain.BitcoinCash, - -> BitcoinAddressType.Legacy - Blockchain.CardanoShelley -> CardanoAddressType.Byron + fun idToType(id: Int, blockchain: Blockchain): AddressType? { + return when (blockchain) { + in blockchainsSupportingSplit -> { + when (id) { + R.id.chip_default -> AddressType.Default + R.id.chip_legacy -> AddressType.Legacy else -> null } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index 09946690a4..1c0c5027f2 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -59,7 +59,7 @@ import com.tangem.tap.features.wallet.ui.images.load import com.tangem.tap.features.wallet.ui.test.TestWallet import com.tangem.tap.features.wallet.ui.utils.assembleWarnings import com.tangem.tap.features.wallet.ui.utils.getAvailableActions -import com.tangem.tap.features.wallet.ui.utils.getFormattedAmount +import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount import com.tangem.tap.features.wallet.ui.utils.isAvailableToBuy import com.tangem.tap.features.wallet.ui.utils.isAvailableToSell @@ -390,7 +390,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt chipGroupAddressType.show() chipGroupAddressType.fitChipsByGroupWidth() - val checkedId = MultipleAddressUiHelper.typeToId(selectedAddress.type) + val checkedId = MultipleAddressUiHelper.typeToId(selectedAddress.type, currency.blockchain) if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId) chipGroupAddressType.setOnCheckedChangeListener { _, checkedId -> @@ -425,7 +425,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt lBalance.root.show() lBalance.groupBalance.show() lBalance.tvError.hide() - lBalance.tvAmount.text = walletData.getFormattedAmount() + lBalance.tvAmount.text = walletData.getFormattedCryptoAmount() lBalance.tvFiatAmount.text = walletData.getFormattedFiatAmount(store.state.globalState.appCurrency) lBalance.tvStatus.setLoadingStatus(R.string.wallet_balance_loading) } @@ -437,7 +437,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), SafeSt lBalance.root.show() lBalance.groupBalance.show() lBalance.tvError.hide() - lBalance.tvAmount.text = walletData.getFormattedAmount() + lBalance.tvAmount.text = walletData.getFormattedCryptoAmount() lBalance.tvFiatAmount.text = walletData.getFormattedFiatAmount(store.state.globalState.appCurrency) when (status) { is WalletDataModel.VerifiedOnline, 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 4aec238877..e0a15c7e4b 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 @@ -15,7 +15,7 @@ import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.ui.images.load -import com.tangem.tap.features.wallet.ui.utils.getFormattedAmount +import com.tangem.tap.features.wallet.ui.utils.getFormattedCryptoAmount import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatRate import com.tangem.tap.store @@ -85,7 +85,7 @@ class WalletAdapter : ListAdapter diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index 00d1e687bb..acac8cd6a8 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -6,6 +6,8 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.* @@ -18,10 +20,8 @@ import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.WalletStoreModel -import com.tangem.tap.domain.model.builders.UserWalletBuilder -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.domain.userWalletList.unlockIfLockable +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.firstOrNull @@ -126,14 +126,16 @@ internal class WalletSelectorMiddleware { private fun addWallet() = scope.launch { Analytics.send(MyWallets.Button.ScanNewCard()) - val prevUseBiometricsForAccessCode = tangemSdkManager.useBiometricsForAccessCode() + val cardSdkConfigRepository = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository) + + val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy() // Update access code policy for access code saving when a card was scanned - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) - ScanCardProcessor.scan( + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.MyWallets), onWalletNotCreated = { // No need to rollback policy, continue with the policy set before the card scan @@ -147,14 +149,14 @@ internal class WalletSelectorMiddleware { saveUserWalletAndPopBackToWalletScreen(scanResponse) .doOnFailure { error -> // Rollback policy if card saving was failed - tangemSdkManager.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) Timber.e(error, "Unable to save user wallet") store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error)) } }, onFailure = { error -> // Rollback policy if card scanning was failed - tangemSdkManager.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) + cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode) Timber.e(error, "Unable to scan card") store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error)) }, @@ -208,7 +210,7 @@ internal class WalletSelectorMiddleware { private suspend fun unlockUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult { Analytics.send(MyWallets.Button.WalletUnlockTapped()) tangemSdkManager.changeDisplayedCardIdNumbersCount(userWallet.scanResponse) - return ScanCardProcessor.scan() + return store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan() .map { scanResponse -> val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() if (scannedUserWalletId == userWallet.walletId) { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 8a96417455..f18499bf50 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -8,6 +8,7 @@ import com.tangem.common.flatMap import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.userwallets.UserWalletBuilder import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic @@ -15,11 +16,10 @@ import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.model.builders.UserWalletBuilder -import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.domain.userWalletList.unlockIfLockable import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.signin.redux.SignInAction +import com.tangem.tap.proxy.redux.DaggerGraphState import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -100,10 +100,10 @@ internal class WelcomeMiddleware { } private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { - tangemSdkManager.setAccessCodeRequestPolicy( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes, ) - ScanCardProcessor.scan( + store.state.daggerGraphState.get(DaggerGraphState::scanCardProcessor).scan( analyticsEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.SignIn), onSuccess = { scanResponse -> scope.launch { onCardScanned(scanResponse) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index a73937a8ee..ae3e244978 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -13,8 +13,8 @@ import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store -import com.tangem.tap.tangemSdk import java.math.BigDecimal /** @@ -72,7 +72,7 @@ class CurrencyExchangeManager( return when (action) { Action.Buy -> buyService Action.Sell -> sellService - } as ExchangeUrlBuilder + } } enum class Action { Buy, Sell } @@ -86,31 +86,26 @@ class CurrencyExchangeManager( } } -suspend fun CurrencyExchangeManager.buyErc20TestnetTokens( - card: CardDTO, - walletManager: EthereumWalletManager, - token: Token, -) { +suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, token: Token) { walletManager.safeUpdate() val amountToSend = Amount(walletManager.wallet.blockchain) val destinationAddress = token.contractAddress - val feeResult = - walletManager.getFee( - amountToSend, - destinationAddress, - ) as? Result.Success ?: return - val fee = feeResult.data[0] + val feeResult = walletManager.getFee( + amountToSend, + destinationAddress, + ) as? Result.Success ?: return + val fee = feeResult.data.minimum val coinValue = walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO - if (coinValue < fee.value) return + if (coinValue < fee.amount.value) return val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress) val signer = TangemSigner( card = card, - tangemSdk = tangemSdk, + tangemSdk = store.state.daggerGraphState.get(DaggerGraphState::cardSdkConfigRepository).sdk, initialMessage = Message(), ) { signResponse -> store.dispatch( diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index e2d0137b1a..8ea80aec87 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -1,6 +1,5 @@ package com.tangem.tap.proxy -import com.tangem.TangemSdk import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationStateHolder import com.tangem.domain.models.scan.CardDTO @@ -41,7 +40,6 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, NavigationState var userTokensRepository: UserTokensRepository? = null var mainStore: Store? = null var tangemSdkManager: TangemSdkManager? = null - var tangemSdk: TangemSdk? = null var walletStoresManager: WalletStoresManager? = null var appFiatCurrency: FiatCurrency = FiatCurrency.Default diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index e6090dd5f0..7c3e6f8b1e 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -6,12 +6,15 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.isNetworkError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.lib.crypto.TransactionManager @@ -22,7 +25,6 @@ import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic.TransactionSent.MemoType import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner -import com.tangem.tap.tangemSdk import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext @@ -32,6 +34,7 @@ import java.math.RoundingMode class TransactionManagerImpl( private val appStateHolder: AppStateHolder, private val analytics: AnalyticsEventHandler, + private val cardSdkConfigRepository: CardSdkConfigRepository, ) : TransactionManager { override suspend fun sendApproveTransaction( @@ -112,7 +115,7 @@ class TransactionManagerImpl( ): SendTxResult { val txData = walletManager.createTransaction( amount = amount, - fee = Amount(value = feeAmount, blockchain = blockchain), + fee = Fee.Common(Amount(value = feeAmount, blockchain = blockchain)), destination = destinationAddress, ).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign)) @@ -214,24 +217,37 @@ class TransactionManagerImpl( return when (fee) { is Result.Success -> { // for not EVM blockchains set gasLimit ZERO for now - val firstFee = fee.data.firstOrNull() ?: error("no fee found") - val minFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = firstFee), - ) - val normalFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.getOrNull(index = 1) ?: firstFee), - ) - val priorityFee = ProxyFee( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.getOrNull(index = 2) ?: firstFee), - ) - ProxyFees( - minFee = minFee, - normalFee = normalFee, - priorityFee = priorityFee, - ) + when (fee.data) { + is TransactionFee.Single -> { + val fee = (fee.data as TransactionFee.Single).normal + val singleFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = fee.amount), + ) + ProxyFees( + minFee = singleFee, + normalFee = singleFee, + priorityFee = singleFee, + ) + } + is TransactionFee.Choosable -> { + val choosableFee = fee.data as TransactionFee.Choosable + ProxyFees( + minFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = choosableFee.minimum.amount), + ), + normalFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = choosableFee.normal.amount), + ), + priorityFee = ProxyFee( + gasLimit = BigInteger.ZERO, + fee = convertToProxyAmount(amount = choosableFee.priority.amount), + ), + ) + } + } } is Result.Failure -> { error(fee.error.message ?: fee.error.customMessage) @@ -280,19 +296,21 @@ class TransactionManagerImpl( } return when (fee) { is Result.Success -> { - val minFee = fee.data.firstOrNull() ?: error("no fee found") + val choosableFee = fee.data + val minProxyFee = ProxyFee( - gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(minFee), + gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit, + fee = convertToProxyAmount(amount = choosableFee.minimum.amount), ) val normalProxyFee = ProxyFee( - gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.getOrNull(index = 1) ?: minFee), + gasLimit = (choosableFee.normal as Fee.Ethereum).gasLimit, + fee = convertToProxyAmount(amount = choosableFee.normal.amount), ) val priorityProxyFee = ProxyFee( - gasLimit = walletManager.gasLimit ?: BigInteger.ZERO, - fee = convertToProxyAmount(fee.data.lastOrNull() ?: minFee), + gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit, + fee = convertToProxyAmount(amount = choosableFee.priority.amount), ) + ProxyFees( minFee = minProxyFee, normalFee = normalProxyFee, @@ -378,7 +396,7 @@ class TransactionManagerImpl( val actualCard = requireNotNull(appStateHolder.getActualCard()) { "no card found" } return TangemSigner( card = actualCard, - tangemSdk = tangemSdk, + tangemSdk = cardSdkConfigRepository.sdk, initialMessage = Message(), ) { signResponse -> appStateHolder.mainStore?.dispatch( diff --git a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt index 1938886ede..432dc559a1 100644 --- a/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/UserWalletManagerImpl.kt @@ -10,6 +10,7 @@ import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.userwallets.UserWalletIdBuilder import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.Currency.NativeToken @@ -17,7 +18,6 @@ import com.tangem.lib.crypto.models.Currency.NonNativeToken import com.tangem.lib.crypto.models.ProxyAmount import com.tangem.lib.crypto.models.ProxyFiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletCurrenciesManager diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index 89f1b31c15..cf7f41785a 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.proxy.di import androidx.compose.ui.text.intl.Locale import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.learn2earn.domain.api.Learn2earnDependencyProvider @@ -43,8 +44,13 @@ class ProxyModule { fun provideTransactionManager( appStateHolder: AppStateHolder, analytics: AnalyticsEventHandler, + cardSdkConfigRepository: CardSdkConfigRepository, ): TransactionManager { - return TransactionManagerImpl(appStateHolder, analytics) + return TransactionManagerImpl( + appStateHolder = appStateHolder, + analytics = analytics, + cardSdkConfigRepository = cardSdkConfigRepository, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index e83ed33f3a..1b0f19c9fe 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -1,6 +1,7 @@ package com.tangem.tap.proxy.redux import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -15,5 +16,6 @@ sealed interface DaggerGraphAction : Action { val walletRouter: WalletRouter, val walletConnectInteractor: WalletConnectInteractor, val tokenDetailsRouter: TokenDetailsRouter, + val cardSdkConfigRepository: CardSdkConfigRepository, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index bdd590be4d..aec162a122 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -18,6 +18,7 @@ object DaggerGraphReducer { walletRouter = action.walletRouter, walletConnectInteractor = action.walletConnectInteractor, tokenDetailsRouter = action.tokenDetailsRouter, + cardSdkConfigRepository = action.cardSdkConfigRepository, ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 84409d4e36..228d3f014b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -2,7 +2,9 @@ package com.tangem.tap.proxy.redux import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -27,6 +29,8 @@ data class DaggerGraphState( val walletConnectInteractor: WalletConnectInteractor? = null, val tokenDetailsFeatureToggles: TokenDetailsFeatureToggles? = null, val tokenDetailsRouter: TokenDetailsRouter? = null, + val scanCardProcessor: ScanCardProcessor? = null, + val cardSdkConfigRepository: CardSdkConfigRepository? = null, ) : StateType { inline fun get(getDependency: DaggerGraphState.() -> T?): T { diff --git a/buildSrc/build/libs/buildSrc.jar b/buildSrc/build/libs/buildSrc.jar new file mode 100644 index 0000000000..270c7fdce9 Binary files /dev/null and b/buildSrc/build/libs/buildSrc.jar differ diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index e1fb463a41..29b4cfb591 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -13,5 +13,7 @@ dependencies { kapt(deps.hilt.kapt) /** Core shouldn't depends on core, but in case with utils and logging its necessary */ - implementation(project(":core:utils")) + implementation(projects.core.utils) + + implementation(projects.core.analytics.models) } \ No newline at end of file diff --git a/core/analytics/models/.gitignore b/core/analytics/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/analytics/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/analytics/models/build.gradle.kts b/core/analytics/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/core/analytics/models/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/AnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt similarity index 81% rename from core/analytics/src/main/java/com/tangem/core/analytics/AnalyticsEvent.kt rename to core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt index 1fbc89d977..e60b14d1ee 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/AnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.core.analytics +package com.tangem.core.analytics.models /** [REDACTED_AUTHOR] diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index 8dda5adedf..3c12cf80a8 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -1,18 +1,9 @@ package com.tangem.core.analytics -import com.tangem.core.analytics.api.AnalyticsEventFilter -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.api.AnalyticsFilterHolder -import com.tangem.core.analytics.api.AnalyticsHandler -import com.tangem.core.analytics.api.AnalyticsHandlerHolder -import com.tangem.core.analytics.api.ParamsInterceptor -import com.tangem.core.analytics.api.ParamsInterceptorHolder +import com.tangem.core.analytics.api.* +import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import kotlinx.coroutines.CoroutineName -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import java.util.concurrent.Executors /** diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt index 8381b2b983..4de7e4aee8 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventFilterApi.kt @@ -1,6 +1,6 @@ package com.tangem.core.analytics.api -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt index 49aadf88c3..0a5f3179de 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/EventHandlerApi.kt @@ -1,6 +1,6 @@ package com.tangem.core.analytics.api -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt index 683a1c45a0..80b0bf2b14 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/ParamsInterceptorApi.kt @@ -1,6 +1,6 @@ package com.tangem.core.analytics.api -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** [REDACTED_AUTHOR] diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index de4006b63a..aa45701df5 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.okHttp) - implementation(deps.okHttp.logging) + implementation(deps.okHttp.prettyLogging) implementation(deps.retrofit) implementation(deps.retrofit.moshi) implementation(deps.reactive.network) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt index 166036783c..3d8b6f3930 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt @@ -1,8 +1,10 @@ package com.tangem.datasource.api.common +import android.util.Log +import com.ihsanbal.logging.Level +import com.ihsanbal.logging.LoggingInterceptor import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import java.util.concurrent.TimeUnit @@ -22,7 +24,7 @@ fun createRetrofitInstance( } interceptors.forEach { okHttpBuilder.addInterceptor(it) } - if (logEnabled) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor()) + if (logEnabled) okHttpBuilder.addInterceptor(createNetworkLoggingInterceptor()) return Retrofit.Builder() .baseUrl(baseUrl) @@ -31,6 +33,9 @@ fun createRetrofitInstance( .build() } -private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY +fun createNetworkLoggingInterceptor(): Interceptor { + return LoggingInterceptor.Builder() + .setLevel(Level.BODY) + .log(Log.VERBOSE) + .build() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt new file mode 100644 index 0000000000..ae96179015 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt @@ -0,0 +1,20 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.cache.CacheKeysStore +import com.tangem.datasource.local.cache.RuntimeCacheKeysStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CacheKeysStoreModule { + + @Provides + @Singleton + fun provideCacheKeysStore(): CacheKeysStore { + return RuntimeCacheKeysStore() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index c8ae82faa9..9c11afc771 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -63,9 +63,7 @@ class NetworkModule { @PromotionOneInch fun providePromotionOneInchApi(authProvider: AuthProvider, @NetworkMoshi moshi: Moshi): PromotionApi { val okClient = OkHttpClient.Builder() - .addHeaders( - AuthenticationHeader(authProvider), - ) + .addHeaders(AuthenticationHeader(authProvider)) .allowLogging() .callTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS) .connectTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt new file mode 100644 index 0000000000..fc32a2b16e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.local.cache + +import com.tangem.datasource.local.cache.model.CacheKey + +interface CacheKeysStore { + + fun get(id: String): CacheKey? + + fun addOrReplace(key: CacheKey) + + fun remove(id: String) + + fun clear() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt new file mode 100644 index 0000000000..229b5ab30c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.local.cache + +import com.tangem.datasource.local.cache.model.CacheKey +import com.tangem.datasource.local.store.RuntimeStore + +internal class RuntimeCacheKeysStore : CacheKeysStore { + + private val store = RuntimeStore(keyProvider = CacheKey::id) + + override fun get(id: String): CacheKey? { + return store.getSync { it.id == id }.firstOrNull() + } + + override fun addOrReplace(key: CacheKey) { + store.addOrReplace(key) + } + + override fun remove(id: String) { + store.remove { it.id == id } + } + + override fun clear() { + store.clear() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt new file mode 100644 index 0000000000..733e61e2b0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.cache.model + +import org.joda.time.Duration +import org.joda.time.LocalDateTime + +data class CacheKey( + val id: String, + val updatedAt: LocalDateTime, + val expiresIn: Duration, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt new file mode 100644 index 0000000000..37a05f8d3e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/store/RuntimeStore.kt @@ -0,0 +1,59 @@ +package com.tangem.datasource.local.store + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update + +internal class RuntimeStore(private val keyProvider: (Data) -> Key) { + + private val store = MutableStateFlow>(hashMapOf()) + + fun get(selector: (Data) -> Boolean = { true }): Flow> { + return store.map { getInternal(it, selector) } + } + + fun getSync(selector: (Data) -> Boolean = { true }): List { + return getInternal(store.value, selector) + } + + // TODO: Uncomment if needed + // fun addOrReplace(items: Collection) { + // if (items.isEmpty()) return + // + // val storeValue = store.value + // + // items.forEach { item -> + // storeValue[keyProvider(item)] = item + // } + // + // store.value = storeValue + // } + + fun addOrReplace(item: Data) { + val storeValue = store.value + storeValue[keyProvider(item)] = item + + store.value = storeValue + } + + fun remove(selector: (Data) -> Boolean) { + val storeValue = store.value + + storeValue.forEach { (key, item) -> + if (selector(item)) { + storeValue.remove(key) + } + } + + store.value = storeValue + } + + fun clear() { + store.update { hashMapOf() } + } + + private fun getInternal(store: HashMap, selector: (Data) -> Boolean): List { + return store.values.filter { selector(it) } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt index 4fd16a0d1d..4a5a6a1b75 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/HttpClientExt.kt @@ -1,10 +1,9 @@ package com.tangem.datasource.utils import com.tangem.datasource.BuildConfig +import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import okhttp3.logging.HttpLoggingInterceptor.Level /** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeader): OkHttpClient.Builder { @@ -26,5 +25,10 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade * * @param level logging level. By default, only the request body. */ -internal fun OkHttpClient.Builder.allowLogging(level: Level = Level.BODY): OkHttpClient.Builder = - if (BuildConfig.DEBUG) addInterceptor(interceptor = HttpLoggingInterceptor().setLevel(level)) else this \ No newline at end of file +internal fun OkHttpClient.Builder.allowLogging(): OkHttpClient.Builder { + return if (BuildConfig.DEBUG) { + addInterceptor(interceptor = createNetworkLoggingInterceptor()) + } else { + this + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 11682ec1f1..385824799e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -230,7 +230,12 @@ private fun PrimaryButtonSample() { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { PrimaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { }) - PrimaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { }) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + showProgress = true, + text = "Manage tokens", + onClick = { }, + ) PrimaryButtonIconEnd( modifier = Modifier.fillMaxWidth(), text = "Manage tokens", @@ -289,7 +294,12 @@ private fun SecondaryButtonSample() { verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { SecondaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { }) - SecondaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { }) + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + showProgress = true, + text = "Manage tokens", + onClick = { }, + ) SecondaryButtonIconEnd( modifier = Modifier.fillMaxWidth(), text = "Manage tokens", diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt new file mode 100644 index 0000000000..a646f37f33 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Notifier.kt @@ -0,0 +1,68 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.res.TangemTheme + +/** + * [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=68%3A20&mode=design&t=WSV3AxC6zV1y0CHF-1) + * */ +@Composable +fun Notifier( + text: String, + modifier: Modifier = Modifier, + textColor: Color = TangemTheme.colors.text.primary1, + backgroundColor: Color = TangemTheme.colors.icon.inactive, +) { + Box( + modifier = modifier + .heightIn(TangemTheme.dimens.size32) + .background( + color = backgroundColor, + shape = TangemTheme.shapes.roundedCorners8, + ), + ) { + Text( + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = TangemTheme.dimens.size10), + text = text, + color = textColor, + style = TangemTheme.typography.button, + ) + } +} + +@Preview +@Composable +private fun TangemNotifierPreview_Light(@PreviewParameter(NotifierProvider::class) text: String) { + TangemTheme(isDark = false) { + Notifier(text = text) + } +} + +@Preview +@Composable +private fun TangemNotifierPreview_Dark(@PreviewParameter(NotifierProvider::class) text: String) { + TangemTheme(isDark = true) { + Notifier(text = text) + } +} + +private class NotifierProvider : CollectionPreviewParameterProvider( + collection = listOf( + "a", + "Card 1 of 2", + "Card 1 of 2 or many other", + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt new file mode 100644 index 0000000000..de7706a43d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -0,0 +1,84 @@ +package com.tangem.core.ui.components.buttons + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.actions.ActionButton +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun HorizontalActionChips( + buttons: ImmutableList, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens.spacing0), +) { + LazyRow( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + contentPadding = contentPadding, + ) { + items( + items = buttons, + key = { it.text }, + itemContent = { ActionButton(config = it) }, + ) + } +} + +@Preview +@Composable +private fun Preview_HorizontalActionChips_Light( + @PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList, +) { + TangemTheme(isDark = false) { + HorizontalActionChips(buttons = buttons) + } +} + +@Preview +@Composable +private fun Preview_HorizontalActionChips_Dark( + @PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList, +) { + TangemTheme(isDark = true) { + HorizontalActionChips(buttons = buttons) + } +} + +private class ActionButtonConfigProvider : CollectionPreviewParameterProvider( + collection = persistentListOf( + ActionButtonConfig( + text = "Buy", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Send", + iconResId = R.drawable.ic_arrow_up_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Receive", + iconResId = R.drawable.ic_arrow_down_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Exchange", + iconResId = R.drawable.ic_exchange_vertical_24, + onClick = {}, + ), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt index b0a5b59116..c995a22c0e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButton.kt @@ -30,7 +30,7 @@ internal fun TangemButton( elevation = elevation, shape = size.toShape(), colors = colors, - contentPadding = size.toContentPadding(icon = icon), + contentPadding = if (showProgress) ButtonDefaults.ContentPadding else size.toContentPadding(icon = icon), ) { ButtonContent( text = text, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletMarketplaceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt similarity index 74% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletMarketplaceBlock.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 7033b42c61..d7f217308c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletMarketplaceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency +package com.tangem.core.ui.components.marketprice import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -17,22 +17,17 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp +import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig -import com.tangem.feature.wallet.presentation.wallet.state.WalletMarketplaceBlockState /** - * Wallet marketplace block - * - * @param state state - * -[REDACTED_AUTHOR] + * @see Figma component */ @Composable -internal fun WalletMarketplaceBlock(state: WalletMarketplaceBlockState, modifier: Modifier = Modifier) { +fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) { var rootWidth by remember { mutableStateOf(value = 0) } Column( modifier = modifier @@ -54,12 +49,12 @@ internal fun WalletMarketplaceBlock(state: WalletMarketplaceBlockState, modifier ) when (state) { - is WalletMarketplaceBlockState.Loading -> { + is MarketPriceBlockState.Loading -> { RectangleShimmer( modifier = Modifier.size(width = TangemTheme.dimens.size158, height = TangemTheme.dimens.size20), ) } - is WalletMarketplaceBlockState.Content -> { + is MarketPriceBlockState.Content -> { Price( config = state, priceWidthDp = with(LocalDensity.current) { rootWidth.div(other = 2).toDp() }, @@ -70,7 +65,7 @@ internal fun WalletMarketplaceBlock(state: WalletMarketplaceBlockState, modifier } @Composable -private fun Price(config: WalletMarketplaceBlockState.Content, priceWidthDp: Dp) { +private fun Price(config: MarketPriceBlockState.Content, priceWidthDp: Dp) { Row( modifier = Modifier, verticalAlignment = Alignment.CenterVertically, @@ -124,35 +119,44 @@ private fun PriceChangeInPercent(config: PriceChangeConfig) { @Preview @Composable -private fun Preview_MarketplaceBlock_Light( - @PreviewParameter(WalletMarketplaceStateProvider::class) - state: WalletMarketplaceBlockState, +private fun Preview_MarketPriceBlock_Light( + @PreviewParameter(WalletMarketPriceBlockStateProvider::class) + state: MarketPriceBlockState, ) { TangemTheme(isDark = false) { - WalletMarketplaceBlock(state = state) + MarketPriceBlock(state = state) } } @Preview @Composable -private fun Preview_MarketplaceBlock_Dark( - @PreviewParameter(WalletMarketplaceStateProvider::class) - state: WalletMarketplaceBlockState, +private fun Preview_MarketPriceBlock_Dark( + @PreviewParameter(WalletMarketPriceBlockStateProvider::class) + state: MarketPriceBlockState, ) { TangemTheme(isDark = true) { - WalletMarketplaceBlock(state = state) + MarketPriceBlock(state = state) } } -private class WalletMarketplaceStateProvider : CollectionPreviewParameterProvider( +private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.marketplaceBlockContent, - WalletPreviewData.marketplaceBlockContent.copy( + MarketPriceBlockState.Content( + currencyName = "BTC", + price = "98900", priceChangeConfig = PriceChangeConfig( valueInPercent = "5.16%", type = PriceChangeConfig.Type.DOWN, ), ), - WalletMarketplaceBlockState.Loading(currencyName = "BTC"), + MarketPriceBlockState.Content( + currencyName = "BTC", + price = "98900", + priceChangeConfig = PriceChangeConfig( + valueInPercent = "10.89%", + type = PriceChangeConfig.Type.UP, + ), + ), + MarketPriceBlockState.Loading(currencyName = "BTC"), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt new file mode 100644 index 0000000000..ae59401aa9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlockState.kt @@ -0,0 +1,17 @@ +package com.tangem.core.ui.components.marketprice + +import androidx.compose.runtime.Immutable + +@Immutable +sealed interface MarketPriceBlockState { + + val currencyName: String + + data class Loading(override val currencyName: String) : MarketPriceBlockState + + data class Content( + override val currencyName: String, + val price: String, + val priceChangeConfig: PriceChangeConfig, + ) : MarketPriceBlockState +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt new file mode 100644 index 0000000000..60af2367e5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.core.ui.components.marketprice + +data class PriceChangeConfig(val valueInPercent: String, val type: Type) { + + /** Price changing type */ + enum class Type { + UP, DOWN + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 9d4f2cf480..76b7894881 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH2 +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme @@ -43,10 +45,10 @@ fun Notification(state: NotificationState, modifier: Modifier = Modifier) { ) .clickable( enabled = when (state) { - is NotificationState.Simple -> false - is NotificationState.Action -> true + is NotificationState.Clickable -> true + is NotificationState.Simple, is NotificationState.Closable -> false }, - onClick = if (state is NotificationState.Action) { + onClick = if (state is NotificationState.Clickable) { state.onClick } else { {} @@ -67,12 +69,23 @@ fun Notification(state: NotificationState, modifier: Modifier = Modifier) { ) NotificationInfoBlock( - title = state.title, - subtitle = state.subtitle, + title = state.title.resolveReference(), + subtitle = state.subtitle?.resolveReference(), modifier = Modifier.align(alignment = Alignment.CenterStart), ) - if (state is NotificationState.Action) { + if (state is NotificationState.Closable) { + Icon( + modifier = Modifier + .size(size = TangemTheme.dimens.size20) + .align(alignment = Alignment.TopEnd), + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } + + if (state is NotificationState.Clickable) { Icon( modifier = Modifier .size(size = TangemTheme.dimens.size20) @@ -149,30 +162,50 @@ private fun Preview_WarningNotification_Dark( private class NotificationStateProvider : CollectionPreviewParameterProvider( collection = listOf( NotificationState.Simple( - title = "Your wallet hasn’t been backed up", - subtitle = "Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit, sed do eiusmod tempor incididunt ut labore et...", + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + subtitle = TextReference.Str( + value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + + "ut labore et...", + ), iconResId = R.drawable.img_attention_20, ), NotificationState.Simple( - title = "Your wallet hasn’t been backed up", + title = TextReference.Str("Your wallet hasn’t been backed up"), subtitle = null, iconResId = R.drawable.ic_alert_circle_24, tint = TangemColorPalette.Amaranth, ), - NotificationState.Action( - title = "Your wallet hasn’t been backed up", - subtitle = "Lorem ipsum dolor sit amet, consectetur " + - "adipiscing elit, sed do eiusmod tempor incididunt ut labore et...", + NotificationState.Clickable( + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + subtitle = TextReference.Str( + value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + + "ut labore et...", + ), iconResId = R.drawable.img_attention_20, onClick = {}, ), - NotificationState.Action( - title = "Your wallet hasn’t been backed up", + NotificationState.Clickable( + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), subtitle = null, iconResId = R.drawable.ic_alert_circle_24, tint = TangemColorPalette.Amaranth, onClick = {}, ), + NotificationState.Closable( + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + subtitle = TextReference.Str( + value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " + + "ut labore et...", + ), + iconResId = R.drawable.img_attention_20, + onCloseClick = {}, + ), + NotificationState.Closable( + title = TextReference.Str(value = "Your wallet hasn’t been backed up"), + subtitle = null, + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + onCloseClick = {}, + ), ), ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt index 065fa1e125..f787e62a66 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationState.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.components.notifications import androidx.annotation.DrawableRes import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.TextReference /** * Notification component state @@ -14,8 +15,8 @@ import androidx.compose.ui.graphics.Color [REDACTED_AUTHOR] */ sealed class NotificationState( - open val title: String, - open val subtitle: String? = null, + open val title: TextReference, + open val subtitle: TextReference? = null, @DrawableRes open val iconResId: Int, open val tint: Color? = null, ) { @@ -29,8 +30,8 @@ sealed class NotificationState( * @property tint icon tint */ data class Simple( - override val title: String, - override val subtitle: String? = null, + override val title: TextReference, + override val subtitle: TextReference? = null, @DrawableRes override val iconResId: Int, override val tint: Color? = null, ) : NotificationState(title, subtitle, iconResId, tint) @@ -42,13 +43,30 @@ sealed class NotificationState( * @property subtitle subtitle * @property iconResId icon resource id * @property tint icon tint - * @param onClick lambda be invoked when notification component is clicked + * @property onClick lambda be invoked when notification component is clicked */ - data class Action( - override val title: String, - override val subtitle: String? = null, + data class Clickable( + override val title: TextReference, + override val subtitle: TextReference? = null, @DrawableRes override val iconResId: Int, override val tint: Color? = null, val onClick: () -> Unit, ) : NotificationState(title, subtitle, iconResId, tint) + + /** + * Closable notification state + * + * @property title title + * @property subtitle subtitle + * @property iconResId icon resource id + * @property tint icon tint + * @property onCloseClick lambda be invoked when close button is clicked + */ + data class Closable( + override val title: TextReference, + override val subtitle: TextReference? = null, + @DrawableRes override val iconResId: Int, + override val tint: Color? = null, + val onCloseClick: (() -> Unit)? = null, + ) : NotificationState(title, subtitle, iconResId, tint) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index c3f075ffb0..8e17a060f2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -43,6 +43,7 @@ data class TangemDimens internal constructor( val size5: Dp = 5.dp, val size7: Dp = 7.dp, val size8: Dp = 8.dp, + val size10: Dp = 10.dp, val size11: Dp = 11.dp, val size12: Dp = 12.dp, val size16: Dp = 16.dp, @@ -101,6 +102,7 @@ data class TangemDimens internal constructor( val spacing34: Dp = 34.dp, val spacing36: Dp = 34.dp, val spacing38: Dp = 38.dp, + val spacing40: Dp = 40.dp, val spacing44: Dp = 44.dp, val spacing50: Dp = 50.dp, val spacing52: Dp = 52.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt index b452e38004..a3802221fb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Shape data class TangemShapes internal constructor( val roundedCornersSmall: Shape, val roundedCornersSmall2: Shape, + val roundedCorners8: Shape, val roundedCornersMedium: Shape, val roundedCornersXMedium: Shape, val roundedCornersLarge: Shape, @@ -16,6 +17,7 @@ data class TangemShapes internal constructor( constructor(dimens: TangemDimens) : this( roundedCornersSmall = RoundedCornerShape(size = dimens.radius2), roundedCornersSmall2 = RoundedCornerShape(size = dimens.radius4), + roundedCorners8 = RoundedCornerShape(size = dimens.radius8), roundedCornersMedium = RoundedCornerShape(size = dimens.radius12), roundedCornersXMedium = RoundedCornerShape(size = dimens.radius16), roundedCornersLarge = RoundedCornerShape(size = dimens.radius28), diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt new file mode 100644 index 0000000000..2a78274eb5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -0,0 +1,73 @@ +package com.tangem.core.ui.utils + +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Currency +import java.util.Locale + +object BigDecimalFormatter { + + const val EMPTY_BALANCE_SIGN = "—" + + private const val TEMP_CURRENCY_CODE = "USD" + + fun formatCryptoAmount( + cryptoAmount: BigDecimal, + cryptoCurrency: String, + decimals: Int, + locale: Locale = Locale.getDefault(), + ): String { + val formatterCurrency = getCurrency(cryptoCurrency) + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + roundingMode = RoundingMode.DOWN + } + + return formatter.format(cryptoAmount) + .replace(formatterCurrency.getSymbol(locale), cryptoCurrency) + } + + fun formatFiatAmount( + fiatAmount: BigDecimal, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), + ): String { + val formatterCurrency = getCurrency(fiatCurrencyCode) + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = 2 + minimumFractionDigits = 2 + roundingMode = RoundingMode.HALF_UP + } + + return formatter.format(fiatAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } + + fun formatPercent(percent: BigDecimal, useAbsoluteValue: Boolean, locale: Locale = Locale.getDefault()): String { + val formatter = NumberFormat.getPercentInstance(locale).apply { + maximumFractionDigits = 2 + minimumFractionDigits = 2 + roundingMode = RoundingMode.HALF_UP + } + val value = if (useAbsoluteValue) percent.abs() else percent + + return formatter.format(value) + } + + private fun getCurrency(code: String): Currency { + return runCatching { Currency.getInstance(code) } + .getOrElse { e -> + // Currency code is not valid ISO 4217 code + if (e is IllegalArgumentException) { + Currency.getInstance(TEMP_CURRENCY_CODE) + } else { + throw e + } + } + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt index 6f28c0350e..f15bce01ea 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt @@ -9,18 +9,21 @@ import javax.inject.Inject interface CoroutineDispatcherProvider { val main: CoroutineDispatcher val io: CoroutineDispatcher + val default: CoroutineDispatcher val single: CoroutineDispatcher } class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider { override val main: CoroutineDispatcher = Dispatchers.Main override val io: CoroutineDispatcher = Dispatchers.IO + override val default: CoroutineDispatcher = Dispatchers.Default override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() } class TestingCoroutineDispatcherProvider( override val main: CoroutineDispatcher = Dispatchers.Unconfined, override val io: CoroutineDispatcher = Dispatchers.Unconfined, + override val default: CoroutineDispatcher = Dispatchers.Unconfined, override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(), ) : CoroutineDispatcherProvider diff --git a/data/card/.gitignore b/data/card/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/card/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts new file mode 100644 index 0000000000..26b3010f2e --- /dev/null +++ b/data/card/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.card" +} + +dependencies { + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + implementation(deps.tangem.card.android) + implementation(deps.tangem.card.core) + + implementation(projects.data.source.preferences) + implementation(projects.domain.card) + implementation(projects.domain.models) +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt new file mode 100644 index 0000000000..321a48b0bf --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardSdkConfigRepository.kt @@ -0,0 +1,58 @@ +package com.tangem.data.card + +import com.tangem.TangemSdk +import com.tangem.common.UserCodeType +import com.tangem.common.core.CardIdDisplayFormat +import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.models.scan.ProductType + +/** + * Implementation of repository for managing of CardSDK config + * + * @property cardSdkProvider CardSDK instance provider + * @property preferencesDataSource application shared preferences + * +[REDACTED_AUTHOR] + */ +internal class DefaultCardSdkConfigRepository( + private val cardSdkProvider: CardSdkProvider, + private val preferencesDataSource: PreferencesDataSource, +) : CardSdkConfigRepository { + + @Deprecated("Use CardSdkConfigRepository's methods instead of this property") + override val sdk: TangemSdk + get() = cardSdkProvider.sdk + + override fun setAccessCodeRequestPolicy(isBiometricsRequestPolicy: Boolean) { + sdk.config.userCodeRequestPolicy = if (isBiometricsRequestPolicy) { + UserCodeRequestPolicy.AlwaysWithBiometrics(codeType = UserCodeType.AccessCode) + } else { + UserCodeRequestPolicy.Default + } + } + + override fun isBiometricsRequestPolicy(): Boolean { + return with(sdk.config.userCodeRequestPolicy) { + this is UserCodeRequestPolicy.AlwaysWithBiometrics && codeType == UserCodeType.AccessCode + } + } + + override fun resetCardIdDisplayFormat() { + sdk.config.cardIdDisplayFormat = CardIdDisplayFormat.Full + } + + override fun updateCardIdDisplayFormat(productType: ProductType) { + sdk.config.cardIdDisplayFormat = when (productType) { + ProductType.Twins -> CardIdDisplayFormat.LastLuhn(numbers = 4) + ProductType.Note, + ProductType.Wallet, + ProductType.Start2Coin, + -> CardIdDisplayFormat.Full + } + } + + override fun isAccessCodeSavingEnabled(): Boolean = preferencesDataSource.shouldSaveAccessCodes +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt new file mode 100644 index 0000000000..11f63f4a31 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt @@ -0,0 +1,28 @@ +package com.tangem.data.card.di + +import com.tangem.data.card.DefaultCardSdkConfigRepository +import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.card.repository.CardSdkConfigRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CardDataModule { + + @Provides + @Singleton + fun provideCardSdkConfigRepository( + cardSdkProvider: CardSdkProvider, + preferencesDataSource: PreferencesDataSource, + ): CardSdkConfigRepository { + return DefaultCardSdkConfigRepository( + cardSdkProvider = cardSdkProvider, + preferencesDataSource = preferencesDataSource, + ) + } +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt b/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt new file mode 100644 index 0000000000..c88f2dbfc2 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/di/CardSdkModule.kt @@ -0,0 +1,23 @@ +package com.tangem.data.card.di + +import com.tangem.data.card.sdk.CardSdkLifecycleObserver +import com.tangem.data.card.sdk.CardSdkProvider +import com.tangem.data.card.sdk.DefaultCardSdkProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface CardSdkModule { + + @Binds + @Singleton + fun provideCardSdkProvider(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkProvider + + @Binds + @Singleton + fun providerCardSdkLifecycleObserver(defaultCardSdkProvider: DefaultCardSdkProvider): CardSdkLifecycleObserver +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt new file mode 100644 index 0000000000..f5f6781392 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkLifecycleObserver.kt @@ -0,0 +1,17 @@ +package com.tangem.data.card.sdk + +import android.content.Context + +/** + * Lifecycle observer for creating Card SDK instance + * +[REDACTED_AUTHOR] + */ +interface CardSdkLifecycleObserver { + + /** Callback of creating activity [context] */ + fun onCreate(context: Context) + + /** Callback of destroying activity */ + fun onDestroy() +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt new file mode 100644 index 0000000000..0d0b88fa41 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/sdk/CardSdkProvider.kt @@ -0,0 +1,14 @@ +package com.tangem.data.card.sdk + +import com.tangem.TangemSdk + +/** + * CardSDK instance provider + * +[REDACTED_AUTHOR] + */ +internal interface CardSdkProvider { + + /** CardSDK instance */ + val sdk: TangemSdk +} \ No newline at end of file diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt new file mode 100644 index 0000000000..dddff7e3da --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -0,0 +1,45 @@ +package com.tangem.data.card.sdk + +import android.content.Context +import androidx.fragment.app.FragmentActivity +import com.tangem.TangemSdk +import com.tangem.common.CardFilter +import com.tangem.common.card.FirmwareVersion +import com.tangem.common.core.Config +import com.tangem.sdk.extensions.initWithBiometrics +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Implementation of CardSDK instance provider + * +[REDACTED_AUTHOR] + */ +@Singleton +internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, CardSdkLifecycleObserver { + + override val sdk: TangemSdk + get() = requireNotNull(value = _sdk) { "Impossible to get the TangemSdk when activity is destroyed" } + + private var _sdk: TangemSdk? = null + + override fun onCreate(context: Context) { + _sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config) + } + + override fun onDestroy() { + _sdk = null + } + + private companion object { + + val config = Config( + linkedTerminal = true, + allowUntrustedCards = true, + filter = CardFilter( + allowedCardTypes = FirmwareVersion.FirmwareType.values().toList(), + maxFirmwareVersion = FirmwareVersion(major = 4, minor = 52), + ), + ) + } +} \ No newline at end of file diff --git a/data/common/.gitignore b/data/common/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/common/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts new file mode 100644 index 0000000000..7ddd807653 --- /dev/null +++ b/data/common/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +dependencies { + implementation(projects.core.datasource) + + implementation(deps.kotlin.coroutines) + implementation(deps.jodatime) + + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/common/src/main/AndroidManifest.xml b/data/common/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5a762109a1 --- /dev/null +++ b/data/common/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt new file mode 100644 index 0000000000..fb698486ad --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/CacheRegistry.kt @@ -0,0 +1,52 @@ +package com.tangem.data.common.cache + +import org.joda.time.Duration + +/** + * Represents a registry for managing cache. + */ +interface CacheRegistry { + + /** + * Checks whether the cache key is expired. + * + * @param key cache key. + * @return `true` if the cache key is expired, `false` otherwise. + */ + suspend fun isExpired(key: String): Boolean + + /** + * Invalidates the cache key in registry. + * + * If the key doesn't exist, or it's already invalidated, this method doesn't have any effect. + * + * @param key cache key. + */ + suspend fun invalidate(key: String) + + /** + * Invalidates all cache keys in the registry. + * + * After the call, the registry doesn't contain any valid keys. + */ + suspend fun invalidateAll() + + /** + * Defines a callback to be invoked when the cache key expires. + * + * @param key cache key. + * @param skipCache if `true`, the callback will be invoked regardless of whether the key has expired or not. + * @param expireIn the duration after which the cache key is considered expired. + * @param block the block of code to be executed when the cache key expires. + */ + suspend fun invokeOnExpire( + key: String, + skipCache: Boolean, + expireIn: Duration = Duration.standardMinutes(DEFAULT_CACHE_KEY_EXPIRE_IN_MINUTES), + block: suspend () -> Unit, + ) + + private companion object { + const val DEFAULT_CACHE_KEY_EXPIRE_IN_MINUTES = 5L + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt new file mode 100644 index 0000000000..1aabdd2e77 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -0,0 +1,52 @@ +package com.tangem.data.common.cache + +import com.tangem.datasource.local.cache.CacheKeysStore +import com.tangem.datasource.local.cache.model.CacheKey +import org.joda.time.Duration +import org.joda.time.LocalDateTime + +internal class DefaultCacheRegistry( + private val cacheKeysStore: CacheKeysStore, +) : CacheRegistry { + + override suspend fun isExpired(key: String): Boolean { + val cacheKey = cacheKeysStore.get(key) ?: return true + + return cacheKey.updatedAt + .plus(cacheKey.expiresIn) + .isBefore(LocalDateTime.now()) + } + + override suspend fun invalidate(key: String) { + cacheKeysStore.remove(key) + } + + override suspend fun invalidateAll() { + cacheKeysStore.clear() + } + + override suspend fun invokeOnExpire( + key: String, + skipCache: Boolean, + expireIn: Duration, + block: suspend () -> Unit, + ) { + val isExpired = isExpired(key) || skipCache + if (!isExpired) return + + cacheKeysStore.addOrReplace( + key = CacheKey( + id = key, + updatedAt = LocalDateTime.now(), + expiresIn = expireIn, + ), + ) + + try { + block() + } catch (e: Throwable) { + invalidate(key) + throw e + } + } +} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt new file mode 100644 index 0000000000..84359cb2e6 --- /dev/null +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/di/CacheRegistryModule.kt @@ -0,0 +1,21 @@ +package com.tangem.data.common.cache.di + +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.common.cache.DefaultCacheRegistry +import com.tangem.datasource.local.cache.CacheKeysStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object CacheRegistryModule { + + @Provides + @Singleton + fun provideCacheRegistry(cacheKeysStore: CacheKeysStore): CacheRegistry { + return DefaultCacheRegistry(cacheKeysStore) + } +} \ No newline at end of file diff --git a/data/tokens/.gitignore b/data/tokens/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/tokens/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts new file mode 100644 index 0000000000..2d8aaaf35d --- /dev/null +++ b/data/tokens/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +dependencies { + implementation(projects.domain.core) + implementation(projects.domain.tokens) + implementation(projects.domain.wallets.models) + + implementation(deps.kotlin.coroutines) + + implementation(deps.arrow.core) + + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt new file mode 100644 index 0000000000..85b6f06d93 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -0,0 +1,30 @@ +package com.tangem.data.tokens.di + +import com.tangem.data.tokens.repository.MockNetworksRepository +import com.tangem.data.tokens.repository.MockQuotesRepository +import com.tangem.data.tokens.repository.MockTokensRepository +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TokensDataModule { + + @Provides + @Singleton + fun provideTokensRepository(): TokensRepository = MockTokensRepository() + + @Provides + @Singleton + fun provideQuotesRepository(): QuotesRepository = MockQuotesRepository() + + @Provides + @Singleton + fun provideNetworksRepository(): NetworksRepository = MockNetworksRepository() +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt new file mode 100644 index 0000000000..af85a6322b --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt @@ -0,0 +1,56 @@ +package com.tangem.data.tokens.mock + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockNetworks { + + val network1 = Network( + id = Network.ID("network1"), + name = "Network One", + ) + + val network2 = Network( + id = Network.ID("network2"), + name = "Network Two", + ) + + val network3 = Network( + id = Network.ID("network3"), + name = "Network Three", + ) + + val networks = setOf(network1, network2, network3) + + val networkStatus1 = NetworkStatus( + networkId = network1.id, + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token1.id to BigDecimal("123.1234556789"), + MockTokens.token2.id to BigDecimal("42.2"), + MockTokens.token3.id to BigDecimal("1000000000.5"), + ), + ), + ) + + val networkStatus2 = NetworkStatus( + networkId = network2.id, + value = NetworkStatus.MissedDerivation, + ) + + val networkStatus3 = NetworkStatus( + networkId = network3.id, + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token7.id to BigDecimal.ZERO, + MockTokens.token8.id to BigDecimal.TEN, + MockTokens.token9.id to BigDecimal.TEN, + MockTokens.token10.id to BigDecimal.TEN, + ), + ), + ) + + val networksStatuses = setOf(networkStatus1, networkStatus2, networkStatus3) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt new file mode 100644 index 0000000000..86f7a6d76c --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockQuotes.kt @@ -0,0 +1,70 @@ +package com.tangem.data.tokens.mock + +import com.tangem.domain.tokens.model.Quote +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockQuotes { + + val quote1 = Quote( + tokenId = MockTokens.token1.id, + fiatRate = BigDecimal("1.23"), + priceChange = BigDecimal("0.01"), + ) + + val quote2 = Quote( + tokenId = MockTokens.token2.id, + fiatRate = BigDecimal("2.34"), + priceChange = BigDecimal("-0.02"), + ) + + val quote3 = Quote( + tokenId = MockTokens.token3.id, + fiatRate = BigDecimal("3.45"), + priceChange = BigDecimal("0.03"), + ) + + val quote4 = Quote( + tokenId = MockTokens.token4.id, + fiatRate = BigDecimal("4.56"), + priceChange = BigDecimal("-0.04"), + ) + + val quote5 = Quote( + tokenId = MockTokens.token5.id, + fiatRate = BigDecimal("5.67"), + priceChange = BigDecimal("0.05"), + ) + + val quote6 = Quote( + tokenId = MockTokens.token6.id, + fiatRate = BigDecimal("6.78"), + priceChange = BigDecimal("-0.06"), + ) + + val quote7 = Quote( + tokenId = MockTokens.token7.id, + fiatRate = BigDecimal("7.89"), + priceChange = BigDecimal("0.07"), + ) + + val quote8 = Quote( + tokenId = MockTokens.token8.id, + fiatRate = BigDecimal("8.90"), + priceChange = BigDecimal("-0.08"), + ) + + val quote9 = Quote( + tokenId = MockTokens.token9.id, + fiatRate = BigDecimal("9.01"), + priceChange = BigDecimal("0.09"), + ) + + val quote10 = Quote( + tokenId = MockTokens.token10.id, + fiatRate = BigDecimal("10.12"), + priceChange = BigDecimal("-0.10"), + ) + + val quotes = setOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt new file mode 100644 index 0000000000..4261727cff --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt @@ -0,0 +1,132 @@ +package com.tangem.data.tokens.mock + +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId + +internal object MockTokens { + + val token1 get() = Token( + id = Token.ID("token1"), + networkId = MockNetworks.network1.id, + name = "Token 1", + symbol = "T1", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + ) + val token2 get() = Token( + id = Token.ID("token2"), + networkId = MockNetworks.network1.id, + name = "Token 2", + symbol = "T2", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token3 get() = Token( + id = Token.ID("token3"), + networkId = MockNetworks.network1.id, + name = "Token 3", + symbol = "T3", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token4 get() = Token( + id = Token.ID("token4"), + networkId = MockNetworks.network2.id, + name = "Token 4", + symbol = "T4", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + ) + val token5 get() = Token( + id = Token.ID("token5"), + networkId = MockNetworks.network2.id, + name = "Token 5", + symbol = "T5", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token6 get() = Token( + id = Token.ID("token6"), + networkId = MockNetworks.network2.id, + name = "Token 6", + symbol = "T6", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token7 get() = Token( + id = Token.ID("token7"), + networkId = MockNetworks.network3.id, + name = "Token 7", + symbol = "T7", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + ) + val token8 get() = Token( + id = Token.ID("token8"), + networkId = MockNetworks.network3.id, + name = "Token 8", + symbol = "T8", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token9 get() = Token( + id = Token.ID("token9"), + networkId = MockNetworks.network3.id, + name = "Token 9", + symbol = "T9", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token10 get() = Token( + id = Token.ID("token10"), + networkId = MockNetworks.network3.id, + name = "Token 10", + symbol = "T10", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + + val tokens get() = mapOf( + UserWalletId(stringValue = "123") to setOf( + token1, token2, token3, token4, token5, + token6, token7, token8, token9, token10, + ), + UserWalletId(stringValue = "321") to setOf(token1, token2, token3), + UserWalletId(stringValue = "42") to setOf(token7, token8, token9, token10), + UserWalletId(stringValue = "24") to setOf(token4, token5, token6), + ) + + val isGrouped get() = mapOf( + UserWalletId(stringValue = "123") to true, + UserWalletId(stringValue = "321") to false, + UserWalletId(stringValue = "42") to false, + UserWalletId(stringValue = "24") to true, + ) + + val isSortedByBalance get() = mapOf( + UserWalletId(stringValue = "123") to true, + UserWalletId(stringValue = "321") to false, + UserWalletId(stringValue = "42") to true, + UserWalletId(stringValue = "24") to false, + ) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt new file mode 100644 index 0000000000..26bc58aa70 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockNetworksRepository.kt @@ -0,0 +1,31 @@ +package com.tangem.data.tokens.repository + +import com.tangem.data.tokens.mock.MockNetworks +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +internal class MockNetworksRepository : NetworksRepository { + + override fun getNetworks(networksIds: Set): Set { + return MockNetworks.networks + .filter { it.id in networksIds } + .toSet() + } + + override fun getNetworkStatuses( + userWalletId: UserWalletId, + networks: Map>, + refresh: Boolean, + ): Flow> { + return flowOf( + MockNetworks.networksStatuses + .filter { it.networkId in networks.keys } + .toSet(), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt new file mode 100644 index 0000000000..2510893558 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockQuotesRepository.kt @@ -0,0 +1,19 @@ +package com.tangem.data.tokens.repository + +import com.tangem.data.tokens.mock.MockQuotes +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.QuotesRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +internal class MockQuotesRepository : QuotesRepository { + + override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { + return flowOf( + MockQuotes.quotes + .filter { it.tokenId in tokensIds } + .toSet(), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt new file mode 100644 index 0000000000..e6b7760a7c --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/MockTokensRepository.kt @@ -0,0 +1,30 @@ +package com.tangem.data.tokens.repository + +import com.tangem.data.tokens.mock.MockTokens +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +internal class MockTokensRepository : TokensRepository { + + override suspend fun sortTokens( + userWalletId: UserWalletId, + sortedTokensIds: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ) = Unit + + override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + return flowOf(value = MockTokens.tokens[userWalletId]!!) + } + + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { + return flowOf(value = MockTokens.isGrouped[userWalletId]!!) + } + + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { + return flowOf(value = MockTokens.isSortedByBalance[userWalletId]!!) + } +} \ No newline at end of file diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index 213bd024b6..a1266a8ba6 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -4,9 +4,11 @@ plugins { } dependencies { - implementation(project(":domain:core")) + implementation(projects.domain.core) // TODO: Remove after new card scan result was implemented - implementation(project(":domain:models")) + implementation(projects.domain.models) + + implementation(projects.core.analytics.models) implementation(deps.tangem.card.core) } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt new file mode 100644 index 0000000000..538ac0f240 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetAccessCodeSavingStatusUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.CardSdkConfigRepository + +/** + * Use case for getting access code saving status + * + * @property cardSdkConfigRepository repository for managing of CardSDK config + * +[REDACTED_AUTHOR] + */ +class GetAccessCodeSavingStatusUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { + + operator fun invoke(): Boolean = cardSdkConfigRepository.isAccessCodeSavingEnabled() +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/GetBiometricsStatusUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetBiometricsStatusUseCase.kt new file mode 100644 index 0000000000..a917d6f691 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetBiometricsStatusUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.CardSdkConfigRepository + +/** + * Check if config has biometrics request policy + * + * @property cardSdkConfigRepository repository for managing of CardSDK config + * +[REDACTED_AUTHOR] + */ +class GetBiometricsStatusUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { + + operator fun invoke(): Boolean = cardSdkConfigRepository.isBiometricsRequestPolicy() +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt new file mode 100644 index 0000000000..961d9acd07 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.card + +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemError +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.models.scan.ScanResponse + +interface ScanCardProcessor { + + suspend fun scan( + cardId: String? = null, + allowsRequestAccessCodeFromRepository: Boolean = false, + ): CompletionResult + + suspend fun scan( + analyticsEvent: AnalyticsEvent? = null, + cardId: String? = null, + onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, + onScanStateChange: suspend (scanInProgress: Boolean) -> Unit = {}, + onWalletNotCreated: suspend () -> Unit = {}, + disclaimerWillShow: () -> Unit = {}, + onFailure: suspend (error: TangemError) -> Unit = {}, + onSuccess: suspend (scanResponse: ScanResponse) -> Unit = {}, + ) +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt index 46312c439f..69dbc7a617 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt @@ -4,47 +4,46 @@ import arrow.core.Either import arrow.core.EitherNel import arrow.core.flatMap import com.tangem.TangemSdk -import com.tangem.common.core.CardIdDisplayFormat +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.core.chain.Chain import com.tangem.domain.core.chain.ChainProcessor -import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse /** - * Use case responsible for scanning a card and returning a [ScanResponse] object. - * @property scanCardRepository A repository object implementing [ScanCardRepository] interface. - * @property tangemSdk An instance of [TangemSdk] to configure the display format of the card ID. - * @constructor Create a new instance of [ScanCardUseCase] with the given dependencies. + * Use case responsible for scanning a card and returning a [ScanResponse] object + * + * @property scanCardRepository a repository object implementing [ScanCardRepository] interface + * @property cardSdkConfigRepository an instance of [TangemSdk] to configure the display format of the card ID + * + * @constructor create a new instance of [ScanCardUseCase] with the given dependencies */ class ScanCardUseCase( private val scanCardRepository: ScanCardRepository, - private val tangemSdk: TangemSdk, + private val cardSdkConfigRepository: CardSdkConfigRepository, ) { - /** - * A [ChainProcessor] object to launch the after-scan chains. - */ + /** A [ChainProcessor] object to launch the after-scan chains */ private val scanChainProcessor by lazy { ChainProcessor() } /** * Scan a card and return a [ScanResponse] object. - * @param cardId an optional card ID to scan. If null, can scan any card present. - * Defaults to null. - * @param allowRequestAccessCodeFromStorage whether to prompt the user for an access code if needed. - * Defaults to false. - * @param afterScanChains A list of chains that should be executed after a successful card scan operation. - * Defaults to an empty array. - * @return A [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse]. + * + * @param cardId an optional card ID to scan. If null, can scan any card present + * @param allowRequestAccessCodeFromStorage whether to prompt the user for an access code if needed + * @param afterScanChains a list of chains that should be executed after a successful card scan + * operation. Defaults to an empty array + * + * @return a [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse] */ suspend operator fun invoke( cardId: String? = null, allowRequestAccessCodeFromStorage: Boolean = false, afterScanChains: List> = emptyList(), ): Either { - resetCardIdDisplayFormat() + cardSdkConfigRepository.resetCardIdDisplayFormat() scanChainProcessor.addChains(afterScanChains) return scanCardRepository.scanCard( @@ -52,31 +51,10 @@ class ScanCardUseCase( allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage, ) .onRight { scanResponse -> - updateCardIdDisplayFormat(scanResponse.productType) + cardSdkConfigRepository.updateCardIdDisplayFormat(scanResponse.productType) } .flatMap { response -> scanChainProcessor.launchChains(initial = response) } } - - /** - * Reset the card ID display format to [CardIdDisplayFormat.Full]. - */ - private fun resetCardIdDisplayFormat() { - tangemSdk.config.cardIdDisplayFormat = CardIdDisplayFormat.Full - } - - /** - * Update the card ID display format according to the [ProductType] of the scanned card. - * @param productType The [ProductType] of the scanned card. - */ - private fun updateCardIdDisplayFormat(productType: ProductType) { - tangemSdk.config.cardIdDisplayFormat = when (productType) { - ProductType.Twins -> CardIdDisplayFormat.LastLuhn(numbers = 4) - ProductType.Note, - ProductType.Wallet, - ProductType.Start2Coin, - -> CardIdDisplayFormat.Full - } - } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt new file mode 100644 index 0000000000..e10f624726 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/SetAccessCodeRequestPolicyUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.CardSdkConfigRepository + +/** + * Set access code request policy by 'isBiometricsRequestPolicy' + * + * @property cardSdkConfigRepository repository for managing of CardSDK config + * +[REDACTED_AUTHOR] + */ +class SetAccessCodeRequestPolicyUseCase(private val cardSdkConfigRepository: CardSdkConfigRepository) { + + operator fun invoke(isBiometricsRequestPolicy: Boolean) { + cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy) + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt new file mode 100644 index 0000000000..1190a2952d --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardSdkConfigRepository.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.card.repository + +import com.tangem.TangemSdk +import com.tangem.domain.models.scan.ProductType + +/** + * Repository for managing with CardSDK config + * +[REDACTED_AUTHOR] + */ +interface CardSdkConfigRepository { + + /** Tangem SDK instance */ + @Deprecated("Use CardSdkConfigRepository's methods instead of this property") + val sdk: TangemSdk + + /** Set access code request policy by [isBiometricsRequestPolicy] */ + fun setAccessCodeRequestPolicy(isBiometricsRequestPolicy: Boolean) + + /** Check if config has biometrics request policy */ + fun isBiometricsRequestPolicy(): Boolean + + /** Reset the card ID display format to start value */ + fun resetCardIdDisplayFormat() + + /** Update the card ID display format according to the [productType] of the scanned card */ + fun updateCardIdDisplayFormat(productType: ProductType) + + /** Check if access code saving is enabled */ + fun isAccessCodeSavingEnabled(): Boolean +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt new file mode 100644 index 0000000000..11567ca43b --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/error/DataError.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.core.error + +sealed class DataError : Exception() { + + sealed class NetworkError : DataError() { + + object NoInternetConnection : NetworkError() + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt new file mode 100644 index 0000000000..ffde660eb6 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/raise/DelegatedRaise.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.core.raise + +import arrow.core.raise.Raise + +abstract class DelegatedRaise( + private val otherRaise: Raise, + private val transformError: (Error) -> OtherError, +) : Raise { + + override fun raise(r: Error): Nothing { + otherRaise.raise(transformError(r)) + } +} \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index f618e11fd9..525d2c8504 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(project(":common")) implementation(project(":libs:auth")) implementation(project(":domain:models")) + implementation(projects.domain.wallets.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) { @@ -22,12 +23,8 @@ dependencies { /** Other libraries */ implementation(deps.reKotlin) - //TODO: refactoring: remove it when all network services moved to the datasource module - implementation(deps.retrofit) - implementation(deps.retrofit.moshi) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.okHttp.logging) implementation(deps.timber) implementation(deps.kotlin.coroutines) diff --git a/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt new file mode 100644 index 0000000000..d788aed532 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/Artwork.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.userwallets + +data class Artwork(val artworkId: String) { + + companion object { + const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png" + const val SERGIO_CARD_URL = "https://app.tangem.com/cards/card_tg059.png" + const val MARTA_CARD_URL = "https://app.tangem.com/cards/card_tg083.png" + const val SERGIO_CARD_ID = "BC01" + const val MARTA_CARD_ID = "BC02" + const val TWIN_CARD_1 = "https://app.tangem.com/cards/card_tg085.png" + const val TWIN_CARD_2 = "https://app.tangem.com/cards/card_tg086.png" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/GetCardImageUseCase.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt similarity index 95% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/GetCardImageUseCase.kt rename to domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt index 9d218c840f..afa5eaaa37 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/GetCardImageUseCase.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/GetCardImageUseCase.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.userWalletList +package com.tangem.domain.userwallets import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result @@ -6,7 +6,6 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinsHelper import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.attestation.TangemApi -import com.tangem.tap.features.wallet.redux.Artwork /** * Use case for getting card image url diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt similarity index 88% rename from app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt rename to domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt index 7fa72a04fa..565930ae9d 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletBuilder.kt @@ -1,12 +1,10 @@ -package com.tangem.tap.domain.model.builders +package com.tangem.domain.userwallets -import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet -import com.tangem.tap.domain.userWalletList.GetCardImageUseCase class UserWalletBuilder( private val scanResponse: ScanResponse, @@ -24,7 +22,7 @@ class UserWalletBuilder( ProductType.Start2Coin -> "Start2Coin" ProductType.Wallet -> when { card.isBackupNotAllowed -> "Tangem card" - card.isStart2Coin -> "Start2Coin" + cardTypesResolver.isStart2Coin() -> "Start2Coin" else -> "Wallet" } } diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt rename to domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt index 30b4ef4407..cad0957152 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/userwallets/UserWalletIdBuilder.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.model.builders +package com.tangem.domain.userwallets import com.tangem.common.extensions.calculateSha256 import com.tangem.common.extensions.hexToBytes diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index 7578791134..a89909b584 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -304,7 +304,7 @@ data class CardDTO( object NoBackup : BackupStatus() val isActive: Boolean - get() = this is Active || this is CardLinked + get() = this is Active companion object { internal fun fromSdkStatus(sdkStatus: Card.BackupStatus?): BackupStatus? { diff --git a/domain/tokens/.gitignore b/domain/tokens/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/tokens/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts new file mode 100644 index 0000000000..d6c92daddb --- /dev/null +++ b/domain/tokens/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + implementation(projects.domain.core) + implementation(projects.domain.wallets.models) + implementation(projects.core.utils) + + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt new file mode 100644 index 0000000000..fcd8578f39 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -0,0 +1,79 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.raise.Raise +import arrow.core.raise.recover +import arrow.core.right +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.error.mapper.mapToTokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.operations.TokenListOperations +import com.tangem.domain.tokens.operations.TokensStatusesOperations +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.flatMapConcat + +class GetTokenListUseCase( + internal val tokensRepository: TokensRepository, + internal val quotesRepository: QuotesRepository, + internal val networksRepository: NetworksRepository, + internal val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = true): Flow> { + return channelFlow { + recover( + block = { + getTokenList(userWalletId, refresh).collect { list -> + send(list.right()) + } + }, + recover = { error -> + send(error.left()) + }, + ) + } + } + private fun Raise.getTokenList(userWalletId: UserWalletId, refresh: Boolean): Flow { + return getTokensStatuses(userWalletId, refresh).flatMapConcat { tokens -> + createTokenList(userWalletId, tokens) + } + } + + private fun Raise.getTokensStatuses( + userWalletId: UserWalletId, + refresh: Boolean, + ): Flow> { + val operations = TokensStatusesOperations( + userWalletId = userWalletId, + refresh = refresh, + useCase = this@GetTokenListUseCase, + raise = this, + transformError = TokensStatusesOperations.Error::mapToTokenListError, + ) + + return operations.getTokensStatusesFlow() + } + + private fun Raise.createTokenList( + userWalletId: UserWalletId, + tokens: Set, + ): Flow { + val operations = TokenListOperations( + userWalletId = userWalletId, + tokens = tokens, + useCase = this@GetTokenListUseCase, + raise = this, + transform = TokenListOperations.Error::mapToTokenListError, + ) + + return operations.getTokenListFlow() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt new file mode 100644 index 0000000000..4b0b8664f4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.error.mapper.mapToTokenListSortingError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.operations.TokenListSortingOperations +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class ToggleTokenListSortingUseCase( + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend operator fun invoke(tokenList: TokenList): Either { + return withContext(dispatchers.default) { + either { + ensure(tokenList.totalFiatBalance !is TokenList.FiatBalance.Loading) { + TokenListSortingError.TokenListIsLoading + } + + when (tokenList) { + is TokenList.GroupedByNetwork -> sortGroupedTokenList(tokenList) + is TokenList.Ungrouped -> sortUngroupedTokenList(tokenList) + is TokenList.NotInitialized -> raise(TokenListSortingError.TokenListIsEmpty) + } + } + } + } + + private suspend fun Raise.sortGroupedTokenList( + tokenList: TokenList.GroupedByNetwork, + ): TokenList.GroupedByNetwork { + val operations = getSortingOperations(tokenList) + val networks = tokenList.groups.map { it.network }.toSet() + + return tokenList.copy( + groups = operations.getGroupedTokens(networks), + sortedBy = operations.getSortType(), + ) + } + + private suspend fun Raise.sortUngroupedTokenList( + tokenList: TokenList.Ungrouped, + ): TokenList.Ungrouped { + val operations = getSortingOperations(tokenList) + + return tokenList.copy( + tokens = operations.getTokens(), + sortedBy = operations.getSortType(), + ) + } + + private fun Raise.getSortingOperations(tokenList: TokenList): TokenListSortingOperations<*> { + return TokenListSortingOperations( + tokenList = tokenList, + sortByBalance = tokenList.sortedBy != TokenList.SortType.BALANCE, + dispatchers = dispatchers, + raise = this, + transformError = TokenListSortingOperations.Error::mapToTokenListSortingError, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt new file mode 100644 index 0000000000..0a321a7874 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListError.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.tokens.error + +import com.tangem.domain.tokens.model.TokenList + +sealed class TokenListError { + + object EmptyTokens : TokenListError() + + data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : TokenListError() + + data class DataError(val cause: Throwable) : TokenListError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt new file mode 100644 index 0000000000..ae49dfb209 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.tokens.error + +sealed class TokenListSortingError { + + object TokenListIsLoading : TokenListSortingError() + + object TokenListIsEmpty : TokenListSortingError() + + object UnableToSortTokenList : TokenListSortingError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt new file mode 100644 index 0000000000..2d0798a489 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.tokens.error.mapper + +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.operations.TokenListOperations +import com.tangem.domain.tokens.operations.TokensStatusesOperations + +internal fun TokensStatusesOperations.Error.mapToTokenListError(): TokenListError { + return when (this) { + is TokensStatusesOperations.Error.DataError -> TokenListError.DataError(this.cause) + is TokensStatusesOperations.Error.EmptyNetworksStatuses, + is TokensStatusesOperations.Error.EmptyQuotes, + is TokensStatusesOperations.Error.EmptyTokens, + -> TokenListError.EmptyTokens + } +} + +internal fun TokenListOperations.Error.mapToTokenListError(): TokenListError { + return when (this) { + is TokenListOperations.Error.DataError -> TokenListError.DataError(this.cause) + is TokenListOperations.Error.UnableToSortTokenList -> + TokenListError.UnableToSortTokenList(this.unsortedTokenList) + is TokenListOperations.Error.UnableToGroupTokenList -> + TokenListError.UnableToSortTokenList(this.ungroupedTokenList) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt new file mode 100644 index 0000000000..2b492ad7ee --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListSortingErrorMappers.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.tokens.error.mapper + +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.operations.TokenListSortingOperations + +internal fun TokenListSortingOperations.Error.mapToTokenListSortingError(): TokenListSortingError { + return when (this) { + is TokenListSortingOperations.Error.EmptyTokens -> TokenListSortingError.TokenListIsEmpty + is TokenListSortingOperations.Error.EmptyNetworks, + is TokenListSortingOperations.Error.NetworkNotFound, + -> TokenListSortingError.UnableToSortTokenList + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt new file mode 100644 index 0000000000..b4b7628aa4 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Network.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.tokens.model + +data class Network( + val id: ID, + val name: String, +) { + + @JvmInline + value class ID(val value: String) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt new file mode 100644 index 0000000000..d1cc917d38 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.tokens.model + +data class NetworkGroup( + val network: Network, + val tokens: Set, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt new file mode 100644 index 0000000000..36d1bc30e6 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +data class NetworkStatus( + val networkId: Network.ID, + val value: Status, +) { + + sealed class Status { + open val amounts: Map? = null + } + + object Unreachable : Status() + + object MissedDerivation : Status() + + data class TransactionInProgress(override val amounts: Map) : Status() + + data class Verified(override val amounts: Map) : Status() + + data class NoAccount(val amountToCreateAccount: BigDecimal) : Status() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt new file mode 100644 index 0000000000..fad3db3666 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +data class Quote( + val tokenId: Token.ID, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt new file mode 100644 index 0000000000..4e1a2fba61 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Token.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.tokens.model + +data class Token( + val id: ID, + val networkId: Network.ID, + val name: String, + val symbol: String, + val iconUrl: String?, + val decimals: Int, + val isCustom: Boolean, + val isCoin: Boolean, +) { + + @JvmInline + value class ID(val value: String) +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt new file mode 100644 index 0000000000..3f1ffaaf75 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +sealed class TokenList { + open val totalFiatBalance: FiatBalance = FiatBalance.Loading + open val sortedBy: SortType = SortType.NONE + + data class GroupedByNetwork( + val groups: Set, + override val totalFiatBalance: FiatBalance, + override val sortedBy: SortType, + ) : TokenList() + + data class Ungrouped( + val tokens: Set, + override val totalFiatBalance: FiatBalance, + override val sortedBy: SortType, + ) : TokenList() + + object NotInitialized : TokenList() + + enum class SortType { + NONE, BALANCE, + } + + sealed class FiatBalance { + object Loading : FiatBalance() + + object Failed : FiatBalance() + + data class Loaded( + val amount: BigDecimal, + val isAllAmountsSummarized: Boolean, + ) : FiatBalance() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt new file mode 100644 index 0000000000..1ea03d8aa3 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenStatus.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +data class TokenStatus( + val id: Token.ID, + val networkId: Network.ID, + val name: String, + val symbol: String, + val decimals: Int, + val iconUrl: String?, + val isCoin: Boolean, + val value: Status, +) { + + sealed class Status { + open val amount: BigDecimal? = null + open val fiatAmount: BigDecimal? = null + open val priceChange: BigDecimal? = null + open val hasTransactionsInProgress: Boolean = false + } + + object Loading : Status() + + object Unreachable : Status() + + object MissedDerivation : Status() + + object NoAccount : Status() + + data class Loaded( + override val amount: BigDecimal, + override val fiatAmount: BigDecimal, + override val priceChange: BigDecimal, + override val hasTransactionsInProgress: Boolean, + ) : Status() + + data class Custom( + override val amount: BigDecimal, + override val fiatAmount: BigDecimal?, + override val priceChange: BigDecimal?, + override val hasTransactionsInProgress: Boolean, + ) : Status() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt new file mode 100644 index 0000000000..69378510f3 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -0,0 +1,89 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.NonEmptySet +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class TokenListFiatBalanceOperations( + private val tokens: NonEmptySet, + private val isAnyTokenLoading: Boolean, + private val dispatcher: CoroutineDispatcherProvider, +) { + + suspend fun calculateFiatBalance(): TokenList.FiatBalance { + return withContext(dispatcher.single) { + var fiatBalance: TokenList.FiatBalance = TokenList.FiatBalance.Loading + if (isAnyTokenLoading) return@withContext fiatBalance + + for (token in tokens) { + when (val status = token.value) { + is TokenStatus.Loading -> { + fiatBalance = TokenList.FiatBalance.Loading + break + } + is TokenStatus.MissedDerivation, + is TokenStatus.Unreachable, + -> { + fiatBalance = TokenList.FiatBalance.Failed + break + } + is TokenStatus.NoAccount -> { + fiatBalance = recalculateBalanceForNoAccountStatus(fiatBalance) + } + is TokenStatus.Loaded -> { + fiatBalance = recalculateBalance(status, fiatBalance) + } + is TokenStatus.Custom -> { + fiatBalance = recalculateBalance(status, fiatBalance) + } + } + } + + fiatBalance + } + } + private fun recalculateBalanceForNoAccountStatus(currentBalance: TokenList.FiatBalance): TokenList.FiatBalance { + return with(currentBalance) { + (this as? TokenList.FiatBalance.Loaded)?.copy( + isAllAmountsSummarized = false, + ) ?: TokenList.FiatBalance.Loaded( + amount = BigDecimal.ZERO, + isAllAmountsSummarized = false, + ) + } + } + + private fun recalculateBalance( + status: TokenStatus.Loaded, + currentBalance: TokenList.FiatBalance, + ): TokenList.FiatBalance { + return with(currentBalance) { + (this as? TokenList.FiatBalance.Loaded)?.copy( + amount = this.amount + status.fiatAmount, + ) ?: TokenList.FiatBalance.Loaded( + amount = status.fiatAmount, + isAllAmountsSummarized = true, + ) + } + } + + private fun recalculateBalance( + status: TokenStatus.Custom, + currentBalance: TokenList.FiatBalance, + ): TokenList.FiatBalance { + return with(currentBalance) { + val isTokenAmountCanBeSummarized = status.fiatAmount != null + + (this as? TokenList.FiatBalance.Loaded)?.copy( + amount = this.amount + (status.fiatAmount ?: BigDecimal.ZERO), + isAllAmountsSummarized = isTokenAmountCanBeSummarized, + ) ?: TokenList.FiatBalance.Loaded( + amount = status.fiatAmount ?: BigDecimal.ZERO, + isAllAmountsSummarized = isTokenAmountCanBeSummarized, + ) + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt new file mode 100644 index 0000000000..9283eacb78 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -0,0 +1,188 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.NonEmptySet +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.withContext + +@Suppress("LongParameterList") +internal class TokenListOperations( + private val tokensRepository: TokensRepository, + private val networksRepository: NetworksRepository, + private val userWalletId: UserWalletId, + private val tokens: Set, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, + transform: (Error) -> E, +) : DelegatedRaise(raise, transform) { + + constructor( + userWalletId: UserWalletId, + tokens: Set, + useCase: GetTokenListUseCase, + raise: Raise, + transform: (Error) -> E, + ) : this( + tokensRepository = useCase.tokensRepository, + networksRepository = useCase.networksRepository, + userWalletId = userWalletId, + tokens = tokens, + dispatchers = useCase.dispatchers, + raise = raise, + transform = transform, + ) + + fun getTokenListFlow(): Flow { + return combine(getIsGrouped(), getIsSortedByBalance()) { isGrouped, isSortedByBalance -> + createTokenList(isGrouped, isSortedByBalance) + } + } + + private suspend fun createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { + return withContext(dispatchers.default) { + val tokensNes = tokens.toNonEmptySetOrNull() + ?: return@withContext TokenList.NotInitialized + + val isAnyTokenLoading = tokensNes.any { it.value is TokenStatus.Loading } + val fiatBalanceOperations = TokenListFiatBalanceOperations(tokensNes, isAnyTokenLoading, dispatchers) + + createTokenList( + tokens = tokensNes, + fiatBalance = fiatBalanceOperations.calculateFiatBalance(), + isAnyTokenLoading = isAnyTokenLoading, + isGrouped = isGrouped, + isSortedByBalance = isSortedByBalance, + ) + } + } + + private suspend fun createTokenList( + tokens: NonEmptySet, + fiatBalance: TokenList.FiatBalance, + isAnyTokenLoading: Boolean, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ): TokenList { + val sortingOperations = TokenListSortingOperations( + tokens = tokens, + isAnyTokenLoading = isAnyTokenLoading, + sortByBalance = isSortedByBalance, + dispatchers = dispatchers, + raise = this, + transformError = { e -> + Error.fromTokenListOperations(e) { createUnsortedUngroupedTokenList(tokens, fiatBalance) } + }, + ) + + return createTokenList(tokens, sortingOperations, fiatBalance, isGrouped) + } + + private suspend fun createTokenList( + tokens: NonEmptySet, + sortingOperations: TokenListSortingOperations<*>, + fiatBalance: TokenList.FiatBalance, + isGrouped: Boolean, + ): TokenList { + return if (isGrouped) { + val networks = ensureNotNull(getNetworks(tokens).toNonEmptySetOrNull()) { + Error.UnableToGroupTokenList( + ungroupedTokenList = createUngroupedTokenList(sortingOperations, fiatBalance), + ) + } + + createGroupedTokenList(sortingOperations, fiatBalance, networks) + } else { + createUngroupedTokenList(sortingOperations, fiatBalance) + } + } + + private suspend fun getNetworks(tokensNes: NonEmptySet): Set { + return withContext(dispatchers.io) { + val networksIds = tokensNes.map { it.networkId }.toNonEmptySet() + catch( + block = { networksRepository.getNetworks(networksIds) }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + + private suspend fun createUngroupedTokenList( + sortingOperations: TokenListSortingOperations<*>, + fiatBalance: TokenList.FiatBalance, + ): TokenList.Ungrouped = TokenList.Ungrouped( + sortedBy = sortingOperations.getSortType(), + totalFiatBalance = fiatBalance, + tokens = sortingOperations.getTokens(), + ) + + private suspend fun createGroupedTokenList( + sortingOperations: TokenListSortingOperations<*>, + fiatBalance: TokenList.FiatBalance, + networks: NonEmptySet, + ): TokenList.GroupedByNetwork = TokenList.GroupedByNetwork( + sortedBy = sortingOperations.getSortType(), + totalFiatBalance = fiatBalance, + groups = sortingOperations.getGroupedTokens(networks), + ) + + private fun createUnsortedUngroupedTokenList( + tokens: NonEmptySet, + fiatBalance: TokenList.FiatBalance, + ): TokenList.Ungrouped { + return TokenList.Ungrouped( + sortedBy = TokenList.SortType.NONE, + totalFiatBalance = fiatBalance, + tokens = tokens, + ) + } + + private fun getIsGrouped(): Flow { + return tokensRepository.isTokensGrouped(userWalletId) + .catch { raise(Error.DataError(it)) } + .onEmpty { emit(value = false) } + .flowOn(dispatchers.io) + } + + private fun getIsSortedByBalance(): Flow { + return tokensRepository.isTokensSortedByBalance(userWalletId) + .catch { raise(Error.DataError(it)) } + .onEmpty { emit(value = false) } + .flowOn(dispatchers.io) + } + + sealed class Error { + + data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : Error() + + data class UnableToGroupTokenList(val ungroupedTokenList: TokenList.Ungrouped) : Error() + + data class DataError(val cause: Throwable) : Error() + + internal companion object { + + fun fromTokenListOperations( + e: TokenListSortingOperations.Error, + createUnsortedUngroupedTokenList: () -> TokenList.Ungrouped, + ): Error = when (e) { + is TokenListSortingOperations.Error.EmptyNetworks, + is TokenListSortingOperations.Error.EmptyTokens, + is TokenListSortingOperations.Error.NetworkNotFound, + -> UnableToSortTokenList( + unsortedTokenList = createUnsortedUngroupedTokenList(), + ) + } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt new file mode 100644 index 0000000000..e40520ec97 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -0,0 +1,134 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.NonEmptySet +import arrow.core.raise.Raise +import arrow.core.raise.ensure +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class TokenListSortingOperations( + private val tokens: Set, + private val isAnyTokenLoading: Boolean, + private val sortByBalance: Boolean, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, + transformError: (Error) -> E, +) : DelegatedRaise(raise, transformError) { + + constructor( + tokenList: TokenList, + dispatchers: CoroutineDispatcherProvider, + raise: Raise, + transformError: (Error) -> E, + sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE, + isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TokenList.FiatBalance.Loading, + ) : this( + tokens = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.tokens }.toSet() + is TokenList.Ungrouped -> tokenList.tokens + is TokenList.NotInitialized -> emptySet() + }, + isAnyTokenLoading = isAnyTokenLoading, + sortByBalance = sortByBalance, + dispatchers = dispatchers, + raise = raise, + transformError = transformError, + ) + + suspend fun getGroupedTokens(networks: Set): NonEmptySet { + return withContext(dispatchers.default) { + ensure(tokens.isNotEmpty()) { Error.EmptyTokens } + val networksNes = ensureNotNull(networks.toNonEmptySetOrNull()) { + Error.EmptyNetworks + } + + if (sortByBalance) { + groupAndSortTokensByBalance(networksNes) + } else { + groupTokens(networksNes) + } + } + } + + suspend fun getTokens(): NonEmptySet { + return withContext(dispatchers.default) { + val tokensNes = ensureNotNull(tokens.toNonEmptySetOrNull()) { + Error.EmptyTokens + } + + if (sortByBalance) sortTokensByBalance(tokensNes) else tokensNes + } + } + + fun getSortType() = if (sortByBalance) TokenList.SortType.BALANCE else TokenList.SortType.NONE + + private fun groupTokens(networks: NonEmptySet): NonEmptySet { + val groupedTokens = tokens + .groupBy { it.networkId } + .map { (networkId, tokens) -> + val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) { + Error.NetworkNotFound(networkId) + } + + NetworkGroup( + network = network, + tokens = ensureNotNull(tokens.toNonEmptySetOrNull()) { Error.EmptyTokens }, + ) + } + .toNonEmptySetOrNull() + + return ensureNotNull(groupedTokens) { Error.EmptyTokens } + } + + private fun groupAndSortTokensByBalance(networks: NonEmptySet): NonEmptySet { + val groupsWithSortedTokens = groupTokens(networks) + .map { group -> + val tokens = group.tokens as? NonEmptySet + ?: error("Tokens can not be empty here") + group.copy(tokens = sortTokensByBalance(tokens)) + } + .toNonEmptySet() + + return if (isAnyTokenLoading) { + groupsWithSortedTokens + } else { + sortGroupsByBalance(groupsWithSortedTokens) + } + } + + private fun sortTokensByBalance(tokens: NonEmptySet): NonEmptySet { + return if (isAnyTokenLoading) { + tokens + } else { + tokens.sortedByDescending { it.value.fiatAmount ?: BigDecimal.ZERO } + .toNonEmptySetOrNull() + ?: error("Tokens can not be empty here") + } + } + + private fun sortGroupsByBalance(groupsWithSortedTokens: NonEmptySet): NonEmptySet { + return groupsWithSortedTokens + .sortedByDescending { group -> + group.tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO } + } + .toNonEmptySetOrNull() + ?: error("Tokens can not be empty here") + } + + sealed class Error { + + object EmptyTokens : Error() + + object EmptyNetworks : Error() + + data class NetworkNotFound(val networkId: Network.ID) : Error() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt new file mode 100644 index 0000000000..04478c7917 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenStatusOperations.kt @@ -0,0 +1,78 @@ +package com.tangem.domain.tokens.operations + +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class TokenStatusOperations( + private val token: Token, + private val quote: Quote?, + private val networkStatus: NetworkStatus?, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend fun createTokenStatus(): TokenStatus = withContext(dispatchers.default) { + TokenStatus( + id = token.id, + networkId = token.networkId, + name = token.name, + symbol = token.symbol, + isCoin = token.isCoin, + decimals = token.decimals, + iconUrl = token.iconUrl, + value = createStatus(), + ) + } + + private fun createStatus(): TokenStatus.Status { + return when (val status = networkStatus?.value) { + null -> TokenStatus.Loading + is NetworkStatus.MissedDerivation -> TokenStatus.MissedDerivation + is NetworkStatus.Unreachable -> TokenStatus.Unreachable + is NetworkStatus.NoAccount -> TokenStatus.NoAccount + is NetworkStatus.TransactionInProgress, + is NetworkStatus.Verified, + -> createStatus( + amount = getTokenAmount(), + hasTransactionsInProgress = status is NetworkStatus.TransactionInProgress, + ) + } + } + + private fun createStatus(amount: BigDecimal, hasTransactionsInProgress: Boolean): TokenStatus.Status { + return when { + token.isCustom -> TokenStatus.Custom( + amount = amount, + fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), + priceChange = quote?.priceChange, + hasTransactionsInProgress = hasTransactionsInProgress, + ) + quote == null -> TokenStatus.Loading + else -> TokenStatus.Loaded( + amount = amount, + fiatAmount = calculateFiatAmount(amount, quote.fiatRate), + priceChange = quote.priceChange, + hasTransactionsInProgress = hasTransactionsInProgress, + ) + } + } + + private fun getTokenAmount(): BigDecimal { + val amount = networkStatus?.value?.amounts?.get(token.id) + return amount ?: error("Incorrect network status: $networkStatus") + } + + private fun calculateFiatAmountOrNull(amount: BigDecimal, fiatRate: BigDecimal?): BigDecimal? { + if (fiatRate == null) return null + + return calculateFiatAmount(amount, fiatRate) + } + + private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal { + return amount * fiatRate + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt new file mode 100644 index 0000000000..cbe9d303ad --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokensStatusesOperations.kt @@ -0,0 +1,130 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.NonEmptySet +import arrow.core.raise.Raise +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.withContext + +@Suppress("LongParameterList") +internal class TokensStatusesOperations( + private val tokensRepository: TokensRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val userWalletId: UserWalletId, + private val refresh: Boolean, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, + transformError: (Error) -> E, +) : DelegatedRaise(raise, transformError) { + + constructor( + userWalletId: UserWalletId, + refresh: Boolean, + useCase: GetTokenListUseCase, + raise: Raise, + transformError: (Error) -> E, + ) : this( + tokensRepository = useCase.tokensRepository, + quotesRepository = useCase.quotesRepository, + networksRepository = useCase.networksRepository, + userWalletId = userWalletId, + refresh = refresh, + dispatchers = useCase.dispatchers, + raise = raise, + transformError = transformError, + ) + + fun getTokensStatusesFlow(): Flow> { + return getTokens().flatMapConcat { + val tokens = it.toNonEmptySetOrNull() + + if (tokens == null) { + flowOf(emptySet()) + } else { + val tokensIds = tokens.map { token -> token.id }.toNonEmptySet() + val groupedTokens = groupTokens(tokens) + + combine(getQuotes(tokensIds), getNetworksStatues(groupedTokens)) { quotes, networksStatuses -> + createTokensStatuses(tokens, quotes, networksStatuses) + } + } + } + } + + private suspend fun createTokensStatuses( + tokens: Set, + quotes: Set, + networkStatuses: Set, + ): Set = withContext(dispatchers.default) { + tokens.mapTo(hashSetOf()) { token -> + val quote = quotes.firstOrNull { it.tokenId == token.id } + val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId } + + createStatus(token, quote, networkStatus) + } + } + + private suspend fun createStatus(token: Token, quote: Quote?, networkStatus: NetworkStatus?): TokenStatus { + val tokenStatusOperations = TokenStatusOperations( + token = token, + quote = quote, + networkStatus = networkStatus, + dispatchers = dispatchers, + ) + + return tokenStatusOperations.createTokenStatus() + } + + private fun getTokens(): Flow> { + return tokensRepository.getTokens(userWalletId, refresh) + .catch { raise(Error.DataError(it)) } + .onEmpty { raise(Error.EmptyTokens) } + .flowOn(dispatchers.io) + } + + private fun getQuotes(tokensIds: NonEmptySet): Flow> { + return quotesRepository.getQuotes(tokensIds, refresh) + .catch { raise(Error.DataError(it)) } + .onEmpty { raise(Error.EmptyQuotes) } + .flowOn(dispatchers.io) + } + + private fun getNetworksStatues(groupedTokens: Map>): Flow> { + return networksRepository.getNetworkStatuses(userWalletId, groupedTokens, refresh) + .catch { raise(Error.DataError(it)) } + .onEmpty { raise(Error.EmptyNetworksStatuses) } + .flowOn(dispatchers.io) + } + + private suspend fun groupTokens(tokens: NonEmptySet): Map> = + withContext(dispatchers.default) { + tokens + .groupBy { it.networkId } + .mapValues { (_, tokens) -> + // Can not be empty + tokens.toNonEmptySetOrNull()!! + .map { it.id } + .toNonEmptySet() + } + } + + sealed class Error { + + object EmptyTokens : Error() + + object EmptyQuotes : Error() + + object EmptyNetworksStatuses : Error() + + data class DataError(val cause: Throwable) : Error() + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt new file mode 100644 index 0000000000..93f7f7c95b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +// FIXME: Use Raise as context instead of Effect when context receivers become stable +// [REDACTED_JIRA] + +/** + * Repository for everything related to the blockchain networks + * */ +interface NetworksRepository { + + fun getNetworks(networksIds: Set): Set + + fun getNetworkStatuses( + userWalletId: UserWalletId, + networks: Map>, + refresh: Boolean, + ): Flow> +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt new file mode 100644 index 0000000000..984ffb6608 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import kotlinx.coroutines.flow.Flow + +// FIXME: Use Raise as context instead of Effect when context receivers become stable +// [REDACTED_JIRA] + +/** + * Repository for everything related to the quotes of tokens + * */ +interface QuotesRepository { + + fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt new file mode 100644 index 0000000000..b544ecb8f5 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +// FIXME: Use Raise as context instead of Effect when context receivers become stable +// [REDACTED_JIRA] + +/** + * Repository for everything related to the tokens of user wallet + * */ +interface TokensRepository { + + suspend fun sortTokens( + userWalletId: UserWalletId, + sortedTokensIds: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ) + + fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> + + fun isTokensGrouped(userWalletId: UserWalletId): Flow + + fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt new file mode 100644 index 0000000000..fb26a27895 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -0,0 +1,333 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.mock.MockNetworks +import com.tangem.domain.tokens.mock.MockQuotes +import com.tangem.domain.tokens.mock.MockTokenLists +import com.tangem.domain.tokens.mock.MockTokens +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.tokens.repository.MockNetworksRepository +import com.tangem.domain.tokens.repository.MockQuotesRepository +import com.tangem.domain.tokens.repository.MockTokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class GetTokenListUseCaseTest { + + private val dispatchers = TestingCoroutineDispatcherProvider() + private val userWalletId = UserWalletId(value = null) + + @Test + fun `when list ungrouped and unsorted then correct token list should be returned`() = runTest { + // Given + val expectedResult = MockTokenLists.ungroupedTokenList.right() + + val useCase = getUseCase( + isGrouped = flowOf(false.right()), + isSortedByBalance = flowOf(false.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(tokens = flowOf(DataError.NetworkError.NoInternetConnection.left())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(quotes = flowOf(DataError.NetworkError.NoInternetConnection.left())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks getting failed and list is groped then error should be received`() = runTest { + // Given + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase( + networks = DataError.NetworkError.NoInternetConnection.left(), + isGrouped = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks statuses getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(statuses = flowOf(DataError.NetworkError.NoInternetConnection.left())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when grouping type getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(isGrouped = flowOf(DataError.NetworkError.NoInternetConnection.left())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when sorting type getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(isSortedByBalance = flowOf(DataError.NetworkError.NoInternetConnection.left())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens getting failed on second emit then error should be received`() = runTest { + // Given + val error = DataError.NetworkError.NoInternetConnection.left() + val expectedResult = listOf( + MockTokenLists.ungroupedTokenList.right(), + TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left(), + ) + + val useCase = getUseCase( + tokens = flowOf( + MockTokens.tokens.right(), + error, + ), + ) + + // When + val result = useCase(userWalletId) + .take(count = 2) + .toList() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list grouped then correct token list should be received`() = runTest { + val expectedResult = MockTokenLists.groupedTokenList.right() + + val useCase = getUseCase(isGrouped = flowOf(true.right())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and networks getting failed then error should be received`() = runTest { + val expectedResult = TokenListError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase( + networks = DataError.NetworkError.NoInternetConnection.left(), + isGrouped = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is sorted and ungrouped then correct token list should be received`() = runTest { + val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + isGrouped = flowOf(false.right()), + isSortedByBalance = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is sorted and grouped then correct token list should be received`() = runTest { + val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + isGrouped = flowOf(true.right()), + isSortedByBalance = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens is empty then not initialized token list should be received`() = runTest { + val expectedResult = MockTokenLists.notInitializedTokenList.right() + + val useCase = getUseCase(tokens = flowOf(emptySet().right())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks is empty and list is grouped then ungrouped list should be received`() = runTest { + val expectedResult = TokenListError.UnableToSortTokenList(MockTokenLists.ungroupedTokenList).left() + + val useCase = getUseCase( + networks = emptySet().right(), + isGrouped = flowOf(true.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens flow is empty then error should be received`() = runTest { + val expectedResult = TokenListError.EmptyTokens.left() + + val useCase = getUseCase(tokens = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks statuses flow is empty then error should be received`() = runTest { + val expectedResult = TokenListError.EmptyTokens.left() + + val useCase = getUseCase(statuses = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when networks statuses is empty then loading token list should be received`() = runTest { + val expectedResult = MockTokenLists.loadingUngroupedTokenList.right() + + val useCase = getUseCase(statuses = flowOf(emptySet().right())) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes flow is empty then error should be received`() = runTest { + val expectedResult = TokenListError.EmptyTokens.left() + + val useCase = getUseCase(quotes = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes is empty and statuses verified then loading token list should be received`() = runTest { + val expectedResult = MockTokenLists.loadingUngroupedTokenList.right() + + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + quotes = flowOf(emptySet().right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + private fun getUseCase( + tokens: Flow>> = flowOf(MockTokens.tokens.right()), + quotes: Flow>> = flowOf(MockQuotes.quotes.right()), + networks: Either> = MockNetworks.networks.right(), + statuses: Flow>> = flowOf(MockNetworks.errorNetworksStatuses.right()), + isGrouped: Flow> = flowOf(MockTokenLists.isGrouped.right()), + isSortedByBalance: Flow> = flowOf(MockTokenLists.isSortedByBalance.right()), + ) = GetTokenListUseCase( + dispatchers = dispatchers, + tokensRepository = MockTokensRepository(tokens, isGrouped, isSortedByBalance), + quotesRepository = MockQuotesRepository(quotes), + networksRepository = MockNetworksRepository(networks, statuses), + ) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt new file mode 100644 index 0000000000..03ad43ba70 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -0,0 +1,90 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.NonEmptySet +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockNetworks { + + val amountToCreateAccount: BigDecimal = BigDecimal.TEN + + val network1 = Network( + id = Network.ID("network1"), + name = "Network One", + ) + + val network2 = Network( + id = Network.ID("network2"), + name = "Network Two", + ) + + val network3 = Network( + id = Network.ID("network3"), + name = "Network Three", + ) + + val networks = nonEmptySetOf(network1, network2, network3) + + val networkStatus1 = NetworkStatus( + networkId = network1.id, + value = NetworkStatus.Unreachable, + ) + + val networkStatus2 = NetworkStatus( + networkId = network2.id, + value = NetworkStatus.MissedDerivation, + ) + + val networkStatus3 = NetworkStatus( + networkId = network3.id, + value = NetworkStatus.NoAccount( + amountToCreateAccount = amountToCreateAccount, + ), + ) + + val errorNetworksStatuses = nonEmptySetOf(networkStatus1, networkStatus2, networkStatus3) + + val verifiedNetworkStatus1: NetworkStatus + get() = networkStatus1.copy( + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token1.id to BigDecimal.TEN, + MockTokens.token2.id to BigDecimal.TEN, + MockTokens.token3.id to BigDecimal.TEN, + ), + ), + ) + + val verifiedNetworkStatus2: NetworkStatus + get() = networkStatus2.copy( + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token4.id to BigDecimal.TEN, + MockTokens.token5.id to BigDecimal.TEN, + MockTokens.token6.id to BigDecimal.TEN, + ), + ), + ) + + val verifiedNetworkStatus3: NetworkStatus + get() = networkStatus3.copy( + value = NetworkStatus.Verified( + amounts = mapOf( + MockTokens.token7.id to BigDecimal.TEN, + MockTokens.token8.id to BigDecimal.TEN, + MockTokens.token9.id to BigDecimal.TEN, + MockTokens.token10.id to BigDecimal.TEN, + ), + ), + ) + + val verifiedNetworksStatuses: NonEmptySet + get() = nonEmptySetOf( + verifiedNetworkStatus1, + verifiedNetworkStatus2, + verifiedNetworkStatus3, + ) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt new file mode 100644 index 0000000000..767391423e --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.Quote +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockQuotes { + + val quote1 = Quote( + tokenId = MockTokens.token1.id, + fiatRate = BigDecimal("1.23"), + priceChange = BigDecimal("0.01"), + ) + + val quote2 = Quote( + tokenId = MockTokens.token2.id, + fiatRate = BigDecimal("2.34"), + priceChange = BigDecimal("-0.02"), + ) + + val quote3 = Quote( + tokenId = MockTokens.token3.id, + fiatRate = BigDecimal("3.45"), + priceChange = BigDecimal("0.03"), + ) + + val quote4 = Quote( + tokenId = MockTokens.token4.id, + fiatRate = BigDecimal("4.56"), + priceChange = BigDecimal("-0.04"), + ) + + val quote5 = Quote( + tokenId = MockTokens.token5.id, + fiatRate = BigDecimal("5.67"), + priceChange = BigDecimal("0.05"), + ) + + val quote6 = Quote( + tokenId = MockTokens.token6.id, + fiatRate = BigDecimal("6.78"), + priceChange = BigDecimal("-0.06"), + ) + + val quote7 = Quote( + tokenId = MockTokens.token7.id, + fiatRate = BigDecimal("7.89"), + priceChange = BigDecimal("0.07"), + ) + + val quote8 = Quote( + tokenId = MockTokens.token8.id, + fiatRate = BigDecimal("8.90"), + priceChange = BigDecimal("-0.08"), + ) + + val quote9 = Quote( + tokenId = MockTokens.token9.id, + fiatRate = BigDecimal("9.01"), + priceChange = BigDecimal("0.09"), + ) + + val quote10 = Quote( + tokenId = MockTokens.token10.id, + fiatRate = BigDecimal("10.12"), + priceChange = BigDecimal("-0.10"), + ) + + val quotes = nonEmptySetOf(quote1, quote2, quote3, quote4, quote5, quote6, quote7, quote8, quote9, quote10) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt new file mode 100644 index 0000000000..b2befbeeb1 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -0,0 +1,104 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockTokenLists { + + const val isGrouped = false + const val isSortedByBalance = false + + val networkGroup1 = NetworkGroup( + network = MockNetworks.network1, + tokens = MockTokensStates.tokenStates + .filter { it.networkId == MockNetworks.network1.id } + .toNonEmptySetOrNull()!!, + ) + + val networkGroup2 = NetworkGroup( + network = MockNetworks.network2, + tokens = MockTokensStates.tokenStates + .filter { it.networkId == MockNetworks.network2.id } + .toNonEmptySetOrNull()!!, + ) + + val networkGroup3 = NetworkGroup( + network = MockNetworks.network3, + tokens = MockTokensStates.tokenStates + .filter { it.networkId == MockNetworks.network3.id } + .toNonEmptySetOrNull()!!, + ) + + val networksGroups = nonEmptySetOf(networkGroup1, networkGroup2, networkGroup3) + + val sortedNetworksGroups = networksGroups.map { group -> + group.copy( + tokens = MockTokensStates.loadedTokensStates + .filter { it.networkId == group.network.id } + .sortedByDescending { it.value.fiatAmount } + .toNonEmptySetOrNull()!!, + ) + } + .sortedByDescending { group -> + group.tokens.sumOf { it.value.fiatAmount!! } + } + .toNonEmptySetOrNull()!! + + val notInitializedTokenList = TokenList.NotInitialized + + val groupedTokenList = TokenList.GroupedByNetwork( + groups = networksGroups, + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val ungroupedTokenList = TokenList.Ungrouped( + tokens = MockTokensStates.tokenStates, + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val loadingUngroupedTokenList = with(ungroupedTokenList) { + copy( + tokens = tokens.map { it.copy(value = TokenStatus.Loading) }.toSet(), + totalFiatBalance = TokenList.FiatBalance.Loading, + ) + } + + val sortedUngroupedTokenList: TokenList.Ungrouped + get() { + val tokens = MockTokensStates.loadedTokensStates + .sortedByDescending { it.value.fiatAmount } + .toNonEmptySetOrNull()!! + + return ungroupedTokenList.copy( + tokens = tokens, + sortedBy = TokenList.SortType.BALANCE, + totalFiatBalance = TokenList.FiatBalance.Loaded( + amount = tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, + isAllAmountsSummarized = true, + ), + ) + } + + val sortedGroupedTokenList: TokenList.GroupedByNetwork + get() { + val groups = sortedNetworksGroups.toSet() + + return groupedTokenList.copy( + groups = groups, + sortedBy = TokenList.SortType.BALANCE, + totalFiatBalance = TokenList.FiatBalance.Loaded( + amount = groups + .flatMap { it.tokens } + .sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, + isAllAmountsSummarized = true, + ), + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt new file mode 100644 index 0000000000..43f22db152 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -0,0 +1,112 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.Token + +internal object MockTokens { + + val token1 = Token( + id = Token.ID("token1"), + networkId = MockNetworks.network1.id, + name = "Token 1", + symbol = "T1", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + + ) + val token2 = Token( + id = Token.ID("token2"), + networkId = MockNetworks.network1.id, + name = "Token 2", + symbol = "T2", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token3 = Token( + id = Token.ID("token3"), + networkId = MockNetworks.network1.id, + name = "Token 3", + symbol = "T3", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token4 = Token( + id = Token.ID("token4"), + networkId = MockNetworks.network2.id, + name = "Token 4", + symbol = "T4", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + + ) + val token5 = Token( + id = Token.ID("token5"), + networkId = MockNetworks.network2.id, + name = "Token 5", + symbol = "T5", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token6 = Token( + id = Token.ID("token6"), + networkId = MockNetworks.network2.id, + name = "Token 6", + symbol = "T6", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token7 = Token( + id = Token.ID("token7"), + networkId = MockNetworks.network3.id, + name = "Token 7", + symbol = "T7", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = true, + ) + val token8 = Token( + id = Token.ID("token8"), + networkId = MockNetworks.network3.id, + name = "Token 8", + symbol = "T8", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token9 = Token( + id = Token.ID("token9"), + networkId = MockNetworks.network3.id, + name = "Token 9", + symbol = "T9", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + val token10 = Token( + id = Token.ID("token10"), + networkId = MockNetworks.network3.id, + name = "Token 10", + symbol = "T10", + isCustom = false, + decimals = 8, + iconUrl = null, + isCoin = false, + ) + + val tokens = nonEmptySetOf(token1, token2, token3, token4, token5, token6, token7, token8, token9, token10) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt new file mode 100644 index 0000000000..0b0c9e41ab --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -0,0 +1,148 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.TokenStatus + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockTokensStates { + + val tokenState1 = TokenStatus( + id = MockTokens.token1.id, + networkId = MockTokens.token1.networkId, + name = MockTokens.token1.name, + symbol = MockTokens.token1.symbol, + decimals = MockTokens.token1.decimals, + iconUrl = MockTokens.token1.iconUrl, + isCoin = MockTokens.token1.isCoin, + value = TokenStatus.Unreachable, + ) + + val tokenState2 = TokenStatus( + id = MockTokens.token2.id, + networkId = MockTokens.token2.networkId, + name = MockTokens.token2.name, + symbol = MockTokens.token2.symbol, + decimals = MockTokens.token2.decimals, + iconUrl = MockTokens.token2.iconUrl, + isCoin = MockTokens.token2.isCoin, + value = TokenStatus.Unreachable, + ) + + val tokenState3 = TokenStatus( + id = MockTokens.token3.id, + networkId = MockTokens.token3.networkId, + name = MockTokens.token3.name, + symbol = MockTokens.token3.symbol, + decimals = MockTokens.token3.decimals, + iconUrl = MockTokens.token3.iconUrl, + isCoin = MockTokens.token3.isCoin, + value = TokenStatus.Unreachable, + ) + + val tokenState4 = TokenStatus( + id = MockTokens.token4.id, + networkId = MockTokens.token4.networkId, + name = MockTokens.token4.name, + symbol = MockTokens.token4.symbol, + decimals = MockTokens.token4.decimals, + iconUrl = MockTokens.token4.iconUrl, + isCoin = MockTokens.token4.isCoin, + value = TokenStatus.MissedDerivation, + ) + + val tokenState5 = TokenStatus( + id = MockTokens.token5.id, + networkId = MockTokens.token5.networkId, + name = MockTokens.token5.name, + symbol = MockTokens.token5.symbol, + decimals = MockTokens.token5.decimals, + iconUrl = MockTokens.token5.iconUrl, + isCoin = MockTokens.token5.isCoin, + value = TokenStatus.MissedDerivation, + ) + + val tokenState6 = TokenStatus( + id = MockTokens.token6.id, + networkId = MockTokens.token6.networkId, + name = MockTokens.token6.name, + symbol = MockTokens.token6.symbol, + decimals = MockTokens.token6.decimals, + iconUrl = MockTokens.token6.iconUrl, + isCoin = MockTokens.token6.isCoin, + value = TokenStatus.MissedDerivation, + ) + + val tokenState7 = TokenStatus( + id = MockTokens.token7.id, + networkId = MockTokens.token7.networkId, + name = MockTokens.token7.name, + symbol = MockTokens.token7.symbol, + decimals = MockTokens.token7.decimals, + iconUrl = MockTokens.token7.iconUrl, + isCoin = MockTokens.token7.isCoin, + value = TokenStatus.NoAccount, + ) + + val tokenState8 = TokenStatus( + id = MockTokens.token8.id, + networkId = MockTokens.token8.networkId, + name = MockTokens.token8.name, + symbol = MockTokens.token8.symbol, + decimals = MockTokens.token8.decimals, + iconUrl = MockTokens.token8.iconUrl, + isCoin = MockTokens.token8.isCoin, + value = TokenStatus.NoAccount, + ) + + val tokenState9 = TokenStatus( + id = MockTokens.token9.id, + networkId = MockTokens.token9.networkId, + name = MockTokens.token9.name, + symbol = MockTokens.token9.symbol, + decimals = MockTokens.token9.decimals, + iconUrl = MockTokens.token9.iconUrl, + isCoin = MockTokens.token9.isCoin, + value = TokenStatus.NoAccount, + ) + + val tokenState10 = TokenStatus( + id = MockTokens.token10.id, + networkId = MockTokens.token10.networkId, + name = MockTokens.token10.name, + symbol = MockTokens.token10.symbol, + decimals = MockTokens.token10.decimals, + iconUrl = MockTokens.token10.iconUrl, + isCoin = MockTokens.token10.isCoin, + value = TokenStatus.NoAccount, + ) + + val tokenStates = nonEmptySetOf( + tokenState1, + tokenState2, + tokenState3, + tokenState4, + tokenState5, + tokenState6, + tokenState7, + tokenState8, + tokenState9, + tokenState10, + ) + + val loadedTokensStates = tokenStates.map { state -> + val networkStatus = MockNetworks.verifiedNetworksStatuses.first { it.networkId == state.networkId } + val amount = (networkStatus.value as NetworkStatus.Verified).amounts[state.id]!! + val quote = MockQuotes.quotes.first { it.tokenId == state.id } + val fiatAmount = amount * quote.fiatRate + + state.copy( + value = TokenStatus.Loaded( + amount = amount, + fiatAmount = fiatAmount, + priceChange = quote.priceChange, + hasTransactionsInProgress = false, + ), + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt new file mode 100644 index 0000000000..98c7002327 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class MockNetworksRepository( + private val networks: Either>, + private val statuses: Flow>>, +) : NetworksRepository { + + override fun getNetworks(networksIds: Set): Set { + return networks.getOrElse { throw it } + } + + override fun getNetworkStatuses( + userWalletId: UserWalletId, + networks: Map>, + refresh: Boolean, + ): Flow> { + return statuses.map { it.getOrElse { e -> throw e } } + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt new file mode 100644 index 0000000000..fcf52e44e4 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.model.Token +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class MockQuotesRepository( + private val quotes: Flow>>, +) : QuotesRepository { + + override fun getQuotes(tokensIds: Set, refresh: Boolean): Flow> { + return quotes.map { it.getOrElse { e -> throw e } } + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt new file mode 100644 index 0000000000..813816c52a --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.tokens.repository + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.domain.core.error.DataError +import com.tangem.domain.tokens.model.Token +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class MockTokensRepository( + private val tokens: Flow>>, + private val isGrouped: Flow>, + private val isSortedByBalance: Flow>, +) : TokensRepository { + + override suspend fun sortTokens( + userWalletId: UserWalletId, + sortedTokensIds: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ) = Unit + + override fun getTokens(userWalletId: UserWalletId, refresh: Boolean): Flow> { + return tokens.map { it.getOrElse { e -> throw e } } + } + + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { + return isGrouped.map { it.getOrElse { e -> throw e } } + } + + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { + return isSortedByBalance.map { it.getOrElse { e -> throw e } } + } +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index a9a1449956..31807a9a48 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { // endregion // region Other libraries + implementation(deps.arrow.core) implementation(deps.kotlin.coroutines) // endregion } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt new file mode 100644 index 0000000000..97ff31ef11 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SaveWalletError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.wallets.models + +/** +[REDACTED_AUTHOR] + */ +sealed interface SaveWalletError { + + // TODO: Finalize in next PRs + object CommonError : SaveWalletError +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt new file mode 100644 index 0000000000..3afdb23663 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.SaveWalletError +import com.tangem.domain.wallets.models.UserWallet + +/** + * Use case for saving user wallet + * + * @property walletsStateHolder state holder for getting static initialized 'userWalletsListManager' + * +[REDACTED_AUTHOR] + */ +class SaveWalletUseCase(private val walletsStateHolder: WalletsStateHolder) { + + suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { + requireNotNull(walletsStateHolder.userWalletsListManager).save(userWallet, canOverride) + .doOnSuccess { return Unit.right() } + .doOnFailure { return SaveWalletError.CommonError.left() } + + return Unit.right() + } +} \ No newline at end of file diff --git a/features/learn2earn/impl/build.gradle.kts b/features/learn2earn/impl/build.gradle.kts index ce9914d94f..d85bd7dfba 100644 --- a/features/learn2earn/impl/build.gradle.kts +++ b/features/learn2earn/impl/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(project(":common")) implementation(project(":domain:legacy")) implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:featuretoggles")) implementation(project(":core:datasource")) implementation(project(":core:utils")) diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/analytics/Learn2earnEvents.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/analytics/Learn2earnEvents.kt index 164fa4e793..100a19ef0a 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/analytics/Learn2earnEvents.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/analytics/Learn2earnEvents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.learn2earn.analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent internal sealed class Learn2earnEvents( category: String, diff --git a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/WebViewResultHandler.kt b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/WebViewResultHandler.kt index 5f5ab27959..f537006cea 100644 --- a/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/WebViewResultHandler.kt +++ b/features/learn2earn/impl/src/main/java/com/tangem/feature/learn2earn/domain/api/WebViewResultHandler.kt @@ -1,6 +1,6 @@ package com.tangem.feature.learn2earn.domain.api -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent /** * Handler that helps determine a result of the Learn2earnWebViewActivity webView actions. diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 1940a2073c..cdd735f59c 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(project(":core:featuretoggles")) implementation(project(":core:datasource")) implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:utils")) implementation(project(":core:ui")) implementation(project(":core:res")) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt index 2db7541c5a..2f6b1d4a0c 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhrase.kt @@ -54,7 +54,6 @@ private fun Content(screen: SeedPhraseScreen, uiState: OnboardingSeedPhraseState state = uiState.introState, ) } - SeedPhraseScreen.AboutSeedPhrase -> { AboutSeedPhraseScreen( state = uiState.aboutState, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt index d0cd349b55..a63b4bccea 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt @@ -5,6 +5,9 @@ import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.bip39.MnemonicErrorResult import com.tangem.feature.onboarding.data.MnemonicRepository import com.tangem.utils.extensions.isNotWhitespace +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList /** [REDACTED_AUTHOR] @@ -58,14 +61,18 @@ internal class DefaultSeedPhraseInteractor constructor( } } - override suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List { - if (text.isEmpty() || cursorPosition == 0 || hasSelection) return emptyList() + override suspend fun getSuggestions( + text: String, + hasSelection: Boolean, + cursorPosition: Int, + ): ImmutableList { + if (text.isEmpty() || cursorPosition == 0 || hasSelection) return persistentListOf() val word = partWordFinder.getLeadPartOfWord(text, cursorPosition) - ?: return emptyList() + ?: return persistentListOf() val suggestions = repository.getWordsDictionary() .filter { it.startsWith(word, ignoreCase = false) && it != word } - .toList() + .toPersistentList() return suggestions } @@ -96,7 +103,7 @@ internal class DefaultSeedPhraseInteractor constructor( } private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) { - MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidEntropyLength + MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidWordCount MnemonicErrorResult.InvalidEntropyLength -> SeedPhraseError.InvalidEntropyLength MnemonicErrorResult.InvalidWordsFile -> SeedPhraseError.InvalidWordsFile MnemonicErrorResult.InvalidChecksum -> SeedPhraseError.InvalidChecksum diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt index 43ed7ac068..2c0f319c19 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt @@ -1,6 +1,7 @@ package com.tangem.feature.onboarding.domain import com.tangem.crypto.bip39.Mnemonic +import kotlinx.collections.immutable.ImmutableList /** [REDACTED_AUTHOR] @@ -10,7 +11,7 @@ interface SeedPhraseInteractor { suspend fun getMnemonicComponents(): Result> suspend fun isWordMatch(word: String): Boolean suspend fun validateMnemonicString(text: String): Result> - suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List + suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): ImmutableList suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult companion object { diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt index baf6d039cc..d593c179ed 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.onboarding.presentation.wallet2.analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent sealed class SeedPhraseEvents( event: String, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt index 74b732e86b..44bdac25c3 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt @@ -52,7 +52,7 @@ data class ImportSeedPhraseState( val onSuggestedPhraseClick: (Int) -> Unit, val buttonCreateWallet: ButtonState, val invalidWords: Set = emptySet(), - val suggestionsList: List = emptyList(), + val suggestionsList: ImmutableList = persistentListOf(), val error: SeedPhraseError? = null, ) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index d29fd51838..be8a31f5df 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -1,6 +1,7 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui import androidx.compose.animation.* +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.OutlinedTextField @@ -11,10 +12,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset -import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.Notifier import com.tangem.core.ui.components.PrimaryButtonIconStart import com.tangem.core.ui.components.TangemTextFieldsDefault +import com.tangem.core.ui.components.buttons.common.* import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.R import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState @@ -23,6 +29,8 @@ import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Onboardi import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseErrorConverter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf /** [REDACTED_AUTHOR] @@ -48,7 +56,9 @@ fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Mo state = state, ) SuggestionsBlock( - state = state, + modifier = Modifier.padding(top = TangemTheme.dimens.size4), + suggestionsList = state.suggestionsList, + onClick = { index -> state.onSuggestedPhraseClick(index) }, ) } } @@ -114,25 +124,86 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif @Suppress("ReusedModifierInstance") @Composable -private fun SuggestionsBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) { +private fun SuggestionsBlock( + suggestionsList: ImmutableList, + onClick: (Int) -> Unit, + modifier: Modifier = Modifier, +) { AnimatedVisibility( enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }), exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(), - visible = state.suggestionsList.isNotEmpty(), + visible = suggestionsList.isNotEmpty(), ) { LazyRow( - modifier = modifier.fillMaxSize(), + modifier = modifier, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16), ) { - items(state.suggestionsList.size) { index -> - PrimaryButton( - modifier = Modifier - .height(TangemTheme.dimens.size46) - .padding(all = TangemTheme.dimens.size4), - text = state.suggestionsList[index], - onClick = { state.onSuggestedPhraseClick(index) }, + items(suggestionsList.size) { index -> + SuggestionButton( + modifier = Modifier.rowPadding( + index = index, + rowSize = suggestionsList.size, + outSide = TangemTheme.dimens.size0, + inSide = TangemTheme.dimens.size4, + ), + text = suggestionsList[index], + onClick = { onClick(index) }, ) } } } -} \ No newline at end of file +} + +@Composable +private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { + Box(modifier = modifier.clickable(onClick = onClick)) { + Notifier( + text = text, + backgroundColor = TangemTheme.colors.background.action, + textColor = TangemTheme.colors.text.primary2, + ) + } +} + +private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) { + 0 -> this.padding(start = outSide, end = inSide) + rowSize - 1 -> this.padding(start = inSide, end = outSide) + else -> this.padding(horizontal = inSide) +} + +@Preview +@Composable +private fun SuggestionsBlockPreview_Light( + @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, +) { + TangemTheme(isDark = false) { + SuggestionsBlock( + suggestionsList = suggestions, + onClick = {}, + ) + } +} + +@Preview +@Composable +private fun SuggestionsBlockPreview_Dark( + @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, +) { + TangemTheme(isDark = true) { + SuggestionsBlock( + suggestionsList = suggestions, + onClick = {}, + ) + } +} + +private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider>( + collection = listOf( + persistentListOf( + "one", + "each", + "explorer", + "unknown", + ), + ), +) \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt index 27a76c7f38..4134269aaa 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.sp import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.onboarding.presentation.wallet2.model.DescriptionResource @@ -62,9 +63,13 @@ fun DescriptionTitleText(text: String) { @Composable fun DescriptionSubTitleText(text: String) { + // the text style made similar to TextViewOnboarding.Body Text( text = text, - style = TangemTheme.typography.subtitle1, + style = TangemTheme.typography.body1.copy( + lineHeight = 20.sp, + letterSpacing = 0.03.sp, + ), color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt index 5ffb9b5c41..794ec1d599 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.text.input.TextFieldValue import com.tangem.feature.onboarding.domain.InsertSuggestionResult import com.tangem.feature.onboarding.domain.SeedPhraseError import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState +import kotlinx.collections.immutable.ImmutableList /** [REDACTED_AUTHOR] @@ -39,12 +40,14 @@ class ImportSeedPhraseStateBuilder { ), ) - fun updateSuggestions(uiState: OnboardingSeedPhraseState, suggestions: List): OnboardingSeedPhraseState = - uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - suggestionsList = suggestions, - ), - ) + fun updateSuggestions( + uiState: OnboardingSeedPhraseState, + suggestions: ImmutableList, + ): OnboardingSeedPhraseState = uiState.copy( + importSeedPhraseState = uiState.importSeedPhraseState.copy( + suggestionsList = suggestions, + ), + ) fun insertSuggestionWord( uiState: OnboardingSeedPhraseState, diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt index dbb553ba63..9a44793b6f 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt @@ -14,7 +14,7 @@ class SeedPhraseErrorConverter : ModuleMessageConverter { + SeedPhraseError.InvalidChecksum -> { context.getString(R.string.onboarding_seed_mnemonic_invalid_checksum) } is SeedPhraseError.InvalidWords -> { diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt index b5d2b6a8d7..c7d9c234ba 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt @@ -22,10 +22,10 @@ import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.extensions.isEven import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject /** @@ -67,7 +67,6 @@ class SeedPhraseViewModel @Inject constructor( ) private val textFieldsDebouncers = mutableMapOf() - private var suggestionWordInserted: AtomicBoolean = AtomicBoolean(false) private var generatedMnemonicComponents: List? = null private var importedMnemonicComponents: List? = null @@ -171,13 +170,17 @@ class SeedPhraseViewModel @Inject constructor( if (fieldState.isError != hasError) { updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) } } - - val allFieldsWithoutError = SeedPhraseField.values() + val isCreateWalletButtonEnabled = SeedPhraseField.values() .map { field -> field.getState(uiState) } - .all { fieldState -> !fieldState.isError } + .all { fieldState -> fieldState.textFieldValue.text.isNotEmpty() && !fieldState.isError } - if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != allFieldsWithoutError) { - updateUi { uiBuilder.checkSeedPhrase.updateCreateWalletButton(uiState, allFieldsWithoutError) } + if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != isCreateWalletButtonEnabled) { + updateUi { + uiBuilder.checkSeedPhrase.updateCreateWalletButton( + uiState = uiState, + enabled = isCreateWalletButtonEnabled, + ) + } } } } @@ -197,19 +200,14 @@ class SeedPhraseViewModel @Inject constructor( val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER) when { - suggestionWordInserted.getAndSet(false) -> { - debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) { - validateMnemonic(inputMnemonic) - } - } - isSameText && !isCursorMoved -> { - // do nothing - } isSameText && isCursorMoved -> { debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) { updateSuggestions(fieldState) } } + isSameText && !isCursorMoved -> { + // do nothing + } else -> { debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) { updateSuggestions(fieldState) @@ -243,7 +241,6 @@ class SeedPhraseViewModel @Inject constructor( is SeedPhraseError.InvalidWords -> { uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words) } - else -> uiState } updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) } @@ -366,7 +363,6 @@ class SeedPhraseViewModel @Inject constructor( private fun buttonSuggestedPhraseClick(suggestionIndex: Int) { viewModelScope.launchSingle { - suggestionWordInserted.set(true) val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex] val cursorPosition = textFieldValue.selection.end @@ -378,7 +374,7 @@ class SeedPhraseViewModel @Inject constructor( ) updateUi { val mediateState = uiBuilder.importSeedPhrase.insertSuggestionWord(uiState, insertResult) - uiBuilder.importSeedPhrase.updateSuggestions(mediateState, emptyList()) + uiBuilder.importSeedPhrase.updateSuggestions(mediateState, persistentListOf()) } } } diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 1f66fc2708..0649e10587 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { /** Core modules */ implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:res")) implementation(project(":core:utils")) implementation(project(":core:ui")) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt index e82990adf2..a5d37a1b19 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/analytics/ReferralEvents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.referral.analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent sealed class ReferralEvents(event: String) : AnalyticsEvent(REFERRAL_PROGRAM_CATEGORY, event) { diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index f4a0857938..8248b21113 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -10,6 +10,7 @@ plugins { dependencies { /** Core modules */ implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:featuretoggles")) implementation(project(":core:utils")) implementation(project(":core:ui")) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index e808db167c..d2395cf29b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.analytics -import com.tangem.core.analytics.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsEvent sealed class SwapEvents( event: String, @@ -13,7 +13,6 @@ sealed class SwapEvents( ) object SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - object ReceiveTokenClicked : SwapEvents(event = "Receive Token Clicked") object ChooseTokenScreenOpened : SwapEvents(event = "Choose Token Screen Opened") object SearchTokenClicked : SwapEvents(event = "Searched Token Clicked") data class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( @@ -24,7 +23,6 @@ sealed class SwapEvents( object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") object ButtonPermissionApproveClicked : SwapEvents(event = "Button - Permission Approve") object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") - object ButtonPermitAndSwapClicked : SwapEvents(event = "Button - Permit and Swap") object ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") object SwapInProgressScreen : SwapEvents(event = "Swap in Progress Screen Opened") } diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index aa624c46ac..d3af011dcd 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -24,6 +24,9 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) + + implementation(deps.kotlin.immutable.collections) /** DI */ implementation(deps.hilt.android) @@ -32,6 +35,7 @@ dependencies { /** Core modules */ implementation(projects.core.featuretoggles) implementation(projects.core.ui) + implementation(projects.core.navigation) /** Feature Apis */ implementation(projects.features.tokendetails.api) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt index 47a8b90f2e..3a0dc79713 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.di +import com.tangem.core.navigation.NavigationStateHolder import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import dagger.Module @@ -14,5 +15,7 @@ internal object TokenDetailsRouterModule { @Provides @ActivityScoped - fun provideTokenDetailsRouter(): TokenDetailsRouter = DefaultTokenDetailsRouter() + fun provideTokenDetailsRouter(navigationStateHolder: NavigationStateHolder): TokenDetailsRouter { + return DefaultTokenDetailsRouter(navigationStateHolder) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt new file mode 100644 index 0000000000..d9e0a1bcf7 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.tokendetails.presentation + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.fragment.app.Fragment +import androidx.hilt.navigation.compose.hiltViewModel +import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class TokenDetailsFragment : Fragment() { + + @Inject + lateinit var tokenDetailsRouter: TokenDetailsRouter + + private val internalTokenDetailsRouter: InnerTokenDetailsRouter + get() = requireNotNull(tokenDetailsRouter as? InnerTokenDetailsRouter) { + "internalTokenDetailsRouter should be instance of InnerTokenDetailsRouter" + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return ComposeView(inflater.context).apply { + setContent { + TangemTheme { + val systemBarsColor = TangemTheme.colors.background.secondary + SystemBarsEffect { + setSystemBarsColor(systemBarsColor) + } + + val viewModel = hiltViewModel() + viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter + TokenDetailsScreen(state = viewModel.uiState) + } + } + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 6bfaf4dcaa..73819028da 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -1,10 +1,17 @@ package com.tangem.feature.tokendetails.presentation.router import androidx.fragment.app.Fragment -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.core.navigation.NavigationAction +import com.tangem.core.navigation.NavigationStateHolder +import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment -internal class DefaultTokenDetailsRouter : TokenDetailsRouter { +internal class DefaultTokenDetailsRouter( + private val navigationStateHolder: NavigationStateHolder, +) : InnerTokenDetailsRouter { - // TODO: Doston fix it in next PRs - override fun getEntryFragment(): Fragment = Fragment() + override fun getEntryFragment(): Fragment = TokenDetailsFragment() + + override fun popBackStack() { + navigationStateHolder.navigate(NavigationAction.PopBackTo()) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt new file mode 100644 index 0000000000..89448deb0d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.tokendetails.presentation.router + +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter + +internal interface InnerTokenDetailsRouter : TokenDetailsRouter { + + fun popBackStack() +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt new file mode 100644 index 0000000000..5c20a6664d --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -0,0 +1,93 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails + +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.features.tokendetails.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal object TokenDetailsPreviewData { + + val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(onBackClick = {}, onMoreClick = {}) + + val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState( + name = "Stellar (XLM) with long name test", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + currency = TokenInfoBlockState.Currency.Native, + ) + val tokenInfoBlockStateWithLongName = TokenInfoBlockState( + name = "Tether (USDT) with long name test", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png", + currency = TokenInfoBlockState.Currency.Token( + networkName = "ERC20", + networkIcon = R.drawable.img_eth_22, + blockchainName = "Ethereum", + ), + ) + + val tokenInfoBlockState = TokenInfoBlockState( + name = "Tether USDT", + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png", + currency = TokenInfoBlockState.Currency.Token( + networkName = "ERC20", + networkIcon = R.drawable.img_eth_22, + blockchainName = "Ethereum", + ), + ) + + private val actionButtons = persistentListOf( + ActionButtonConfig( + text = "Buy", + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Send", + iconResId = R.drawable.ic_arrow_up_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Receive", + iconResId = R.drawable.ic_arrow_down_24, + onClick = {}, + ), + ActionButtonConfig( + text = "Exchange", + iconResId = R.drawable.ic_exchange_vertical_24, + onClick = {}, + ), + ) + + private val disabledActionButtons = actionButtons.map { it.copy(enabled = false) }.toPersistentList() + + val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = disabledActionButtons) + val balanceContent = TokenDetailsBalanceBlockState.Content( + actionButtons = actionButtons, + fiatBalance = "123,00$", + cryptoBalance = "866,96 USDT", + ) + val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = disabledActionButtons) + + val marketPriceContent = MarketPriceBlockState.Content( + currencyName = "USDT", + price = "98900 $", + priceChangeConfig = PriceChangeConfig( + valueInPercent = "10.89%", + type = PriceChangeConfig.Type.UP, + ), + ) + + private val marketPriceLoading = MarketPriceBlockState.Loading(currencyName = "USDT") + + val tokenDetailsState = TokenDetailsState( + topAppBarConfig = tokenDetailsTopAppBarConfig, + tokenInfoBlockState = tokenInfoBlockState, + tokenBalanceBlockState = balanceLoading, + marketPriceBlockState = marketPriceLoading, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt new file mode 100644 index 0000000000..a67edd1a61 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import kotlinx.collections.immutable.ImmutableList + +sealed class TokenDetailsBalanceBlockState { + + abstract val actionButtons: ImmutableList + + data class Loading( + override val actionButtons: ImmutableList, + ) : TokenDetailsBalanceBlockState() + + data class Content( + override val actionButtons: ImmutableList, + val fiatBalance: String, + val cryptoBalance: String, + ) : TokenDetailsBalanceBlockState() + + data class Error( + override val actionButtons: ImmutableList, + ) : TokenDetailsBalanceBlockState() +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt new file mode 100644 index 0000000000..8c7ba02c14 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState + +data class TokenDetailsState( + val topAppBarConfig: TokenDetailsTopAppBarConfig, + val tokenInfoBlockState: TokenInfoBlockState, + val tokenBalanceBlockState: TokenDetailsBalanceBlockState, + val marketPriceBlockState: MarketPriceBlockState, +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt new file mode 100644 index 0000000000..8ed33f724b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +data class TokenDetailsTopAppBarConfig( + val onBackClick: () -> Unit, + val onMoreClick: () -> Unit, +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt new file mode 100644 index 0000000000..ac4c80561c --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.annotation.DrawableRes + +data class TokenInfoBlockState( + val name: String, + val iconUrl: String, + val currency: Currency, +) { + sealed class Currency { + object Native : Currency() + + /** + * @param networkName - token standard. Samples: ERC20, BEP20, BEP2, TRC20 and etc. + * @param blockchainName - token's blockchain name. Ethereum, Tron and etc. + */ + data class Token( + val networkName: String, + val blockchainName: String, + @DrawableRes val networkIcon: Int, + ) : Currency() + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt new file mode 100644 index 0000000000..a8a20dfa34 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.marketprice.MarketPriceBlock +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock + +@Composable +internal fun TokenDetailsScreen(state: TokenDetailsState) { + Scaffold( + topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + Column( + modifier = Modifier + .padding(paddingValues = scaffoldPaddings) + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + TokenInfoBlock(state = state.tokenInfoBlockState) + TokenDetailsBalanceBlock(state = state.tokenBalanceBlockState) + MarketPriceBlock(state = state.marketPriceBlockState) + } + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsScreen_LightTheme() { + TangemTheme(isDark = false) { + TokenDetailsScreen(state = TokenDetailsPreviewData.tokenDetailsState) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsScreen_DarkTheme() { + TangemTheme(isDark = true) { + TokenDetailsScreen(state = TokenDetailsPreviewData.tokenDetailsState) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt new file mode 100644 index 0000000000..df54b41461 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -0,0 +1,136 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.buttons.HorizontalActionChips +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState +import com.tangem.features.tokendetails.impl.R + +@Composable +internal fun TokenDetailsBalanceBlock(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { + Surface( + modifier = modifier.fillMaxWidth(), + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + ) { + Column { + Text( + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing12, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + text = stringResource(id = R.string.onboarding_balance_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + ) + FiatBalance( + state = state, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = TangemTheme.dimens.spacing12), + ) + CryptoBalance( + state = state, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing4) + .padding(horizontal = TangemTheme.dimens.spacing12), + ) + + HorizontalActionChips( + buttons = state.actionButtons, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12), + ) + } + } +} + +@Composable +private fun FiatBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { + when (state) { + is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer( + modifier = modifier.size( + width = TangemTheme.dimens.size102, + height = TangemTheme.dimens.size24, + ), + ) + is TokenDetailsBalanceBlockState.Content -> Text( + modifier = modifier, + text = state.fiatBalance, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + is TokenDetailsBalanceBlockState.Error -> Text( + modifier = modifier, + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Composable +private fun CryptoBalance(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { + when (state) { + is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer( + modifier = modifier.size( + width = TangemTheme.dimens.size70, + height = TangemTheme.dimens.size16, + ), + ) + is TokenDetailsBalanceBlockState.Content -> Text( + modifier = modifier, + text = state.cryptoBalance, + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.primary1, + ) + is TokenDetailsBalanceBlockState.Error -> Text( + modifier = modifier, + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.primary1, + ) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsBalanceBlock_LightTheme( + @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, +) { + TangemTheme(isDark = false) { + TokenDetailsBalanceBlock(state) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsBalanceBlock_DarkTheme( + @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, +) { + TangemTheme(isDark = true) { + TokenDetailsBalanceBlock(state) + } +} + +private class TokenDetailsBalanceBlockStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenDetailsPreviewData.balanceLoading, + TokenDetailsPreviewData.balanceContent, + TokenDetailsPreviewData.balanceError, + ), +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt new file mode 100644 index 0000000000..a7901787f0 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -0,0 +1,58 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.features.tokendetails.impl.R +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { + TopAppBar( + navigationIcon = { + IconButton(onClick = config.onBackClick) { + Icon( + painter = painterResource(id = R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Back", + ) + } + }, + title = {}, + actions = { + IconButton(onClick = config.onMoreClick) { + Icon( + painter = painterResource(id = R.drawable.ic_more_vertical_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "More", + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + titleContentColor = TangemTheme.colors.icon.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ), + scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(), + ) +} + +@Preview +@Composable +private fun Preview_TokenDetailsTopAppBar_LightTheme() { + TangemTheme(isDark = false) { + TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig) + } +} + +@Preview +@Composable +private fun Preview_TokenDetailsTopAppBar_DarkTheme() { + TangemTheme(isDark = true) { + TokenDetailsTopAppBar(config = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt new file mode 100644 index 0000000000..96ac8f6bf3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenInfoBlock.kt @@ -0,0 +1,141 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import coil.compose.rememberAsyncImagePainter +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfoBlockState +import com.tangem.features.tokendetails.impl.R + +@Composable +internal fun TokenInfoBlock(state: TokenInfoBlockState, modifier: Modifier = Modifier) { + Row(modifier = modifier.fillMaxWidth()) { + Column( + modifier = Modifier.weight(1F), + ) { + Text( + text = state.name, + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, + ) + NetworkInfoText(state.currency) + } + + val tokenIconPainter = when (LocalInspectionMode.current) { + // show drawable res in preview + true -> painterResource(id = R.drawable.img_stellar_22) + false -> rememberAsyncImagePainter(model = state.iconUrl) + } + + Image( + modifier = Modifier.size(TangemTheme.dimens.size48), + painter = tokenIconPainter, + contentDescription = null, + ) + } +} + +@Composable +private fun NetworkInfoText(currency: TokenInfoBlockState.Currency) { + when (currency) { + TokenInfoBlockState.Currency.Native -> { + Text( + text = stringResource(id = R.string.common_main_network), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption, + ) + } + is TokenInfoBlockState.Currency.Token -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + val state = extractNetwork(tokenCurrency = currency) + Text( + text = state.normalText, + style = TangemTheme.typography.caption, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = currency.networkIcon), + tint = Color.Unspecified, + contentDescription = null, + ) + Text( + text = state.boldText, + style = TangemTheme.typography.caption.copy(fontWeight = FontWeight.Medium), + color = TangemTheme.colors.text.primary1, + ) + } + } + } +} + +private const val SEPARATOR = " %image% " + +@Composable +private fun extractNetwork(tokenCurrency: TokenInfoBlockState.Currency.Token): ExtractedTokenNetworkText { + val splitString = stringResource( + id = R.string.token_details_token_type_subtitle, + formatArgs = arrayOf( + tokenCurrency.networkName, + tokenCurrency.blockchainName, + ), + ).split(SEPARATOR) + + return remember(splitString) { + ExtractedTokenNetworkText( + normalText = splitString.firstOrNull().orEmpty(), + boldText = splitString.getOrNull(1).orEmpty(), + ) + } +} + +private data class ExtractedTokenNetworkText(val normalText: String, val boldText: String) + +@Preview +@Composable +private fun Preview_TokenInfoBlock_LightTheme( + @PreviewParameter(TokenInfoStateProvider::class) + state: TokenInfoBlockState, +) { + TangemTheme(isDark = false) { + TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary)) + } +} + +@Preview +@Composable +private fun Preview_TokenInfoBlock_DarkTheme( + @PreviewParameter(TokenInfoStateProvider::class) + state: TokenInfoBlockState, +) { + TangemTheme(isDark = true) { + TokenInfoBlock(state, Modifier.background(TangemTheme.colors.background.secondary)) + } +} + +private class TokenInfoStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenDetailsPreviewData.tokenInfoBlockState, + TokenDetailsPreviewData.tokenInfoBlockStateWithLongName, + TokenDetailsPreviewData.tokenInfoBlockStateWithLongNameInMainCurrency, + ), +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt new file mode 100644 index 0000000000..4a75d46fb5 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject +import kotlin.properties.Delegates + +private const val LOADING_DELAY = 4_000L + +@HiltViewModel +internal class TokenDetailsViewModel @Inject constructor() : ViewModel() { + + var router: InnerTokenDetailsRouter by Delegates.notNull() + + var uiState by mutableStateOf(getInitialState()) + private set + + init { + // simulate loading state + viewModelScope.launch { + delay(LOADING_DELAY) + uiState = uiState.copy( + tokenBalanceBlockState = TokenDetailsPreviewData.balanceContent, + marketPriceBlockState = TokenDetailsPreviewData.marketPriceContent, + ) + } + } + + private fun getInitialState() = TokenDetailsPreviewData.tokenDetailsState.copy( + topAppBarConfig = TokenDetailsPreviewData.tokenDetailsTopAppBarConfig.copy( + onBackClick = ::onBackClick, + ), + ) + + private fun onBackClick() { + router.popBackStack() + } +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 2e5651a89c..55e9edc0c3 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -26,8 +26,11 @@ dependencies { implementation(deps.compose.reorderable) /** Other libraries */ + implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) + implementation(deps.arrow.core) /** DI */ implementation(deps.hilt.android) @@ -37,14 +40,17 @@ dependencies { implementation(project(":core:featuretoggles")) implementation(project(":core:navigation")) implementation(project(":core:ui")) + implementation(projects.core.utils) /** Feature Apis */ implementation(project(":features:wallet:api")) /** Domain modules */ implementation(project(":common")) + implementation(projects.domain.card) implementation(project(":domain:legacy")) implementation(project(":domain:models")) implementation(project(":domain:wallets")) implementation(project(":domain:wallets:models")) + implementation(projects.domain.tokens) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 6f6b726f82..5dfb53248c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,8 +1,10 @@ package com.tangem.feature.wallet.presentation.common import com.tangem.core.ui.R +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import com.tangem.feature.wallet.presentation.organizetokens.DraggableItem @@ -18,7 +20,7 @@ internal object WalletPreviewData { val walletTopBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}) val walletCardContentState = WalletCardState.Content( - id = UUID.randomUUID().toString(), + id = UserWalletId(UUID.randomUUID().toString()), title = "Wallet 1", balance = "8923,05 $", additionalInfo = "3 cards • Seed enabled", @@ -27,7 +29,7 @@ internal object WalletPreviewData { ) val walletCardLoadingState = WalletCardState.Loading( - id = UUID.randomUUID().toString(), + id = UserWalletId(UUID.randomUUID().toString()), title = "Wallet 1", additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, @@ -35,7 +37,7 @@ internal object WalletPreviewData { ) val walletCardHiddenContentState = WalletCardState.HiddenContent( - id = UUID.randomUUID().toString(), + id = UserWalletId(UUID.randomUUID().toString()), title = "Wallet 1", additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, @@ -43,21 +45,23 @@ internal object WalletPreviewData { ) val walletCardErrorState = WalletCardState.Error( - id = UUID.randomUUID().toString(), + id = UserWalletId(UUID.randomUUID().toString()), title = "Wallet 1", additionalInfo = "3 cards • Seed enabled", imageResId = R.drawable.ill_businessman_3d, onClick = null, ) + val wallets = mapOf( + UserWalletId(stringValue = "123") to walletCardContentState, + UserWalletId(stringValue = "321") to walletCardLoadingState, + UserWalletId(stringValue = "42") to walletCardHiddenContentState, + UserWalletId(stringValue = "24") to walletCardErrorState, + ) + val walletListConfig = WalletsListConfig( selectedWalletIndex = 0, - wallets = persistentListOf( - walletCardContentState, - walletCardLoadingState, - walletCardHiddenContentState, - walletCardErrorState, - ), + wallets = wallets.values.toPersistentList(), onWalletChange = {}, ) @@ -196,7 +200,16 @@ internal object WalletPreviewData { ), ) - val manageButtons = persistentListOf( + val bottomSheet = WalletBottomSheetConfig( + isShow = false, + onDismissRequest = {}, + content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + onUnlockClick = {}, + onScanClick = {}, + ), + ) + + private val manageButtons = persistentListOf( WalletManageButton.Buy(onClick = {}), WalletManageButton.Send(onClick = {}), WalletManageButton.Receive(onClick = {}), @@ -204,15 +217,6 @@ internal object WalletPreviewData { WalletManageButton.CopyAddress(onClick = {}), ) - val marketplaceBlockContent = WalletMarketplaceBlockState.Content( - currencyName = "BTC", - price = "0.11$", - priceChangeConfig = PriceChangeConfig( - valueInPercent = "5.16%", - type = PriceChangeConfig.Type.UP, - ), - ) - val multicurrencyWalletScreenState = WalletStateHolder.MultiCurrencyContent( onBackClick = {}, topBarConfig = walletTopBarConfig, @@ -266,12 +270,17 @@ internal object WalletPreviewData { ), ), ), + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), notifications = persistentListOf( WalletNotification.UnreachableNetworks, WalletNotification.LikeTangemApp(onClick = {}), - WalletNotification.NeedToBackup(onClick = {}), + WalletNotification.BackupCard(onClick = {}), WalletNotification.ScanCard(onClick = {}), ), + bottomSheet = bottomSheet, onOrganizeTokensClick = {}, ) @@ -298,8 +307,20 @@ internal object WalletPreviewData { ), ), ), + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), - buttons = manageButtons, - marketplaceBlockState = marketplaceBlockContent, + buttons = manageButtons.map(WalletManageButton::config).toPersistentList(), + bottomSheet = bottomSheet, + marketPriceBlockState = MarketPriceBlockState.Content( + currencyName = "BTC", + price = "98900.12$", + priceChangeConfig = PriceChangeConfig( + valueInPercent = "5.16%", + type = PriceChangeConfig.Type.UP, + ), + ), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index 62b877549a..5c4cb19fb7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -28,11 +28,11 @@ import androidx.constraintlayout.compose.Dimension import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.marketprice.PriceChangeConfig import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTypography import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.common.state.TokenItemState.TokenOptionsState import org.burnoutcrew.reorderable.ReorderableLazyListState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/PriceChangeConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/PriceChangeConfig.kt deleted file mode 100644 index 9d6b4989ee..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/PriceChangeConfig.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.wallet.presentation.common.state - -/** - * Price changing config - * - * @property valueInPercent value in percent - * @property type type [Type] - */ -internal data class PriceChangeConfig(val valueInPercent: String, val type: Type) { - - /** Price changing type */ - enum class Type { - UP, DOWN - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 5489b5e602..b1c8417e9e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.marketprice.PriceChangeConfig /** Token item state */ @Immutable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 12a164c4fa..2f30c3d355 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -4,6 +4,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.hilt.navigation.compose.hiltViewModel @@ -41,6 +42,8 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation ) { composable(WalletScreens.WALLET.name) { val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } + LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel) + WalletScreen(state = viewModel.uiState) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt new file mode 100644 index 0000000000..5aafcd4464 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +import androidx.annotation.DrawableRes +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.feature.wallet.impl.R + +/** + * Wallet bottom sheet config + * + * @property isShow flag that determine if bottom sheet is shown + * @property onDismissRequest lambda be invoked when bottom sheet is dismissed + * @property content content config + * +[REDACTED_AUTHOR] + */ +// TODO: Finalize notification strings [REDACTED_JIRA] +internal data class WalletBottomSheetConfig( + val isShow: Boolean, + val onDismissRequest: () -> Unit, + val content: BottomSheetContentConfig, +) { + + sealed class BottomSheetContentConfig( + open val title: TextReference, + open val subtitle: TextReference, + @DrawableRes open val iconResId: Int, + open val tint: Color? = null, + val primaryButtonConfig: ButtonConfig, + val secondaryButtonConfig: ButtonConfig? = null, + ) { + + data class ButtonConfig(val text: String, val onClick: () -> Unit, @DrawableRes val iconResId: Int? = null) + + data class UnlockWallets( + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + ) : BottomSheetContentConfig( + title = TextReference.Str(value = "Unlock needed"), + subtitle = TextReference.Str( + value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor " + + "incididunt ut labore et dolore magna aliqua.", + ), + iconResId = R.drawable.ic_locked_24, + tint = TangemColorPalette.Black, + primaryButtonConfig = ButtonConfig(text = "Unlock", onClick = onUnlockClick), + secondaryButtonConfig = ButtonConfig( + text = "Scan card", + onClick = onScanClick, + iconResId = R.drawable.ic_tangem_24, + ), + ) + + data class LikeTangemApp( + val onRateTheAppClick: () -> Unit, + val onShareClick: () -> Unit, + ) : BottomSheetContentConfig( + title = TextReference.Str(value = "Like Tangem App?"), + subtitle = TextReference.Str(value = "How was your experience with our app? Let us know:"), + iconResId = R.drawable.ic_star_24, + tint = TangemColorPalette.Tangerine, + primaryButtonConfig = ButtonConfig(text = "Rate the app", onClick = onRateTheAppClick), + secondaryButtonConfig = ButtonConfig(text = "Share feedback", onClick = onShareClick), + ) + + data class MultiWalletAlreadySignedHashes(val onLearnClick: () -> Unit) : BottomSheetContentConfig( + title = TextReference.Res( + id = R.string.warning_important_security_info, + formatArgs = WrappedList(listOf("\u26A0")), + ), + subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously), + iconResId = R.drawable.img_attention_20, + tint = null, + primaryButtonConfig = ButtonConfig(text = "Learn more", onClick = onLearnClick), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt index a04b0e3f45..b1f88360b1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletCardState.kt @@ -2,13 +2,14 @@ package com.tangem.feature.wallet.presentation.wallet.state import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.domain.wallets.models.UserWalletId /** Wallet card state */ @Immutable internal sealed interface WalletCardState { /** Id */ - val id: String + val id: UserWalletId /** Title */ val title: String @@ -34,7 +35,7 @@ internal sealed interface WalletCardState { * @property balance wallet balance */ data class Content( - override val id: String, + override val id: UserWalletId, override val title: String, override val additionalInfo: String, override val imageResId: Int?, @@ -52,7 +53,7 @@ internal sealed interface WalletCardState { * @property onClick lambda be invoked when wallet card is clicked */ data class Loading( - override val id: String, + override val id: UserWalletId, override val title: String, override val additionalInfo: String, override val imageResId: Int?, @@ -69,7 +70,7 @@ internal sealed interface WalletCardState { * @property onClick lambda be invoked when wallet card is clicked */ data class HiddenContent( - override val id: String, + override val id: UserWalletId, override val title: String, override val additionalInfo: String, override val imageResId: Int?, @@ -86,7 +87,7 @@ internal sealed interface WalletCardState { * @property onClick lambda be invoked when wallet card is clicked */ data class Error( - override val id: String, + override val id: UserWalletId, override val title: String, override val additionalInfo: String, override val imageResId: Int?, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMarketplaceBlockState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMarketplaceBlockState.kt deleted file mode 100644 index 591c548d9e..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletMarketplaceBlockState.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state - -import com.tangem.feature.wallet.presentation.common.state.PriceChangeConfig - -/** - * Wallet marketplace component state - * - * @property currencyName currency name - * -[REDACTED_AUTHOR] - */ -internal sealed class WalletMarketplaceBlockState(open val currencyName: String) { - - /** - * Loading state - * - * @property currencyName currency name - */ - data class Loading(override val currencyName: String) : WalletMarketplaceBlockState(currencyName = currencyName) - - /** - * Content state - * - * @property currencyName currency name - * @property price price - * @property priceChangeConfig price change config - */ - data class Content( - override val currencyName: String, - val price: String, - val priceChangeConfig: PriceChangeConfig, - ) : WalletMarketplaceBlockState(currencyName = currencyName) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt index 39e760dd4f..66079037c6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletNotification.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.notifications.NotificationState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.res.TangemColorPalette import com.tangem.feature.wallet.impl.R @@ -11,26 +13,107 @@ import com.tangem.feature.wallet.impl.R * [REDACTED_AUTHOR] */ +// TODO: Finalize notification strings [REDACTED_JIRA] sealed class WalletNotification(open val state: NotificationState) { + /** Clickable notification */ + sealed interface Clickable { + + /** Lambda be invoked when notification is clicked */ + val onClick: () -> Unit + } + + /** "Development card" notification */ + object DevCard : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.alert_developer_card), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** "Test card" notification */ + object TestCard : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.warning_testnet_card_message), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** "Demo card" notification */ + object DemoCard : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.alert_demo_message), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** "Card verification failed" notification */ + object CardVerificationFailed : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.warning_failed_to_verify_card_title), + subtitle = TextReference.Res(id = R.string.warning_failed_to_verify_card_message), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** + * "Wallet already signed hashes" notification + * + * @property onClick lambda be invoked when notification is clicked + */ + data class WalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions), + iconResId = R.drawable.img_attention_20, + onClick = onClick, + tint = null, + ), + ) + + /** + * "Multi wallet already signed hashes" notification + * + * @property onClick lambda be invoked when notification is clicked + */ + data class MultiWalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Res( + id = R.string.warning_important_security_info, + formatArgs = WrappedList(listOf("\u26A0")), + ), + subtitle = TextReference.Res(id = R.string.warning_signed_tx_previously), + iconResId = R.drawable.img_attention_20, + onClick = onClick, + tint = null, + ), + ) + /** * "Backup the card" notification * * @property onClick lambda be invoked when notification is clicked */ - data class NeedToBackup(val onClick: () -> Unit) : WalletNotification( - state = NotificationState.Action( - title = "Backup your card", - iconResId = R.drawable.ic_alert_circle_24, + data class BackupCard(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Str(value = "Backup your card"), + iconResId = R.drawable.img_attention_20, onClick = onClick, - tint = TangemColorPalette.Amaranth, + tint = null, ), ) /** "Unreachable networks" notification */ object UnreachableNetworks : WalletNotification( state = NotificationState.Simple( - title = "Some networks are unreachable", + title = TextReference.Str(value = "Some networks are unreachable"), iconResId = R.drawable.img_attention_20, tint = null, ), @@ -41,9 +124,9 @@ sealed class WalletNotification(open val state: NotificationState) { * * @property onClick lambda be invoked when notification is clicked */ - data class LikeTangemApp(val onClick: () -> Unit) : WalletNotification( - state = NotificationState.Action( - title = "Like Tangem App?", + data class LikeTangemApp(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Str(value = "Like Tangem App?"), iconResId = R.drawable.ic_star_24, onClick = onClick, tint = TangemColorPalette.Tangerine, @@ -55,11 +138,24 @@ sealed class WalletNotification(open val state: NotificationState) { * * @property onClick lambda be invoked when notification is clicked */ - data class ScanCard(val onClick: () -> Unit) : WalletNotification( - state = NotificationState.Action( - title = "Scan your card to continue", + data class ScanCard(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Str(value = "Scan your card to continue"), iconResId = R.drawable.ic_tangem_24, onClick = onClick, ), ) + + /** + * "Unlock wallets" notification + * + * @property onClick lambda be invoked when notification is clicked + */ + data class UnlockWallets(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Clickable( + title = TextReference.Str(value = "Unlock needed"), + iconResId = R.drawable.ic_locked_24, + onClick = onClick, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt new file mode 100644 index 0000000000..8aafa384f0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletPullToRefreshConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.wallet.presentation.wallet.state + +/** + * Wallet screen top bar config + * + * @property isRefreshing state is indicator visible + * @property onRefresh lambda be invoked when pulled to refresh + */ +data class WalletPullToRefreshConfig(val isRefreshing: Boolean, val onRefresh: () -> Unit) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt index 02b9d37a3d..169139dacd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateHolder.kt @@ -1,15 +1,20 @@ package com.tangem.feature.wallet.presentation.wallet.state +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf /** * Wallet screen state holder * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property contentItems content items - * @property notifications notifications + * @property onBackClick lambda be invoked when back button is clicked + * @property topBarConfig top bar config + * @property walletsListConfig wallets list config + * @property pullToRefreshConfig pull to refresh config + * @property contentItems content items + * @property notifications notifications * [REDACTED_AUTHOR] */ @@ -17,47 +22,172 @@ internal sealed class WalletStateHolder( open val onBackClick: () -> Unit, open val topBarConfig: WalletTopBarConfig, open val walletsListConfig: WalletsListConfig, + open val pullToRefreshConfig: WalletPullToRefreshConfig, open val contentItems: ImmutableList, open val notifications: ImmutableList, + open val bottomSheet: WalletBottomSheetConfig? = null, ) { + fun copySealed( + onBackClick: () -> Unit = this.onBackClick, + topBarConfig: WalletTopBarConfig = this.topBarConfig, + walletsListConfig: WalletsListConfig = this.walletsListConfig, + pullToRefreshConfig: WalletPullToRefreshConfig = this.pullToRefreshConfig, + contentItems: ImmutableList = this.contentItems, + notifications: ImmutableList = this.notifications, + bottomSheet: WalletBottomSheetConfig? = this.bottomSheet, + ): WalletStateHolder { + return when (this) { + is MultiCurrencyContent -> this.copy( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = contentItems as ImmutableList, + notifications = notifications, + bottomSheet = bottomSheet, + ) + is SingleCurrencyContent -> this.copy( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = contentItems as ImmutableList, + notifications = notifications, + bottomSheet = bottomSheet, + ) + is UnlockWalletContent -> this.copy( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + ) + is Loading -> copy(onBackClick = onBackClick) + } + } + /** * Multi currency wallet content state * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property contentItems content items - * @property notifications notifications - * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + * @property onBackClick lambda be invoked when back button is clicked + * @property topBarConfig top bar config + * @property walletsListConfig wallets list config + * @property pullToRefreshConfig pull to refresh config + * @property contentItems content items + * @property notifications notifications + * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked */ data class MultiCurrencyContent( override val onBackClick: () -> Unit, override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, + override val bottomSheet: WalletBottomSheetConfig? = null, val onOrganizeTokensClick: () -> Unit, - ) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications) + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = contentItems, + bottomSheet = bottomSheet, + notifications = notifications, + ) /** * Single currency wallet content state * - * @property onBackClick lambda be invoked when back button is clicked - * @property topBarConfig top bar config - * @property walletsListConfig wallets list config - * @property contentItems content items - * @property notifications notifications - * @property buttons manage buttons - * @property marketplaceBlockState marketplace block state + * @property onBackClick lambda be invoked when back button is clicked + * @property topBarConfig top bar config + * @property walletsListConfig wallets list config + * @property pullToRefreshConfig pull to refresh config + * @property contentItems content items + * @property notifications notifications + * @property buttons manage buttons + * @property marketPriceBlockState market price block state */ data class SingleCurrencyContent( override val onBackClick: () -> Unit, override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, override val contentItems: ImmutableList, override val notifications: ImmutableList, - val buttons: ImmutableList, - val marketplaceBlockState: WalletMarketplaceBlockState, - ) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications) + override val bottomSheet: WalletBottomSheetConfig? = null, + val buttons: ImmutableList, + val marketPriceBlockState: MarketPriceBlockState, + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = contentItems, + notifications = notifications, + bottomSheet = bottomSheet, + ) + + /** + * Unlock wallet content state + * + * @property onBackClick lambda be invoked when back button is clicked + * @property topBarConfig top bar config + * @property walletsListConfig wallets list config + * @property onUnlockWalletsNotificationClick lambda be invoked when unlock wallets notification is clicked + * @property onBottomSheetDismissRequest lambda be invoked when bottom sheet is dismissed + * @property onUnlockClick lambda be invoked when unlock button is clicked + * @property onScanClick lambda be invoked when scan card button is clicked + */ + data class UnlockWalletContent( + override val onBackClick: () -> Unit, + override val topBarConfig: WalletTopBarConfig, + override val walletsListConfig: WalletsListConfig, + override val pullToRefreshConfig: WalletPullToRefreshConfig, + val onUnlockWalletsNotificationClick: () -> Unit, + val onBottomSheetDismissRequest: () -> Unit, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + contentItems = persistentListOf(WalletContentItemState.Loading), + notifications = persistentListOf(WalletNotification.UnlockWallets(onUnlockWalletsNotificationClick)), + bottomSheet = WalletBottomSheetConfig( + isShow = false, + onDismissRequest = onBottomSheetDismissRequest, + content = WalletBottomSheetConfig.BottomSheetContentConfig.UnlockWallets( + onUnlockClick = onUnlockClick, + onScanClick = onScanClick, + ), + ), + ) + + /** + * Loading state + * + * @property onBackClick lambda be invoked when back button is clicked + */ + data class Loading(override val onBackClick: () -> Unit) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = WalletTopBarConfig(onScanCardClick = {}, onMoreClick = {}), + walletsListConfig = WalletsListConfig( + selectedWalletIndex = 0, + wallets = persistentListOf( + WalletCardState.Loading( + id = UserWalletId(stringValue = ""), + title = "", + additionalInfo = "", + imageResId = null, + ), + ), + onWalletChange = {}, + ), + pullToRefreshConfig = WalletPullToRefreshConfig(isRefreshing = false, onRefresh = {}), + contentItems = persistentListOf(WalletContentItemState.Loading), + notifications = persistentListOf(), + bottomSheet = null, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt new file mode 100644 index 0000000000..21bf52bee0 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/builder/WalletStateFactory.kt @@ -0,0 +1,109 @@ +package com.tangem.feature.wallet.presentation.wallet.state.builder + +import com.tangem.common.Provider +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.* +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class WalletStateFactory( + private val routerProvider: Provider, + private val onScanCardClick: () -> Unit, + private val onWalletChange: (Int) -> Unit, + private val onRefreshSwipe: () -> Unit, +) { + + fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = ::onBackClick) + + fun getContentState(wallets: List): WalletStateHolder { + val cardTypeResolver = requireNotNull(wallets.firstOrNull()).scanResponse.cardTypesResolver + + return if (cardTypeResolver.isMultiwalletAllowed()) { + createMultiCurrencyState(wallets) + } else { + createSingleCurrencyState(wallets) + } + } + + private fun createMultiCurrencyState(wallets: List): WalletStateHolder.MultiCurrencyContent { + return WalletStateHolder.MultiCurrencyContent( + onBackClick = ::onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(wallets), + pullToRefreshConfig = createPullToRefreshConfig(), + contentItems = persistentListOf(), + notifications = persistentListOf(), // TODO: create notifications + bottomSheet = WalletBottomSheetConfig( + // TODO: check notifications + isShow = false, + onDismissRequest = {}, + content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( + onRateTheAppClick = {}, + onShareClick = {}, + ), + ), + onOrganizeTokensClick = routerProvider()::openOrganizeTokensScreen, + ) + } + + private fun createSingleCurrencyState(wallets: List): WalletStateHolder.SingleCurrencyContent { + return WalletStateHolder.SingleCurrencyContent( + onBackClick = ::onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(wallets), + pullToRefreshConfig = createPullToRefreshConfig(), + contentItems = persistentListOf(), + notifications = persistentListOf(), // TODO: create notifications + bottomSheet = WalletBottomSheetConfig( + // TODO: check notifications + isShow = false, + onDismissRequest = {}, + content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( + onRateTheAppClick = {}, + onShareClick = {}, + ), + ), + buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons + // TODO: create market price block + marketPriceBlockState = WalletPreviewData.singleWalletScreenState.marketPriceBlockState, + ) + } + + private fun onBackClick() = routerProvider().popBackStack() + + private fun createTopBarConfig(): WalletTopBarConfig { + return WalletTopBarConfig( + onScanCardClick = onScanCardClick, + onMoreClick = routerProvider()::openDetailsScreen, + ) + } + + private fun createWalletsListConfig(wallets: List): WalletsListConfig { + return WalletsListConfig( + selectedWalletIndex = 0, + wallets = wallets.map { wallet -> + WalletCardState.Loading( + id = if (wallet.scanResponse.cardTypesResolver.isMultiwalletAllowed()) { // TODO + UserWalletId("123") + } else { + UserWalletId("321") + }, + // TODO: wallet.walletId, + title = wallet.name, + additionalInfo = "", // TODO + imageResId = WalletImageResolver.resolve(cardTypesResolver = wallet.scanResponse.cardTypesResolver), + ) + }.toImmutableList(), + onWalletChange = onWalletChange, + ) + } + + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { + return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = onRefreshSwipe) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 438aaf5d32..5f825b106f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -8,16 +8,22 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.transactions.Transaction import com.tangem.core.ui.res.TangemTheme @@ -28,12 +34,11 @@ import com.tangem.feature.wallet.presentation.common.component.TokenItem import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockGroupTitle import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockTitle -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.WalletManageButtons -import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.WalletMarketplaceBlock import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator @@ -44,6 +49,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimat * [REDACTED_AUTHOR] */ +@OptIn(ExperimentalMaterialApi::class) @Suppress("LongMethod") @Composable internal fun WalletScreen(state: WalletStateHolder) { @@ -56,88 +62,108 @@ internal fun WalletScreen(state: WalletStateHolder) { val walletsListState = rememberLazyListState() val changeableItemModifier = Modifier.changeWalletAnimator(walletsListState) + val pullRefreshState = rememberPullRefreshState( + refreshing = state.pullToRefreshConfig.isRefreshing, + onRefresh = state.pullToRefreshConfig.onRefresh, + ) - LazyColumn( + Box( modifier = Modifier .padding(paddingValues = scaffoldPaddings) - .fillMaxSize(), - contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8), - horizontalAlignment = Alignment.CenterHorizontally, + .pullRefresh(pullRefreshState), ) { - item { - WalletsList( - config = state.walletsListConfig, - lazyListState = walletsListState, - ) - } - - if (state is WalletStateHolder.SingleCurrencyContent) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing8), + horizontalAlignment = Alignment.CenterHorizontally, + ) { item { - WalletManageButtons( - buttons = state.buttons, - modifier = changeableItemModifier.padding(top = TangemTheme.dimens.spacing14), + WalletsList( + config = state.walletsListConfig, + lazyListState = walletsListState, ) } - } - items( - items = state.notifications, - itemContent = { item -> - Notification( - state = item.state, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) - }, - ) - - if (state is WalletStateHolder.SingleCurrencyContent) { - item { - WalletMarketplaceBlock( - state = state.marketplaceBlockState, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) - } - } - - itemsIndexed( - items = state.contentItems, - key = { index, item -> - when (item) { - is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> item.networkName - is WalletContentItemState.MultiCurrencyItem.Token -> index - is WalletContentItemState.SingleCurrencyItem.Title -> index - is WalletContentItemState.SingleCurrencyItem.GroupTitle -> item.title - is WalletContentItemState.SingleCurrencyItem.Transaction -> index - is WalletContentItemState.Loading -> index + if (state is WalletStateHolder.SingleCurrencyContent) { + item { + HorizontalActionChips( + buttons = state.buttons, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14), + contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), + ) } - }, - itemContent = { index, item -> - ContentItem( - item = item, - modifier = changeableItemModifier.walletContentItemDecoration( - currentIndex = index, - lastIndex = state.contentItems.lastIndex, - ), - ) - }, - ) + } - if (state is WalletStateHolder.MultiCurrencyContent) { - item { - OrganizeTokensButton( - onClick = state.onOrganizeTokensClick, - modifier = changeableItemModifier - .padding(top = TangemTheme.dimens.spacing14) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) + items( + items = state.notifications, + itemContent = { item -> + Notification( + state = item.state, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + }, + ) + + if (state is WalletStateHolder.SingleCurrencyContent) { + item { + MarketPriceBlock( + state = state.marketPriceBlockState, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + } + } + + itemsIndexed( + items = state.contentItems, + key = { index, item -> + when (item) { + is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> item.networkName + is WalletContentItemState.MultiCurrencyItem.Token -> index + is WalletContentItemState.SingleCurrencyItem.Title -> index + is WalletContentItemState.SingleCurrencyItem.GroupTitle -> item.title + is WalletContentItemState.SingleCurrencyItem.Transaction -> index + is WalletContentItemState.Loading -> index + } + }, + itemContent = { index, item -> + ContentItem( + item = item, + modifier = changeableItemModifier.walletContentItemDecoration( + currentIndex = index, + lastIndex = state.contentItems.lastIndex, + ), + ) + }, + ) + + if (state is WalletStateHolder.MultiCurrencyContent) { + item { + OrganizeTokensButton( + onClick = state.onOrganizeTokensClick, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + } } } + + PullRefreshIndicator( + refreshing = state.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) } } + + state.bottomSheet?.let { bottomSheetConfig -> + if (bottomSheetConfig.isShow) WalletBottomSheet(config = bottomSheetConfig) + } } @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt new file mode 100644 index 0000000000..45b7a7a752 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt @@ -0,0 +1,161 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.PrimaryButtonIconStart +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConfig + +/** + * Wallet bottom sheet with detail notification information + * + * @param config component config + * +[REDACTED_AUTHOR] + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WalletBottomSheet(config: WalletBottomSheetConfig) { + ModalBottomSheet( + onDismissRequest = config.onDismissRequest, + sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true), + containerColor = TangemTheme.colors.background.primary, + dragHandle = { BottomSheetDefaults.DragHandle() }, + ) { + BottomSheetContent(config = config.content) + } +} + +@Composable +private fun BottomSheetContent(config: WalletBottomSheetConfig.BottomSheetContentConfig) { + Column( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(top = TangemTheme.dimens.spacing40, bottom = TangemTheme.dimens.spacing16), + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing40), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val iconTint = config.tint + if (iconTint != null) { + Icon( + painter = painterResource(id = config.iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size48), + tint = iconTint, + ) + } else { + Image( + painter = painterResource(id = config.iconResId), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size48), + ) + } + + Text( + text = config.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + Text( + text = config.subtitle.resolveReference(), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body2, + ) + + Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) { + val buttonModifier = Modifier.fillMaxWidth() + PrimaryButton(config = config.primaryButtonConfig, modifier = buttonModifier) + + if (config.secondaryButtonConfig != null) { + SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier) + } + } + } +} + +@Composable +private fun PrimaryButton( + config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig, + modifier: Modifier = Modifier, +) { + if (config.iconResId == null) { + PrimaryButton( + text = config.text, + onClick = config.onClick, + modifier = modifier, + ) + } else { + PrimaryButtonIconStart( + text = config.text, + iconResId = config.iconResId, + onClick = config.onClick, + modifier = modifier, + ) + } +} + +@Composable +private fun SecondaryButton( + config: WalletBottomSheetConfig.BottomSheetContentConfig.ButtonConfig, + modifier: Modifier = Modifier, +) { + if (config.iconResId == null) { + SecondaryButton( + text = config.text, + onClick = config.onClick, + modifier = modifier, + ) + } else { + SecondaryButtonIconStart( + text = config.text, + iconResId = config.iconResId, + onClick = config.onClick, + modifier = modifier, + ) + } +} + +@Preview +@Composable +private fun WalletBottomSheetContent_Light( + @PreviewParameter(WalletBottomSheetConfigProvider::class) + config: WalletBottomSheetConfig, +) { + TangemTheme(isDark = false) { + // Use preview of content because ModalBottomSheet isn't supported in Preview mode + BottomSheetContent(config = config.content) + } +} + +@Preview +@Composable +private fun WalletBottomSheetContent_Dark( + @PreviewParameter(WalletBottomSheetConfigProvider::class) + config: WalletBottomSheetConfig, +) { + TangemTheme(isDark = false) { + // Use preview of content because ModalBottomSheet isn't supported in Preview mode + BottomSheetContent(config = config.content) + } +} + +private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider( + collection = listOf(WalletPreviewData.bottomSheet), +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index 6c39b86f10..3f162e41f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -19,10 +19,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector -import kotlinx.coroutines.InternalCoroutinesApi /** * Wallets list component @@ -32,7 +30,7 @@ import kotlinx.coroutines.InternalCoroutinesApi * [REDACTED_AUTHOR] */ -@OptIn(ExperimentalFoundationApi::class, InternalCoroutinesApi::class) +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState, modifier: Modifier = Modifier) { val horizontalCardPadding = TangemTheme.dimens.spacing16 @@ -45,7 +43,7 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), flingBehavior = rememberSnapFlingBehavior(lazyListState = lazyListState), ) { - items(items = config.wallets, key = WalletCardState::id) { state -> + items(items = config.wallets, key = { it.id.stringValue }) { state -> WalletCard(state = state, modifier = Modifier.width(itemWidth)) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletManageButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletManageButtons.kt deleted file mode 100644 index 19b1a57045..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/WalletManageButtons.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.buttons.actions.ActionButton -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton -import kotlinx.collections.immutable.ImmutableList - -/** - * Wallet manage buttons - * - * @param buttons manage buttons - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun WalletManageButtons(buttons: ImmutableList, modifier: Modifier = Modifier) { - LazyRow( - modifier = modifier - .background(color = TangemTheme.colors.background.secondary) - .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), - verticalAlignment = Alignment.CenterVertically, - ) { - items( - items = buttons, - key = { it.config.text }, - itemContent = { ActionButton(config = it.config) }, - ) - } -} - -@Preview -@Composable -private fun Preview_WalletManageButtons_Light( - @PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList, -) { - TangemTheme(isDark = false) { - WalletManageButtons(buttons = buttons) - } -} - -@Preview -@Composable -private fun Preview_WalletManageButtons_Dark( - @PreviewParameter(WalletManageButtonProvider::class) buttons: ImmutableList, -) { - TangemTheme(isDark = true) { - WalletManageButtons(buttons = buttons) - } -} - -private class WalletManageButtonProvider : CollectionPreviewParameterProvider>( - collection = listOf(WalletPreviewData.manageButtons), -) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt index ea04e144c4..b06ad40f02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/decorations/WalletContentItemDecoration.kt @@ -12,21 +12,27 @@ import com.tangem.core.ui.res.TangemTheme */ internal fun Modifier.walletContentItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed { val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16) - when (currentIndex) { - 0 -> { + val isSingleItem = currentIndex == 0 && lastIndex == 0 + when { + isSingleItem -> { + modifierWithHorizontalPadding + .padding(top = TangemTheme.dimens.spacing14) + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + } + currentIndex == 0 -> { modifierWithHorizontalPadding .padding(top = TangemTheme.dimens.spacing14) .clip( - RoundedCornerShape( + shape = RoundedCornerShape( topStart = TangemTheme.dimens.radius16, topEnd = TangemTheme.dimens.radius16, ), ) } - lastIndex -> { + currentIndex == lastIndex -> { modifierWithHorizontalPadding .clip( - RoundedCornerShape( + shape = RoundedCornerShape( bottomStart = TangemTheme.dimens.radius16, bottomEnd = TangemTheme.dimens.radius16, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt new file mode 100644 index 0000000000..2fe69a783c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState +import com.tangem.utils.converter.Converter + +internal class FiatBalanceToWalletCardConverter( + private val currentState: WalletCardState, + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter { + + override fun convert(value: TokenList.FiatBalance): WalletCardState { + // TODO: [REDACTED_JIRA] + return when (value) { + is TokenList.FiatBalance.Loading -> with(currentState) { + WalletCardState.Loading(id, title, additionalInfo, imageResId, onClick) + } + is TokenList.FiatBalance.Failed -> with(currentState) { + WalletCardState.Error(id, title, additionalInfo, imageResId, onClick) + } + is TokenList.FiatBalance.Loaded -> with(currentState) { + if (isWalletContentHidden) { + WalletCardState.HiddenContent(id, title, additionalInfo, imageResId, onClick) + } else { + WalletCardState.Content( + id = id, + title = title, + additionalInfo = additionalInfo, + imageResId = imageResId, + onClick = onClick, + balance = formatFiatAmount(value.amount, fiatCurrencyCode, fiatCurrencySymbol), + ) + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt new file mode 100644 index 0000000000..c00f864c32 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/LoadingItemsProvider.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState.MultiCurrencyItem +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal object LoadingItemsProvider { + + fun getLoadingMultiCurrencyTokens(): PersistentList { + return List(size = 5) { TokenItemState.Loading } + .map { MultiCurrencyItem.Token(it) } + .toPersistentList() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt new file mode 100644 index 0000000000..d6f3112be9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.utils.converter.Converter + +internal class TokenListErrorToWalletStateConverter( + private val currentState: WalletStateHolder, +) : Converter { + + // TODO: [REDACTED_JIRA] + override fun convert(value: TokenListError): WalletStateHolder { + return currentState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt new file mode 100644 index 0000000000..e488aa7b7f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState.MultiCurrencyItem +import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf + +internal class TokenListToContentItemsConverter( + isWalletContentHidden: Boolean, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, +) : Converter> { + + private val tokenStatusConverter = TokenStatusToTokenItemConverter( + isWalletContentHidden, + fiatCurrencyCode, + fiatCurrencySymbol, + ) + + override fun convert(value: TokenList): ImmutableList { + return when (value) { + is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems() + is TokenList.Ungrouped -> value.mapToMultiCurrencyItems() + is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens() + } + } + + private fun TokenList.GroupedByNetwork.mapToMultiCurrencyItems(): PersistentList { + return groups.fold(initial = persistentListOf()) { acc, group -> + acc.mutate { it.addGroup(group) } + } + } + + private fun TokenList.Ungrouped.mapToMultiCurrencyItems(): PersistentList { + return tokens.fold(initial = persistentListOf()) { acc, token -> + acc.mutate { it.addToken(token) } + } + } + + private fun MutableList.addGroup(group: NetworkGroup): List { + this.add(MultiCurrencyItem.NetworkGroupTitle(group.network.name)) + + group.tokens.forEach { token -> + this.addToken(token) + } + + return this + } + + private fun MutableList.addToken(token: TokenStatus): List { + val tokenItemState = tokenStatusConverter.convert(token) + + this.add(MultiCurrencyItem.Token(tokenItemState)) + + return this + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt new file mode 100644 index 0000000000..1db861142b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -0,0 +1,59 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.MultiCurrencyContent +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder.SingleCurrencyContent +import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class TokenListToWalletStateConverter( + private val currentState: WalletStateHolder, + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter { + + override fun convert(value: TokenList): WalletStateHolder { + return when (currentState) { + is MultiCurrencyContent -> currentState.updateWithTokenList(value) + is SingleCurrencyContent -> currentState.updateWithTokenList(value) + is WalletStateHolder.Loading, + is WalletStateHolder.UnlockWalletContent, + -> currentState + } + } + + private fun MultiCurrencyContent.updateWithTokenList(tokenList: TokenList): MultiCurrencyContent { + val converter = TokenListToContentItemsConverter(isWalletContentHidden, fiatCurrencyCode, fiatCurrencySymbol) + + return this.copy( + walletsListConfig = updateSelectedWallet(tokenList.totalFiatBalance), + contentItems = converter.convert(tokenList), + ) + } + + private fun SingleCurrencyContent.updateWithTokenList(tokenList: TokenList): SingleCurrencyContent { + return this.copy( + walletsListConfig = updateSelectedWallet(tokenList.totalFiatBalance), + ) + } + + private fun WalletStateHolder.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { + val selectedWalletIndex = walletsListConfig.selectedWalletIndex + val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex] + val converter = FiatBalanceToWalletCardConverter( + selectedWalletCard, + isWalletContentHidden, + fiatCurrencyCode, + fiatCurrencySymbol, + ) + + return walletsListConfig.copy( + wallets = walletsListConfig.wallets + .toPersistentList() + .set(selectedWalletIndex, converter.convert(fiatBalance)), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt new file mode 100644 index 0000000000..b654b47713 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenStatusToTokenItemConverter.kt @@ -0,0 +1,105 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.marketprice.PriceChangeConfig +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.tokens.model.TokenStatus +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class TokenStatusToTokenItemConverter( + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter { + + private val TokenStatus.networkIconResId: Int? + @DrawableRes get() { + // TODO: [REDACTED_JIRA] + return if (isCoin) null else R.drawable.img_eth_22 + } + + private val TokenStatus.tokenIconResId: Int + @DrawableRes get() { + // TODO: [REDACTED_JIRA] + return R.drawable.img_eth_22 + } + + override fun convert(value: TokenStatus): TokenItemState { + return when (value.value) { + is TokenStatus.Loading -> TokenItemState.Loading + is TokenStatus.Loaded, + is TokenStatus.Custom, + -> value.mapToTokenItemState() + // TODO: Add other token item states, currently not designed + is TokenStatus.MissedDerivation, + is TokenStatus.NoAccount, + is TokenStatus.Unreachable, + -> value.mapToUnreachableTokenItemState() + } + } + + private fun TokenStatus.mapToTokenItemState(): TokenItemState.Content { + return TokenItemState.Content( + id = this.id.value, + name = this.name, + tokenIconUrl = this.iconUrl, + tokenIconResId = this.tokenIconResId, + networkIconResId = this.networkIconResId, + amount = getFormattedAmount(), + hasPending = value.hasTransactionsInProgress, + tokenOptions = if (isWalletContentHidden) { + TokenItemState.TokenOptionsState.Hidden(getPriceChangeConfig()) + } else { + TokenItemState.TokenOptionsState.Visible( + fiatAmount = getFormattedFiatAmount(), + priceChange = getPriceChangeConfig(), + ) + }, + ) + } + + private fun TokenStatus.getFormattedAmount(): String { + val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatCryptoAmount(amount, symbol, decimals) + } + + private fun TokenStatus.getFormattedFiatAmount(): String { + val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, fiatCurrencyCode, fiatCurrencySymbol) + } + + private fun TokenStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( + id = this.id.value, + name = this.name, + tokenIconUrl = this.iconUrl, + tokenIconResId = this.tokenIconResId, + networkIconResId = this.networkIconResId, + ) + + private fun TokenStatus.getPriceChangeConfig(): PriceChangeConfig { + val priceChange = value.priceChange + ?: return PriceChangeConfig(UNKNOWN_AMOUNT_SIGN, PriceChangeConfig.Type.DOWN) + + return PriceChangeConfig( + valueInPercent = BigDecimalFormatter.formatPercent(priceChange, useAbsoluteValue = true), + type = priceChange.getPriceChangeType(), + ) + } + + private fun BigDecimal?.getPriceChangeType(): PriceChangeConfig.Type { + return when { + this == null -> PriceChangeConfig.Type.DOWN + this < BigDecimal.ZERO -> PriceChangeConfig.Type.DOWN + else -> PriceChangeConfig.Type.UP + } + } + + private companion object { + const val UNKNOWN_AMOUNT_SIGN = "—" + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 4cdda4fe30..a369895e5b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -1,15 +1,35 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import androidx.lifecycle.* +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase +import com.tangem.domain.card.GetBiometricsStatusUseCase +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder -import com.tangem.feature.wallet.presentation.wallet.state.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.builder.WalletStateFactory +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates @@ -18,42 +38,147 @@ import kotlin.properties.Delegates * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") @HiltViewModel -internal class WalletViewModel @Inject constructor() : ViewModel() { +internal class WalletViewModel @Inject constructor( + private val saveWalletUseCase: SaveWalletUseCase, + private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase, + private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, + private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, + private val getTokenListUseCase: GetTokenListUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val scanCardProcessor: ScanCardProcessor, + private val dispatchers: CoroutineDispatcherProvider, +) : ViewModel(), DefaultLifecycleObserver { /** Feature router */ var router: InnerWalletRouter by Delegates.notNull() - /** Screen state */ - var uiState by mutableStateOf(getInitialState()) - private set - - // TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData - private fun getInitialState(): WalletStateHolder = WalletPreviewData.multicurrencyWalletScreenState.copy( - onBackClick = { router.popBackStack() }, - topBarConfig = WalletTopBarConfig( - onScanCardClick = { router.openOrganizeTokensScreen() }, - onMoreClick = { router.openDetailsScreen() }, - ), - walletsListConfig = WalletPreviewData.multicurrencyWalletScreenState.walletsListConfig.copy( - onWalletChange = ::selectWallet, - ), + private val stateFactory = WalletStateFactory( + routerProvider = Provider { router }, + onScanCardClick = ::onScanCardClick, + onWalletChange = ::changeWallet, + onRefreshSwipe = ::refreshContent, ) - // TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData - private fun selectWallet(index: Int) { - if (uiState.walletsListConfig.selectedWalletIndex == index) return + /** Screen state */ + var uiState by mutableStateOf(stateFactory.getInitialState()) + private set - Log.i("WalletViewModel", "selectWallet: $index") + override fun onCreate(owner: LifecycleOwner) { + getWalletsUseCase() + .distinctUntilChanged() + .flowWithLifecycle(owner.lifecycle) + .onEach { wallets -> + if (wallets.isEmpty()) return@onEach - uiState = if (index % 2 == 0) { - WalletPreviewData.multicurrencyWalletScreenState.copy( - walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), - ) - } else { - WalletPreviewData.singleWalletScreenState.copy( - walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), - ) + uiState = stateFactory.getContentState(wallets = wallets) + updateContentItems() + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun updateContentItems() { + getTokenListUseCase( + userWalletId = uiState.walletsListConfig.wallets.get( + index = uiState.walletsListConfig.selectedWalletIndex, + ).id, + ) + .distinctUntilChanged() + .mapLatest(::updateStateWithTokenListOrError) + .onEach { + uiState = it.copySealed( + pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = getRefreshingStatus()), + ) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + + private fun getRefreshingStatus(): Boolean { + return uiState.contentItems.any { state -> + val isMultiCurrencyItem = state as? WalletContentItemState.MultiCurrencyItem.Token + val isSingleCurrencyItem = state as? WalletContentItemState.SingleCurrencyItem.Transaction + + isMultiCurrencyItem?.state is TokenItemState.Loading || + isSingleCurrencyItem?.state is TransactionState.Loading || + state is WalletContentItemState.Loading } } + + private fun onScanCardClick() { + val prevRequestPolicyStatus = getBiometricsStatusUseCase() + + // Update access the code policy according access code saving status + setAccessCodeRequestPolicyUseCase(isBiometricsRequestPolicy = getAccessCodeSavingStatusUseCase()) + + viewModelScope.launch(dispatchers.io) { + scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) + .doOnSuccess { + // If card's public key is null then user wallet will be null + val userWallet = UserWalletBuilder(scanResponse = it).build() + + if (userWallet != null) { + saveWalletUseCase(userWallet) + .onLeft { + // Rollback policy if card saving was failed + setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) + } + } else { + // Rollback policy if card saving was failed + setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) + } + } + .doOnFailure { + // Rollback policy if card scanning was failed + setAccessCodeRequestPolicyUseCase(prevRequestPolicyStatus) + } + } + } + + private fun changeWallet(index: Int) { + if (uiState.walletsListConfig.selectedWalletIndex == index) return + + uiState = when (val state = uiState) { + is WalletStateHolder.MultiCurrencyContent -> state.copy( + walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), + ) + is WalletStateHolder.SingleCurrencyContent -> state.copy( + walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), + ) + is WalletStateHolder.UnlockWalletContent -> state.copy( + walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), + ) + is WalletStateHolder.Loading -> state + } + + updateContentItems() + } + + private fun refreshContent() { + uiState = uiState.copySealed(pullToRefreshConfig = uiState.pullToRefreshConfig.copy(isRefreshing = true)) + updateContentItems() + } + + private fun updateStateWithTokenListOrError(tokenList: Either): WalletStateHolder { + val updateStateWithError = { error: TokenListError -> + val converter = TokenListErrorToWalletStateConverter(uiState) + + converter.convert(error) + } + val updateState = { list: TokenList -> + val converter = TokenListToWalletStateConverter( + uiState, + isWalletContentHidden = false, // TODO: [REDACTED_JIRA] + fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] + fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] + ) + + converter.convert(list) + } + + return tokenList.fold(ifLeft = updateStateWithError, ifRight = updateState) + } } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 951a9904dc..edf13f07ee 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -71,16 +71,18 @@ zxingQrBarcodeScanner = "1.9.8" zxingQrCode = "3.5.1" mviCore = "1.3.1" kotlinSerialization = "1.4.1" -arrow = "1.2.0-RC" +arrow = "1.2.0" reactiveNetwork = "3.0.8" walletConnectCore = "1.18.0" walletConnectWeb3 = "1.11.0" +prettyLogger = "2.2.0" +okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_4.9-286" +tangemBlockchainSdk = "develop-288" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_4.9-277" +tangemCardSdk = "develop-278" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem @@ -201,7 +203,7 @@ material = { module = "com.google.android.material:material", version.ref = "goo moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } -okHttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" } otaliastudiosCameraView = { module = "com.otaliastudios:cameraview", version.ref = "otaliastudiosCameraView" } shopify-buy = { module = "com.shopify.mobilebuysdk:buy3", version.ref = "shopifyBuySdk" } spongecastle-core = { module = "com.madgag.spongycastle:core", version.ref = "spongycastleCryptoCore" } @@ -222,4 +224,5 @@ arrow-fx = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" } reactive-network = { module = "com.github.pwittchen:reactivenetwork-rx2", version.ref = "reactiveNetwork" } walletConnectCore = { module = "com.walletconnect:android-core", version.ref = "walletConnectCore" } walletConnectWeb3 = { module = "com.walletconnect:web3wallet", version.ref = "walletConnectWeb3" } +prettyLogger = { module = "com.orhanobut:logger", version.ref = "prettyLogger" } # endregion Other diff --git a/settings.gradle.kts b/settings.gradle.kts index 523476db95..276169d26e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ include(":common") // region Core modules include(":core:analytics") +include(":core:analytics:models") include(":core:datasource") include(":core:featuretoggles") include(":core:navigation") @@ -80,8 +81,12 @@ include(":domain:core") include(":domain:card") include(":domain:wallets") include(":domain:wallets:models") +include(":domain:tokens") // endregion Domain modules // region Data modules +include(":data:common") +include(":data:card") +include(":data:tokens") include(":data:source:preferences") // endregion Data modules \ No newline at end of file