diff --git a/app/build.gradle.kts b/app/build.gradle.kts index df6d24b384..de202bea18 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -25,11 +25,17 @@ dependencies { implementation(project(":domain:legacy")) implementation(project(":domain:models")) implementation(project(":domain:core")) - implementation(project(":domain:card")) + implementation(projects.domain.card) + implementation(projects.domain.demo) implementation(project(":domain:wallets")) implementation(project(":domain:wallets:models")) + implementation(projects.domain.settings) + implementation(projects.domain.tokens) + implementation(projects.domain.txhistory) + implementation(project(":common")) implementation(project(":core:analytics")) + implementation(projects.core.analytics.models) implementation(project(":core:navigation")) implementation(project(":core:featuretoggles")) implementation(project(":core:res")) @@ -38,7 +44,13 @@ dependencies { implementation(project(":core:utils")) implementation(project(":libs:crypto")) implementation(project(":libs:auth")) + implementation(project(":data:source:preferences")) + implementation(projects.data.card) + implementation(projects.data.common) + implementation(projects.data.settings) + implementation(projects.data.tokens) + implementation(projects.data.txhistory) /** Features */ implementation(project(":features:onboarding")) @@ -119,13 +131,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 +149,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/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index b791bd4cf6..8c1c53b739 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit b791bd4cf6c5eca9778f89e87cd62b72d24f5ce9 +Subproject commit 8c1c53b73950698d4acfa4d925b8c14bf6f0da63 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..b4783eda19 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -5,21 +5,26 @@ 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 +import com.tangem.blockchain.common.ExceptionHandler import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder 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 @@ -27,6 +32,7 @@ import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggle import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder +import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler @@ -59,7 +65,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 +156,12 @@ class TapApplication : Application(), ImageLoaderFactory { @Inject lateinit var tokenDetailsFeatureToggles: TokenDetailsFeatureToggles + @Inject + lateinit var scanCardProcessor: ScanCardProcessor + + @Inject + lateinit var blockchainExceptionHandler: BlockchainExceptionHandler + override fun onCreate() { super.onCreate() @@ -168,12 +179,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 +211,7 @@ class TapApplication : Application(), ImageLoaderFactory { if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( - HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }, + createNetworkLoggingInterceptor(), ) } @@ -266,6 +285,7 @@ class TapApplication : Application(), ImageLoaderFactory { jsonConverter = MoshiConverter.sdkMoshiConverter, ) factory.build(Analytics, buildData) + ExceptionHandler.append(blockchainExceptionHandler) } private fun initFeedbackManager( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt index 5d779e661f..6b8f903fb9 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/converters/ShopOrderToEventConverter.kt @@ -3,7 +3,7 @@ package com.tangem.tap.common.analytics.converters import com.shopify.buy3.Storefront import com.tangem.common.Converter import com.tangem.tap.common.analytics.events.Shop -import com.tangem.tap.common.shop.data.ProductType +import com.tangem.tap.features.shop.domain.models.ProductType /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index 47623e1f10..e5e4c697a6 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -157,5 +157,10 @@ sealed class AnalyticsParam { const val ERROR_KEY = "Error Key" const val CREATION_TYPE = "Creation type" const val DAPP_NAME = "DApp Name" + const val DAPP_URL = "DApp Url" + const val METHOD_NAME = "Method Name" + const val VALIDATION = "Validation" + const val BLOCKCHAIN_EXCEPTION_HOST = "exception_host" + const val BLOCKCHAIN_SELECTED_HOST = "selected_host" } } \ No newline at end of file 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/BlockchainExceptionEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt new file mode 100644 index 0000000000..385ab3e517 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.analytics.events + +import com.tangem.core.analytics.models.AnalyticsEvent + +class BlockchainExceptionEvent( + selectedHost: String, + exceptionHost: String, + error: String, +) : AnalyticsEvent( + category = "BlockchainSdk", + event = "Exception", + params = mapOf( + AnalyticsParam.BLOCKCHAIN_SELECTED_HOST to selectedHost, + AnalyticsParam.BLOCKCHAIN_EXCEPTION_HOST to exceptionHost, + AnalyticsParam.ERROR_DESCRIPTION to error, + ), +) \ No newline at end of file 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..13c04ec76f 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,7 @@ 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 /** [REDACTED_AUTHOR] @@ -12,16 +13,51 @@ sealed class WalletConnect( ) : AnalyticsEvent("Wallet Connect", event, params, error) { class ScreenOpened : WalletConnect(event = "WC Screen Opened") - class NewSessionEstablished(dAppName: String) : WalletConnect( + class NewSessionEstablished(dAppName: String, dAppUrl: String) : WalletConnect( event = "New Session Established", params = mapOf( AnalyticsParam.DAPP_NAME to dAppName, + AnalyticsParam.DAPP_URL to dAppUrl, ), ) - class SessionDisconnected : WalletConnect("Session Disconnected") - class RequestSigned : WalletConnect("Request Signed") + class SessionDisconnected(dAppName: String, dAppUrl: String) : WalletConnect( + event = "Session Disconnected", + params = mapOf( + AnalyticsParam.DAPP_NAME to dAppName, + AnalyticsParam.DAPP_URL to dAppUrl, + ), + ) - class SignError(error: Throwable) : WalletConnect("Sign", error = error) - class TransactionError(error: Throwable) : WalletConnect("Transaction", error = error) + class RequestHandled( + params: RequestHandledParams, + ) : WalletConnect( + event = "Request Handled", + params = params.toParamsMap(), + ) + + data class RequestHandledParams( + val dAppName: String, + val dAppUrl: String, + val methodName: String, + val blockchain: String, + val errorCode: String? = null, + ) { + fun toParamsMap(): Map { + val validation = if (errorCode == null) Validation.SUCCESS.param else Validation.FAIL.param + return mapOf( + AnalyticsParam.DAPP_NAME to dAppName, + AnalyticsParam.DAPP_URL to dAppUrl, + AnalyticsParam.METHOD_NAME to methodName, + AnalyticsParam.BLOCKCHAIN to blockchain, + AnalyticsParam.VALIDATION to validation, + if (errorCode != null) AnalyticsParam.ERROR_CODE to errorCode else null to null, + ).filterNotNull() + } + } + + enum class Validation(val param: String) { + SUCCESS("Success"), + FAIL("Fail"), + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt new file mode 100644 index 0000000000..b227a5f1f1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/BlockchainExceptionHandler.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.common.analytics.handlers + +import com.tangem.blockchain.common.ExceptionHandlerOutput +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.events.BlockchainExceptionEvent +import javax.inject.Inject + +class BlockchainExceptionHandler @Inject constructor( + private val analyticsHandler: AnalyticsEventHandler, +) : ExceptionHandlerOutput { + override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) { + analyticsHandler.send( + BlockchainExceptionEvent( + selectedHost = nextHost, + exceptionHost = currentHost, + error = message, + ), + ) + } +} \ No newline at end of file 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..b23f4a5b54 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,19 +9,19 @@ 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 import com.tangem.tap.common.analytics.events.AnalyticsParam -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 import com.tangem.tap.scope +import com.tangem.utils.extensions.copy import kotlinx.coroutines.launch import java.math.BigDecimal 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 deleted file mode 100644 index f945462b83..0000000000 --- a/app/src/main/java/com/tangem/tap/common/di/domain/wallets/WalletsDomainModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.common.di.domain.wallets - -import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -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) -object WalletsDomainModule { - - @Provides - @ViewModelScoped - fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase { - return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt index 68aca58758..4d081cc2e6 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -45,6 +45,7 @@ fun Blockchain.getGreyedOutIconRes(): Int { Blockchain.Cronos -> R.drawable.ic_cronos_no_color Blockchain.Telos, Blockchain.TelosTestnet -> R.drawable.ic_telos_no_color Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> R.drawable.ic_azero_no_color + Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> R.drawable.ic_octaspace_no_color else -> R.drawable.ic_tangem_logo } } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt b/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt deleted file mode 100644 index ee2867d25c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Collections.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.common.extensions - -fun MutableList.removeBy(predicate: (T) -> Boolean): Boolean { - val toRemove = this.filter(predicate) - this.removeAll(toRemove) - return toRemove.isNotEmpty() -} - -fun MutableList.replaceBy(item: T, predicate: (T) -> Boolean): Boolean { - val toRemove = this.filter(predicate) - if (toRemove.isEmpty()) return false - - val indexes = toRemove.map { indexOf(it) } - this.removeAll(toRemove) - indexes.forEach { this.add(it, item) } - return true -} - -fun MutableList.replaceByOrAdd(item: T, predicate: (T) -> Boolean) { - if (!replaceBy(item, predicate)) add(item) -} - -fun MutableList.copy(): MutableList { - return this.map { it }.toMutableList() -} \ 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/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 36dfe253b5..6665cab485 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.common.services.Result +import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.tap.common.TestActions import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.amountToCreateAccount import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.demo.isDemoCard 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/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 15b8d10c94..d26e36149c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -2,11 +2,11 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.DomainGlobalAction -import com.tangem.tap.common.extensions.replaceBy import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.tap.preferencesStorage import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.extensions.replaceBy import org.rekotlin.Action @Suppress("LongMethod", "ComplexMethod") diff --git a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt index 3032ef611a..97d9bd093b 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/TangemShopService.kt @@ -8,12 +8,13 @@ import com.tangem.core.analytics.Analytics import com.tangem.datasource.config.models.ShopifyShop import com.tangem.tap.common.analytics.converters.ShopOrderToEventConverter import com.tangem.tap.common.extensions.filterNotNull -import com.tangem.tap.common.shop.data.ProductType import com.tangem.tap.common.shop.data.TangemProduct import com.tangem.tap.common.shop.data.TotalSum import com.tangem.tap.common.shop.googlepay.GooglePayService import com.tangem.tap.common.shop.shopify.ShopifyService import com.tangem.tap.common.shop.shopify.data.CheckoutItem +import com.tangem.tap.features.shop.domain.models.ProductType +import com.tangem.tap.features.shop.domain.models.ProductType.Companion.SKUS_TO_DISPLAY import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -206,12 +207,6 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) { } } } - - companion object { - const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S" - const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S" - val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU) - } } private fun Storefront.MoneyV2.format(): String { diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt b/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt deleted file mode 100644 index 4238b4d883..0000000000 --- a/app/src/main/java/com/tangem/tap/common/shop/data/ProductType.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.tap.common.shop.data - -import com.tangem.tap.common.shop.TangemShopService - -enum class ProductType(val sku: String) { - WALLET_2_CARDS(TangemShopService.TANGEM_WALLET_2_CARDS_SKU), - WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU), - ; - - companion object { - fun fromSku(sku: String): ProductType? { - return when (sku) { - WALLET_2_CARDS.sku -> WALLET_2_CARDS - WALLET_3_CARDS.sku -> WALLET_3_CARDS - else -> null - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt index 79eb72e1f1..9a01a5df51 100644 --- a/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt +++ b/app/src/main/java/com/tangem/tap/common/shop/data/TangemProduct.kt @@ -1,5 +1,7 @@ package com.tangem.tap.common.shop.data +import com.tangem.tap.features.shop.domain.models.ProductType + data class TangemProduct( val type: ProductType, val totalSum: TotalSum? = null, diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt new file mode 100644 index 0000000000..d46dfadcad --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.data + +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.firstOrNull + +// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented +// [REDACTED_JIRA] +internal class RuntimeUserWalletsStore( + private val walletsStateHolder: WalletsStateHolder, +) : UserWalletsStore { + + override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? { + return walletsStateHolder.userWalletsListManager + ?.userWallets + ?.firstOrNull() + ?.singleOrNull { it.walletId == key } + } +} \ 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/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt new file mode 100644 index 0000000000..6aeaad1dcf --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.di.data + +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.tap.data.RuntimeUserWalletsStore +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 UserWalletsStoreModule { + + @Provides + @Singleton + fun provideUserWalletsStore(walletsStateHolder: WalletsStateHolder): UserWalletsStore { + return RuntimeUserWalletsStore(walletsStateHolder) + } +} \ 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..a7f064da55 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -0,0 +1,51 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.card.* +import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.demo.IsDemoCardUseCase +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 CardDomainModule { + + @Provides + @ViewModelScoped + fun provideGetBiometricsStatusUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): GetBiometricsStatusUseCase { + return GetBiometricsStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } + + @Provides + @ViewModelScoped + fun provideSetAccessCodeRequestPolicyUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): SetAccessCodeRequestPolicyUseCase { + return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } + + @Provides + @ViewModelScoped + fun provideGetAccessCodeSavingStatusUseCase( + cardSdkConfigRepository: CardSdkConfigRepository, + ): GetAccessCodeSavingStatusUseCase { + return GetAccessCodeSavingStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository) + } + + @Provides + @ViewModelScoped + fun provideGetCardWasScannedUseCase(cardRepository: CardRepository): GetCardWasScannedUseCase { + return GetCardWasScannedUseCase(cardRepository = cardRepository) + } + + @Provides + @ViewModelScoped + fun provideIsDemoCardUseCase(): IsDemoCardUseCase = IsDemoCardUseCase(config = DemoConfig()) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt new file mode 100644 index 0000000000..3cfbe41b23 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.card.* +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 CardLegacyDomainModule { + + @Provides + @Singleton + fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt new file mode 100644 index 0000000000..4897db1d24 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase +import com.tangem.domain.settings.repositories.SettingsRepository +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 SettingsDomainModule { + + @Provides + @ViewModelScoped + fun providesGetWalletsUseCase(settingsRepository: SettingsRepository): IsUserAlreadyRateAppUseCase { + return IsUserAlreadyRateAppUseCase(settingsRepository = settingsRepository) + } +} \ 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..8fc08144df --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -0,0 +1,63 @@ +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 provideGetPrimaryCurrencyUseCase( + tokensRepository: TokensRepository, + quotesRepository: QuotesRepository, + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): GetPrimaryCurrencyUseCase { + return GetPrimaryCurrencyUseCase(tokensRepository, quotesRepository, networksRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideToggleTokenListGroupingUseCase( + networksRepository: NetworksRepository, + dispatchers: CoroutineDispatcherProvider, + ): ToggleTokenListGroupingUseCase { + return ToggleTokenListGroupingUseCase(networksRepository, dispatchers) + } + + @Provides + @ViewModelScoped + fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { + return ToggleTokenListSortingUseCase(dispatchers) + } + + @Provides + @ViewModelScoped + fun provideApplyTokenListSortingUseCase( + tokensRepository: TokensRepository, + dispatchers: CoroutineDispatcherProvider, + ): ApplyTokenListSortingUseCase { + return ApplyTokenListSortingUseCase(tokensRepository, dispatchers) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt new file mode 100644 index 0000000000..3ab36cd231 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/TxHistoryDomainModule.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.tokens.* +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +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 TxHistoryDomainModule { + + @Provides + @ViewModelScoped + fun provideGetTxHistoryItemsCountUseCase(txHistoryRepository: TxHistoryRepository): GetTxHistoryItemsCountUseCase { + return GetTxHistoryItemsCountUseCase(repository = txHistoryRepository) + } + + @Provides + @ViewModelScoped + fun provideGetTxHistoryItemsUseCase(txHistoryRepository: TxHistoryRepository): GetTxHistoryItemsUseCase { + return GetTxHistoryItemsUseCase(repository = txHistoryRepository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt new file mode 100644 index 0000000000..1c4bc06044 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt @@ -0,0 +1,27 @@ +package com.tangem.tap.di.domain + +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.walletmanager.DefaultWalletManagersFacade +import com.tangem.domain.walletmanager.WalletManagersFacade +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 WalletManagersFacadeModule { + + @Provides + @Singleton + fun provideWalletManagersFacade( + walletManagersStore: WalletManagersStore, + userWalletsStore: UserWalletsStore, + configManager: ConfigManager, + ): WalletManagersFacade { + return DefaultWalletManagersFacade(walletManagersStore, userWalletsStore, configManager) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt new file mode 100644 index 0000000000..9321638b94 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.legacy.WalletsStateHolder +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +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 WalletsDomainModule { + + @Provides + @ViewModelScoped + fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase { + return GetWalletsUseCase(walletsStateHolder = walletsStateHolder) + } + + @Provides + @ViewModelScoped + fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase { + return SaveWalletUseCase(walletsStateHolder = walletsStateHolder) + } + + @Provides + @ViewModelScoped + fun providesGetExploreUrlUseCase(walletsManagersFacade: WalletManagersFacade): GetExploreUrlUseCase { + return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade) + } +} \ 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..5bac8dc2b2 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -1,6 +1,6 @@ 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 @@ -14,14 +14,13 @@ 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 +37,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 +65,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 +82,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 +94,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 +113,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 +129,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 +156,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 +164,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 +172,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 +180,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 +191,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 +227,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/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index 9239ac0d3f..01bb883f69 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.configurable.warningMessage import com.tangem.blockchain.common.Blockchain -import com.tangem.tap.common.extensions.removeBy +import com.tangem.utils.extensions.removeByReplace import com.tangem.wallet.R import java.util.concurrent.CopyOnWriteArrayList @@ -44,12 +44,12 @@ class WarningMessagesManager { } fun removeWarnings(origin: WarningMessage.Origin) { - warningsList.removeBy { it.origin == origin } + warningsList.removeByReplace { it.origin == origin } sortByPriority() } fun removeWarnings(messageRes: Int) { - warningsList.removeBy { it.messageResId == messageRes } + warningsList.removeByReplace { it.messageResId == messageRes } } fun containsWarning(warning: WarningMessage) = warning in warningsList diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt deleted file mode 100644 index 9adb6b24aa..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/extensions/Blockchain.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.tap.domain.extensions - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.common.card.EllipticCurve -import java.math.BigDecimal - -@Suppress("MagicNumber") -fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { - return when (this) { - Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(1.5) else BigDecimal.ONE - Blockchain.XRP -> BigDecimal(10) - else -> null - } -} - -fun Blockchain.minimalAmount(): BigDecimal { - return 1.toBigDecimal().movePointLeft(decimals()) -} - -fun Blockchain.getPrimaryCurve(): EllipticCurve? { - return when { - getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { - EllipticCurve.Secp256k1 - } - getSupportedCurves().contains(EllipticCurve.Ed25519) -> { - EllipticCurve.Ed25519 - } - else -> { - null - } - } -} - -private const val NODL = "NODL" \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt index 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/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/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index c3f23eb9b4..5bef94e017 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -20,6 +20,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.TwinsHelper +import com.tangem.domain.common.extensions.getPrimaryCurve import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse @@ -30,7 +31,6 @@ import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask import com.tangem.operations.files.ReadFilesTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand import com.tangem.tap.domain.TapSdkError -import com.tangem.tap.domain.extensions.getPrimaryCurve import com.tangem.tap.domain.tokens.UserTokensRepository import com.tangem.tap.preferencesStorage import com.tangem.tap.scope 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..c3ed94f70c 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 @@ -79,8 +79,8 @@ class UserTokensRepository( private fun List.toUserTokensResponse(): UserTokensResponse { return UserTokensResponse( tokens = CurrencyConverter.convertList(input = this), - group = GROUP_DEFAULT_VALUE, - sort = SORT_DEFAULT_VALUE, + group = UserTokensResponse.GroupType.NONE, + sort = UserTokensResponse.SortType.MANUAL, ) } @@ -121,8 +121,6 @@ class UserTokensRepository( private fun getUserWalletId(card: CardDTO): String? = UserWalletIdBuilder.card(card).build()?.stringValue companion object { - private const val GROUP_DEFAULT_VALUE = "none" - private const val SORT_DEFAULT_VALUE = "manual" private const val NOT_FOUND_HTTP_CODE = "404" // TODO("After adding DI") get dependencies by DI diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt index b67bb497de..ff90f83942 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt @@ -7,6 +7,7 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.files.FileReader import com.tangem.tap.features.wallet.models.Currency +@Deprecated("Use [com.tangem.datasource.local.token.UserTokensStore] instead.") class UserTokensStorageService(private val fileReader: FileReader) { private val userTokensAdapter: JsonAdapter = MoshiConverter.networkMoshi.adapter(UserTokensResponse::class.java) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt index cadd73b606..4ab48a80a9 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt @@ -9,10 +9,10 @@ import com.tangem.common.flatMap import com.tangem.common.services.secure.SecureStorage import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.common.extensions.replaceByOrAdd import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.utils.publicInformation +import com.tangem.utils.extensions.plusOrReplace import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -29,12 +29,9 @@ internal class DefaultUserWalletsPublicInformationRepository( getAll() .flatMap { savedInformation -> val infoToSave = withContext(Dispatchers.Default) { - savedInformation.toMutableList() - .apply { - replaceByOrAdd(userWallet.publicInformation) { - userWallet.walletId == it.walletId - } - } + savedInformation.plusOrReplace(userWallet.publicInformation) { + userWallet.walletId == it.walletId + } } save(infoToSave) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index b89be4b97f..6fa441f106 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -16,7 +16,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.TestActions import com.tangem.tap.common.entities.FiatCurrency -import com.tangem.tap.common.extensions.replaceByOrAdd import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.walletStores.WalletStoresError import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository @@ -32,6 +31,7 @@ import com.tangem.tap.features.wallet.models.getPendingTransactions import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.plusOrReplace import kotlinx.coroutines.* import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -428,11 +428,10 @@ internal class DefaultWalletAmountsRepository( private suspend fun updateWalletManagerInStorage(userWalletId: UserWalletId, walletManager: WalletManager) = withContext(Dispatchers.Default) { WalletManagerStorage.update { prevManagers -> - val newManagersForUserWallet = prevManagers[userWalletId].orEmpty().toMutableList().apply { - replaceByOrAdd(walletManager) { + val newManagersForUserWallet = prevManagers[userWalletId].orEmpty() + .plusOrReplace(walletManager) { it.wallet.blockchain == walletManager.wallet.blockchain } - } prevManagers.apply { set(userWalletId, newManagersForUserWallet) diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt index 7a51a18e10..2fbd1e0624 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt @@ -9,13 +9,13 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation +import com.tangem.domain.common.extensions.makeWalletManagerForApp 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.wallets.legacy.WalletManagersRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.walletStores.WalletStoresError import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt index 85fd9d5c8e..c7eef5a974 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/utils/WalletDataOperations.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.address.AddressType import com.tangem.common.core.TangemError -import com.tangem.tap.domain.extensions.amountToCreateAccount +import com.tangem.domain.common.extensions.amountToCreateAccount import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.features.demo.DemoHelper 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..a4bda602c0 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 @@ -3,10 +3,9 @@ package com.tangem.tap.domain.walletconnect 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 import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TapError @@ -36,7 +35,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 +54,9 @@ class WalletConnectManager { .addInterceptor(RetryInterceptor()) .build() } + private val interceptor by lazy { - HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } + createNetworkLoggingInterceptor() } private var sessions: MutableMap = mutableMapOf() @@ -226,7 +225,6 @@ class WalletConnectManager { } private fun onSessionClosed(session: WCSession) { - Analytics.send(WalletConnect.SessionDisconnected()) sessions.remove(session.topic) walletConnectRepository.removeSession(session) store.dispatchOnMain(WalletConnectAction.RemoveSession(session)) @@ -394,7 +392,6 @@ class WalletConnectManager { ), ) } - Analytics.send(WalletConnect.NewSessionEstablished("")) } } } 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..f84ad537dd 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,12 +6,12 @@ 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 import com.tangem.blockchain.extensions.isAscii import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString @@ -22,7 +22,6 @@ import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic.TransactionSent.MemoType -import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder @@ -35,8 +34,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 +75,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 +161,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 -> { @@ -170,7 +173,6 @@ class WalletConnectSdkHelper { HEX_PREFIX + data.walletManager.wallet.recentTransactions.last().hash } is SimpleResult.Failure -> { - (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.TransactionError(it)) } Timber.e(result.error as BlockchainSdkError) null } @@ -182,7 +184,6 @@ class WalletConnectSdkHelper { transactionData = data.transaction, nonce = null, blockchain = data.walletManager.wallet.blockchain, - gasLimit = null, ) ?: return null val command = SignHashCommand( @@ -201,7 +202,6 @@ class WalletConnectSdkHelper { ).toHexString() } is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) } Timber.e(result.error.customMessage) null } @@ -240,7 +240,6 @@ class WalletConnectSdkHelper { ) } is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.TransactionError(it)) } Timber.e(result.error.customMessage) null } @@ -334,7 +333,6 @@ class WalletConnectSdkHelper { ) } is CompletionResult.Failure -> { - (result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) } Timber.e(result.error.customMessage) null } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt index 9890f9863f..3304faa535 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/WalletConnectRepositoryImpl.kt @@ -2,9 +2,10 @@ package com.tangem.tap.domain.walletconnect2.data import android.app.Application import arrow.core.flatten -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WcJrpcMethods import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer import com.tangem.tap.domain.walletconnect2.domain.WcRequest import com.tangem.tap.domain.walletconnect2.domain.models.* @@ -25,6 +26,7 @@ import javax.inject.Inject class WalletConnectRepositoryImpl @Inject constructor( private val application: Application, private val wcRequestDeserializer: WcJrpcRequestsDeserializer, + private val analyticsHandler: AnalyticsEventHandler, ) : WalletConnectRepository { private var sessionProposal: Wallet.Model.SessionProposal? = null @@ -37,6 +39,8 @@ class WalletConnectRepositoryImpl @Inject constructor( private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() override val activeSessions: Flow> = _activeSessions + private var currentSessions: List = emptyList() + /** * @param projectId Project ID at https://cloud.walletconnect.com/ */ @@ -127,8 +131,12 @@ class WalletConnectRepositoryImpl @Inject constructor( // we can send approval automatically, because in WC 2.0 the list of chains is approved when // initial connection is established sendRequest( - topic = sessionRequest.topic, - id = sessionRequest.request.id, + RequestData( + topic = sessionRequest.topic, + requestId = sessionRequest.request.id, + blockchain = sessionRequest.chainId.toString(), + method = WcJrpcMethods.WALLET_ADD_ETHEREUM_CHAIN.code, + ), result = "", ) } @@ -142,6 +150,7 @@ class WalletConnectRepositoryImpl @Inject constructor( id = sessionRequest.request.id, metaUrl = sessionRequest.peerMetaData?.url ?: "", metaName = sessionRequest.peerMetaData?.name ?: "", + method = sessionRequest.request.method, ), ) } @@ -263,7 +272,12 @@ class WalletConnectRepositoryImpl @Inject constructor( params = sessionApproval, onSuccess = { Timber.d("Approved successfully: $it") - Analytics.send(WalletConnect.NewSessionEstablished(sessionProposal.name)) + analyticsHandler.send( + WalletConnect.NewSessionEstablished( + dAppName = sessionProposal.name, + dAppUrl = sessionProposal.url, + ), + ) }, onError = { Timber.d("Error while approving: $it") @@ -278,23 +292,56 @@ class WalletConnectRepositoryImpl @Inject constructor( ) } - override fun sendRequest(topic: String, id: Long, result: String) { + override fun sendRequest(requestData: RequestData, result: String) { + val session = currentSessions.find { it.topic == requestData.topic } + analyticsHandler.send( + WalletConnect.RequestHandled( + WalletConnect.RequestHandledParams( + dAppName = session?.name ?: "", + dAppUrl = session?.url ?: "", + methodName = requestData.method, + blockchain = requestData.blockchain, + ), + ), + ) Web3Wallet.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( - sessionTopic = topic, + sessionTopic = requestData.topic, jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult( - id = id, + id = requestData.requestId, result = result, ), ), onSuccess = {}, onError = { - Analytics.send(WalletConnect.TransactionError(it.throwable)) + WalletConnect.RequestHandledParams( + dAppName = session?.name ?: "", + dAppUrl = session?.url ?: "", + methodName = requestData.method, + blockchain = requestData.blockchain, + errorCode = WalletConnectError.ValidationError.toString(), + ) }, ) } - override fun rejectRequest(topic: String, id: Long) { + override fun rejectRequest(requestData: RequestData, error: WalletConnectError) { + val session = currentSessions.find { it.topic == requestData.topic } + analyticsHandler.send( + WalletConnect.RequestHandled( + WalletConnect.RequestHandledParams( + dAppName = session?.name ?: "", + dAppUrl = session?.url ?: "", + methodName = requestData.method, + blockchain = requestData.blockchain, + errorCode = error.toString(), + ), + ), + ) + cancelRequest(requestData.topic, requestData.requestId) + } + + override fun cancelRequest(topic: String, id: Long) { Web3Wallet.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( sessionTopic = topic, @@ -325,10 +372,16 @@ class WalletConnectRepositoryImpl @Inject constructor( } override fun disconnect(topic: String) { + val session = currentSessions.find { it.topic == topic } Web3Wallet.disconnectSession( params = Wallet.Params.SessionDisconnect(topic), onSuccess = { - Analytics.send(WalletConnect.SessionDisconnected()) + analyticsHandler.send( + WalletConnect.SessionDisconnected( + dAppName = session?.name ?: "", + dAppUrl = session?.url ?: "", + ), + ) updateSessions() Timber.d("Disconnected successfully: $it") }, @@ -364,6 +417,7 @@ class WalletConnectRepositoryImpl @Inject constructor( ) } Timber.d("Available sessions: $availableSessions") + currentSessions = availableSessions _activeSessions.emit(availableSessions) } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index e37d7c2949..9f5ce79361 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -1,7 +1,5 @@ package com.tangem.tap.domain.walletconnect2.domain -import com.tangem.core.analytics.Analytics -import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.models.* @@ -148,8 +146,8 @@ class WalletConnectInteractor( walletConnectRepository.disconnect(topic) } - fun rejectRequest(topic: String, id: Long) { - walletConnectRepository.rejectRequest(topic, id) + fun cancelRequest(topic: String, id: Long) { + walletConnectRepository.cancelRequest(topic, id) } private suspend fun handleRequest(sessionRequest: WalletConnectEvents.SessionRequest) { @@ -164,16 +162,23 @@ class WalletConnectInteractor( null } } + val networkId = sessionRequest.chainId?.let { blockchainHelper.chainIdToNetworkIdOrNull(it) } ?: "" + val requestData = RequestData( + topic = sessionRequest.topic, + requestId = sessionRequest.id, + blockchain = networkId, + method = sessionRequest.method, + ) + if (error != null) { - walletConnectRepository.rejectRequest(sessionRequest.topic, sessionRequest.id) + walletConnectRepository.rejectRequest(requestData, error) return } when (sessionRequest.request) { is WcRequest.BnbCancel -> Unit is WcRequest.BnbTxConfirm -> walletConnectRepository.sendRequest( - topic = sessionRequest.topic, - id = sessionRequest.id, + requestData = requestData, result = "", ) else -> { @@ -212,16 +217,18 @@ class WalletConnectInteractor( Timber.d("Signed hash: $signedHash") + val requestData = RequestData( + topic = request.topic, + requestId = request.requestId, + blockchain = networkId, + method = currentRequest.method, + ) + if (signedHash == null) { - walletConnectRepository.rejectRequest( - topic = request.topic, - id = request.requestId, - ) + walletConnectRepository.rejectRequest(requestData, WalletConnectError.SigningError) } else { - Analytics.send(WalletConnect.RequestSigned()) walletConnectRepository.sendRequest( - topic = request.topic, - id = request.requestId, + requestData = requestData, result = signedHash, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt index 4069a56fa8..93e772b0d2 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectRepository.kt @@ -1,9 +1,6 @@ package com.tangem.tap.domain.walletconnect2.domain -import com.tangem.tap.domain.walletconnect2.domain.models.Account -import com.tangem.tap.domain.walletconnect2.domain.models.NetworkNamespace -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents -import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectSession +import com.tangem.tap.domain.walletconnect2.domain.models.* import kotlinx.coroutines.flow.Flow interface WalletConnectRepository { @@ -24,7 +21,9 @@ interface WalletConnectRepository { fun reject() - fun sendRequest(topic: String, id: Long, result: String) + fun sendRequest(requestData: RequestData, result: String) - fun rejectRequest(topic: String, id: Long) + fun rejectRequest(requestData: RequestData, error: WalletConnectError) + + fun cancelRequest(topic: String, id: Long) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt new file mode 100644 index 0000000000..38cd19ca97 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/RequestData.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.domain.walletconnect2.domain.models + +data class RequestData( + val topic: String, + val requestId: Long, + val method: String, + val blockchain: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt index 35da4b88f8..ca01b701a7 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectError.kt @@ -7,4 +7,6 @@ sealed class WalletConnectError : Exception() { data class ExternalApprovalError(override val message: String?) : WalletConnectError() object WrongUserWallet : WalletConnectError() object UnsupportedMethod : WalletConnectError() + object SigningError : WalletConnectError() + object ValidationError : WalletConnectError() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt index e481daa22d..d47c296a42 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt @@ -24,5 +24,6 @@ sealed interface WalletConnectEvents { val id: Long, val metaName: String, val metaUrl: String, + val method: String, ) : WalletConnectEvents } \ No newline at end of file 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..fcf584cf31 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 @@ -1,16 +1,7 @@ package com.tangem.tap.features.demo -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.TransactionData -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.extensions.Result -import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.common.CompletionResult +import com.tangem.domain.demo.DemoConfig import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.common.demo.DemoConfig import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.DetailsAction @@ -21,13 +12,6 @@ import com.tangem.tap.store import com.tangem.wallet.R import org.rekotlin.Action -/** -[REDACTED_AUTHOR] - */ -interface DemoMiddleware { - fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean -} - object DemoHelper { val config = DemoConfig() @@ -66,59 +50,10 @@ object DemoHelper { return false } - fun injectDemoBalance(walletManager: WalletManager?) { - val manager = walletManager ?: return - - val blockchain = walletManager.wallet.blockchain - val amount = config.getBalance(blockchain) - manager.wallet.setAmount(amount) - } - private fun getScanResponse(appState: () -> AppState?): ScanResponse? { val state = appState() ?: return null return state.globalState.onboardingState.onboardingManager?.scanResponse ?: state.globalState.scanResponse } -} - -class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender { - - 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), - ), - ) - } - - @Suppress("MagicNumber") - override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { - val dataToSign = randomString(32).toByteArray() - val signerResponse = signer.sign( - hash = dataToSign, - publicKey = walletManager.wallet.publicKey, - ) - return when (signerResponse) { - is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError()) - is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error) - } - } - - private fun randomInt(from: Int, to: Int): Int = kotlin.random.Random.nextInt(from, to) - - private fun randomString(length: Int): String { - val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') - return (1..length) - .map { randomInt(0, charPool.size) } - .map(charPool::get) - .joinToString("") - } - - companion object { - val ID = DemoTransactionSender::class.java.simpleName - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddleware.kt new file mode 100644 index 0000000000..4f46273749 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoMiddleware.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.features.demo + +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse +import org.rekotlin.Action + +/** +[REDACTED_AUTHOR] + */ +interface DemoMiddleware { + fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt index 4624db8d34..ae1cf2fa2a 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt @@ -1,10 +1,10 @@ package com.tangem.tap.features.demo import com.tangem.common.extensions.guard -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.common.demo.DemoConfig +import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext -import com.tangem.tap.domain.extensions.makePrimaryWalletManager +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.ProgressState diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt new file mode 100644 index 0000000000..7e24b61c6d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt @@ -0,0 +1,51 @@ +package com.tangem.tap.features.demo + +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.common.CompletionResult +import kotlin.random.Random + +class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender { + + override suspend fun getFee(amount: Amount, destination: String): Result { + val blockchain = walletManager.wallet.blockchain + return Result.Success( + TransactionFee.Choosable( + minimum = Fee.Common(Amount(minimumFee, blockchain)), + normal = Fee.Common(Amount(normalFee, blockchain)), + priority = Fee.Common(Amount(priorityFee, blockchain)), + ), + ) + } + + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { + val signerResponse = signer.sign( + hash = getDataToSign(), + publicKey = walletManager.wallet.publicKey, + ) + return when (signerResponse) { + is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError()) + is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error) + } + } + + private fun getDataToSign(): ByteArray { + val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') + return IntRange(start = 1, endInclusive = 32) + .map { Random.nextInt(from = 0, until = charPool.size) } + .map(charPool::get) + .joinToString(separator = "") + .toByteArray() + } + + companion object { + val ID: String = 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/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index b3b5057a76..c2276c9065 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -182,7 +182,7 @@ class WalletConnectMiddleware { } is WalletConnectAction.RejectRequest -> { walletConnectManager.rejectRequest(action.topic, action.id) - walletConnectInteractor.rejectRequest(action.topic, action.id) + walletConnectInteractor.cancelRequest(action.topic, action.id) } is WalletConnectAction.SendTransaction -> { walletConnectManager.completeTransaction(action.topic) 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 392f4bf8ce..863083a6e0 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,10 @@ 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.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.analytics.events.Shop @@ -21,10 +20,14 @@ 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.redux.HomeMiddleware.NEW_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 @@ -76,11 +79,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/intentHandler/handlers/BackgroundScanIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt index 18a51c222f..df5b3759d8 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/BackgroundScanIntentHandler.kt @@ -4,6 +4,7 @@ import android.content.Intent import android.nfc.NfcAdapter import android.nfc.Tag import android.os.Build +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.features.welcome.redux.WelcomeAction @@ -41,10 +42,10 @@ class BackgroundScanIntentHandler( // TODO: Remove delay after [REDACTED_JIRA] scope.launch { delay(timeMillis = 200) - store.dispatch(WelcomeAction.ProceedWithCard) + store.dispatchWithMain(WelcomeAction.ProceedWithCard) } } else { - store.dispatch(HomeAction.ReadCard()) + store.dispatchWithMain(HomeAction.ReadCard()) } return true diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt index 3fc6c318d6..9d28a45db6 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/SellCurrencyIntentHandler.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.features.intentHandler.IntentHandler import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store @@ -20,7 +21,7 @@ class SellCurrencyIntentHandler : IntentHandler { val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false Timber.d("MoonPay Sell: $amount $currency to $destinationAddress") - store.dispatch( + store.dispatchWithMain( WalletAction.TradeCryptoAction.SendCrypto( currencyId = currency, amount = amount, diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt index e61d0b74d7..79e4774682 100644 --- a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/WalletConnectLinkIntentHandler.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.intentHandler.handlers import android.content.Intent +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.removePrefixOrNull import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction @@ -25,7 +26,7 @@ class WalletConnectLinkIntentHandler : IntentHandler { return if (wcUri == null) { false } else { - store.dispatch(WalletConnectAction.HandleDeepLink(wcUri)) + store.dispatchWithMain(WalletConnectAction.HandleDeepLink(wcUri)) true } } 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/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 0006840098..711c5acc69 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -4,6 +4,7 @@ import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.common.analytics.events.AnalyticsParam @@ -14,7 +15,6 @@ 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.TapError -import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.features.onboarding.OnboardingDialog 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..023e3137c4 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 @@ -6,9 +6,11 @@ import com.tangem.common.extensions.guard import com.tangem.core.analytics.Analytics import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.common.extensions.makePrimaryWalletManager 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 @@ -18,8 +20,6 @@ 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.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..628e934435 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,14 +7,16 @@ 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.minimalAmount import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.scan.CardDTO import com.tangem.tap.* @@ -30,7 +32,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.domain.extensions.minimalAmount import com.tangem.tap.features.demo.DemoTransactionSender import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.send.redux.* @@ -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/shop/data/DefaultShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt index 74505b69d3..1abbd3d17c 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/data/DefaultShopRepository.kt @@ -2,10 +2,13 @@ package com.tangem.tap.features.shop.data import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.ShopResponse +import com.tangem.domain.common.extensions.withIOContext import com.tangem.tap.features.shop.domain.ShopRepository +import com.tangem.tap.features.shop.domain.models.SalesProduct import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import timber.log.Timber +import java.util.Locale /** * Default implementation of shop feature repository @@ -20,6 +23,8 @@ internal class DefaultShopRepository( private val dispatchers: CoroutineDispatcherProvider, ) : ShopRepository { + private val salesProductConverter = SalesProductConverter() + override suspend fun isShopifyOrderingAvailable(): Boolean { return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) } .fold( @@ -31,7 +36,24 @@ internal class DefaultShopRepository( ) } + override suspend fun getSalesProductInfo(): List { + return withIOContext { + val salesInfo = tangemTechApi.getSalesInfo(locale = getLocaleName(), shops = SHOPIFY_NAME) + salesProductConverter.convert(salesInfo) + } + } + + private fun getLocaleName(): String { + return if (Locale.getDefault().language == "ru") { + RU_LOCALE + } else { + EN_LOCALE + } + } + private companion object { - const val SHOPIFY_NAME = "shopify" + private const val SHOPIFY_NAME = "shopify" + private const val RU_LOCALE = "ru" + private const val EN_LOCALE = "en" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt b/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt new file mode 100644 index 0000000000..9d8f440da6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/data/SalesProductConverter.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.features.shop.data + +import com.tangem.datasource.api.tangemTech.models.SalesResponse +import com.tangem.tap.features.shop.domain.models.Notification +import com.tangem.tap.features.shop.domain.models.ProductState +import com.tangem.tap.features.shop.domain.models.ProductType +import com.tangem.tap.features.shop.domain.models.SalesProduct +import com.tangem.utils.converter.Converter + +internal class SalesProductConverter : Converter> { + + override fun convert(value: SalesResponse): List { + return value.sales.map { sales -> + val productState = when (sales.state) { + "order" -> ProductState.ORDER + "pre-order" -> ProductState.PRE_ORDER + "sold-out" -> ProductState.SOLD_OUT + else -> ProductState.SOLD_OUT + } + val productType = when (sales.product.code) { + "pack2" -> ProductType.WALLET_2_CARDS + "pack3" -> ProductType.WALLET_3_CARDS + else -> ProductType.WALLET_3_CARDS + } + SalesProduct( + id = sales.id, + productType = productType, + state = productState, + name = sales.product.name, + notification = sales.notification?.let { notification -> + Notification( + type = notification.type, + title = notification.title, + description = notification.description, + ) + }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt index 2c00aa1181..a6f22bec79 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/di/ShopUseCaseModule.kt @@ -3,6 +3,8 @@ package com.tangem.tap.features.shop.di import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.tap.features.shop.data.DefaultShopRepository import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase +import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase +import com.tangem.tap.features.shop.domain.ShopRepository import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -17,12 +19,26 @@ internal object ShopUseCaseModule { @Provides @ViewModelScoped - fun provideShopifyOrderingAvailabilityUseCase( - tangemTechApi: TangemTechApi, - dispatchers: CoroutineDispatcherProvider, - ): ShopifyOrderingAvailabilityUseCase { + fun provideShopifyOrderingAvailabilityUseCase(shopRepository: ShopRepository): ShopifyOrderingAvailabilityUseCase { return DefaultShopifyOrderingAvailabilityUseCase( - shopRepository = DefaultShopRepository(tangemTechApi, dispatchers), + shopRepository = shopRepository, ) } + + @Provides + @ViewModelScoped + fun provideGetShopifySalesProductsUseCase(shopRepository: ShopRepository): GetShopifySalesProductsUseCase { + return GetShopifySalesProductsUseCase( + shopRepository = shopRepository, + ) + } + + @Provides + @ViewModelScoped + fun provideDefaultShopRepository( + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + ): ShopRepository { + return DefaultShopRepository(tangemTechApi = tangemTechApi, dispatchers = dispatchers) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt b/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt new file mode 100644 index 0000000000..97b5839f47 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/di/ShopifyTogglesModule.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.features.shop.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.tap.features.shop.toggles.DefaultShopifyFeatureToggleManager +import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager +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 ShopifyTogglesModule { + + @Provides + @Singleton + fun provideDefaultShopifyFeatureToggleManager( + featureToggleManager: FeatureTogglesManager, + ): ShopifyFeatureToggleManager { + return DefaultShopifyFeatureToggleManager(featureToggleManager) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt new file mode 100644 index 0000000000..7881b82dea --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/GetShopifySalesProductsUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.tap.features.shop.domain + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.tap.features.shop.domain.models.SalesError +import com.tangem.tap.features.shop.domain.models.SalesProduct + +/** + * Use case to get shopify available products + * + * @property shopRepository shop feature repository + */ +class GetShopifySalesProductsUseCase( + private val shopRepository: ShopRepository, +) { + + suspend operator fun invoke(): Either> { + return try { + shopRepository.getSalesProductInfo().right() + } catch (e: Exception) { + SalesError.DataError(e).left() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt index c8cd6f3780..67803630b8 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/ShopRepository.kt @@ -1,12 +1,17 @@ package com.tangem.tap.features.shop.domain +import com.tangem.tap.features.shop.domain.models.SalesProduct + /** * Shop feature repository * [REDACTED_AUTHOR] */ -internal interface ShopRepository { +interface ShopRepository { /** Get shopify ordering availability */ suspend fun isShopifyOrderingAvailable(): Boolean + + /** Get actual sales product info */ + suspend fun getSalesProductInfo(): List } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt new file mode 100644 index 0000000000..0987fdd8bd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/models/ProductType.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.features.shop.domain.models + +private const val TANGEM_WALLET_2_CARDS_SKU = "TG115X2-S" +private const val TANGEM_WALLET_3_CARDS_SKU = "TG115X3-S" + +enum class ProductType(val sku: String) { + + WALLET_2_CARDS(TANGEM_WALLET_2_CARDS_SKU), + WALLET_3_CARDS(TANGEM_WALLET_3_CARDS_SKU), + ; + + companion object { + val SKUS_TO_DISPLAY = listOf(TANGEM_WALLET_2_CARDS_SKU, TANGEM_WALLET_3_CARDS_SKU) + fun fromSku(sku: String): ProductType? { + return when (sku) { + WALLET_2_CARDS.sku -> WALLET_2_CARDS + WALLET_3_CARDS.sku -> WALLET_3_CARDS + else -> null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt new file mode 100644 index 0000000000..373b8d14c8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesError.kt @@ -0,0 +1,5 @@ +package com.tangem.tap.features.shop.domain.models + +sealed class SalesError { + data class DataError(val cause: Throwable) : SalesError() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt new file mode 100644 index 0000000000..b3b5a083d3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/domain/models/SalesProduct.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.shop.domain.models + +/** + * Sales product + * + * @property id product id + * @property productType shows TW2 cards or 3 cards + * @property state state as order available etc + * @property name product name + * @property notification optional notification + */ +data class SalesProduct( + val id: String, + val productType: ProductType, + val state: ProductState, + val name: String, + val notification: Notification?, +) + +data class Notification( + val type: String, + val title: String, + val description: String, +) + +enum class ProductState { + ORDER, + SOLD_OUT, + PRE_ORDER, +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt index 34966466cc..2e39e2bd4f 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/presentation/ShopViewModel.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.shop.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.tap.features.shop.domain.GetShopifySalesProductsUseCase import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.proxy.AppStateHolder @@ -15,6 +16,7 @@ import javax.inject.Inject * Shop screen view model * * @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability + * @property getShopifySalesProductsUseCase use case to get actual sales info * @property dispatchers coroutine dispatchers provider * @property appStateHolder redux state holder * @@ -23,6 +25,7 @@ import javax.inject.Inject @HiltViewModel internal class ShopViewModel @Inject constructor( private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase, + private val getShopifySalesProductsUseCase: GetShopifySalesProductsUseCase, private val dispatchers: CoroutineDispatcherProvider, private val appStateHolder: AppStateHolder, ) : ViewModel() { @@ -36,4 +39,22 @@ internal class ShopViewModel @Inject constructor( appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility)) } } + + /** + * Get actual sales products info + * to configure view dynamically + */ + fun getActualSalesInfo() { + viewModelScope.launch(dispatchers.main) { + val action = getShopifySalesProductsUseCase().fold( + ifLeft = { + ShopAction.SalesProductsError + }, + ifRight = { + ShopAction.SalesProductsLoaded(it) + }, + ) + appStateHolder.mainStore?.dispatch(action) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt index 54c6bcad43..a72324e845 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopAction.kt @@ -2,9 +2,10 @@ package com.tangem.tap.features.shop.redux import android.content.Intent import com.tangem.tap.common.redux.NotificationAction -import com.tangem.tap.common.shop.data.ProductType +import com.tangem.tap.features.shop.domain.models.ProductType import com.tangem.tap.common.shop.data.TangemProduct import com.tangem.tap.common.shop.googlepay.GooglePayService +import com.tangem.tap.features.shop.domain.models.SalesProduct import com.tangem.wallet.R import org.rekotlin.Action @@ -44,4 +45,8 @@ sealed interface ShopAction : Action { object ResetState : ShopAction data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction + + data class SalesProductsLoaded(val salesProducts: List) : ShopAction + + object SalesProductsError : ShopAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt index 60cb0667ad..11061c7abf 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopReducer.kt @@ -39,5 +39,11 @@ private fun internalReduce(action: Action, state: ShopState): ShopState { is ShopAction.FinishSuccessfulOrder, is ShopAction.LoadProducts.Failure, -> state + is ShopAction.SalesProductsLoaded -> state.copy( + salesProducts = action.salesProducts, + ) + is ShopAction.SalesProductsError -> state.copy( + salesProducts = emptyList(), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt index 491e961e58..08bac48f4d 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/redux/ShopState.kt @@ -1,12 +1,14 @@ package com.tangem.tap.features.shop.redux -import com.tangem.tap.common.shop.data.ProductType +import com.tangem.tap.features.shop.domain.models.ProductType import com.tangem.tap.common.shop.data.TangemProduct +import com.tangem.tap.features.shop.domain.models.SalesProduct import org.rekotlin.StateType data class ShopState( val availableProducts: List = emptyList(), val selectedProduct: ProductType = ProductType.WALLET_3_CARDS, + val salesProducts: List = emptyList(), val promoCode: String? = null, val promoCodeLoading: Boolean = false, val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt new file mode 100644 index 0000000000..eda902f707 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/toggles/DefaultShopifyFeatureToggleManager.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.features.shop.toggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultShopifyFeatureToggleManager( + private val featureTogglesManager: FeatureTogglesManager, +) : ShopifyFeatureToggleManager { + + override val isDynamicSalesProductsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("SHOPIFY_DYNAMIC_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt b/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt new file mode 100644 index 0000000000..ab3b8fc968 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/shop/toggles/ShopifyFeatureToggleManager.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.shop.toggles + +/** + * Shopify feature toggle manager that provides info about shopify toggle availability + * + */ +interface ShopifyFeatureToggleManager { + + val isDynamicSalesProductsEnabled: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt index 4af0db0285..58b89b9901 100644 --- a/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/shop/ui/ShopFragment.kt @@ -17,20 +17,27 @@ import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.extensions.getQuantityString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.shop.data.ProductType import com.tangem.tap.features.BaseStoreFragment +import com.tangem.tap.features.shop.domain.models.ProductState +import com.tangem.tap.features.shop.domain.models.ProductType +import com.tangem.tap.features.shop.domain.models.SalesProduct import com.tangem.tap.features.shop.presentation.ShopViewModel import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.shop.redux.ShopState +import com.tangem.tap.features.shop.toggles.ShopifyFeatureToggleManager import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentShopBinding import dagger.hilt.android.AndroidEntryPoint import org.rekotlin.StoreSubscriber +import javax.inject.Inject @AndroidEntryPoint internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber { + @Inject + lateinit var shopifyFeatureToggleManager: ShopifyFeatureToggleManager + private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind) private var cardTranslationY = 70f @@ -50,7 +57,11 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - viewModel.checkOrderingDelayBlockVisibility() + if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { + viewModel.getActualSalesInfo() + } else { + viewModel.checkOrderingDelayBlockVisibility() + } activity?.onBackPressedDispatcher?.addCallback( this, @@ -142,7 +153,11 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu animateProductSelection(state.selectedProduct) handlePriceState(state) handlePromoCodeState(state) - handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible) + if (shopifyFeatureToggleManager.isDynamicSalesProductsEnabled) { + handleNotificationBlock(state) + } else { + handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible) + } handleButtonsState(state) } @@ -187,6 +202,17 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide() } + private fun handleNotificationBlock(state: ShopState) { + if (isVisible) { + binding.tvSoldOutDesc.show() + getSelectedSalesProduct(state)?.notification?.let { notification -> + binding.tvSoldOutDesc.text = notification.description + } + } else { + binding.tvSoldOutDesc.hide() + } + } + private fun handleButtonsState(state: ShopState) = with(binding) { btnPayGooglePay.root.show(state.isGooglePayAvailable) btnAlternativePayment.show(state.isGooglePayAvailable) @@ -194,8 +220,15 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu if (state.total != null) { btnAlternativePayment.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } - btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } btnPayGooglePay.root.setOnClickListener { store.dispatch(ShopAction.BuyWithGooglePay) } + btnMainAction.setOnClickListener { store.dispatch(ShopAction.StartWebCheckout) } + if (state.salesProducts.isNotEmpty()) { + getSelectedSalesProduct(state)?.let { selectedProduct -> + btnMainAction.text = getMainBtnTextByProductState( + productState = selectedProduct.state, + ) + } + } } } @@ -203,4 +236,16 @@ internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSu store.dispatch(ShopAction.ResetState) super.handleOnBackPressed() } + + private fun getSelectedSalesProduct(state: ShopState): SalesProduct? { + return state.salesProducts.find { + it.productType == state.selectedProduct + } + } + + private fun getMainBtnTextByProductState(productState: ProductState): String = when (productState) { + ProductState.ORDER -> getString(R.string.shop_buy_now) + ProductState.SOLD_OUT -> "Sold out" // getString(R.string.sold_out) // todo finalize in next PR + ProductState.PRE_ORDER -> "Pre order" // getString(R.string.pre_order) // todo finalize in next PR + } } \ 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..57afd33a26 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.blockchain.network.ResultChecker 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, @@ -343,7 +361,7 @@ class TransactionManagerImpl( return SendTxResult.Success } is SimpleResult.Failure -> { - if (result.isNetworkError()) return SendTxResult.NetworkError(result.error) + if (ResultChecker.isNetworkError(result)) return SendTxResult.NetworkError(result.error) val error = result.error as? BlockchainSdkError ?: return SendTxResult.UnknownError() when (error) { is BlockchainSdkError.WrappedTangemError -> { @@ -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/TxHistoryManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt new file mode 100644 index 0000000000..ff8b430a00 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/proxy/TxHistoryManagerImpl.kt @@ -0,0 +1,86 @@ +package com.tangem.tap.proxy + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.WalletManager +import com.tangem.blockchain.common.txhistory.TransactionHistoryItem +import com.tangem.blockchain.common.txhistory.TransactionHistoryState +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.common.BlockchainNetwork +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.lib.crypto.TxHistoryManager +import com.tangem.lib.crypto.models.ProxyAmount +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryItem +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryState +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionStatus + +class TxHistoryManagerImpl( + private val appStateHolder: AppStateHolder, +) : TxHistoryManager { + + override suspend fun checkTxHistoryState(networkId: String, derivationPath: String?): ProxyTransactionHistoryState { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val walletManager = getActualWalletManager(blockchain, derivationPath) + val state = walletManager.getTransactionHistoryState(address = walletManager.wallet.address) + return state.mapToProxy() + } + + override suspend fun getTxHistoryItems( + networkId: String, + derivationPath: String?, + page: Int, + pageSize: Int, + ): List { + val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } + val walletManager = getActualWalletManager(blockchain, derivationPath) + val itemsResult = walletManager.getTransactionsHistory( + address = walletManager.wallet.address, + page = page, + pageSize = pageSize, + ) + + return when (itemsResult) { + is Result.Success -> itemsResult.data.map { historyItem -> historyItem.mapToProxy() } + is Result.Failure -> error(itemsResult.error.message ?: itemsResult.error.customMessage) + } + } + + private fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { + val blockchainNetwork = BlockchainNetwork(blockchain, derivationPath, emptyList()) + val walletManager = appStateHolder.walletState?.getWalletManager(blockchainNetwork) + return requireNotNull(walletManager) { "no wallet manager found" } + } + + private fun TransactionHistoryState.mapToProxy(): ProxyTransactionHistoryState { + return when (this) { + TransactionHistoryState.Success.Empty -> ProxyTransactionHistoryState.Success.Empty + is TransactionHistoryState.Failed.FetchError -> ProxyTransactionHistoryState.Failed.FetchError(exception) + TransactionHistoryState.NotImplemented -> ProxyTransactionHistoryState.NotImplemented + is TransactionHistoryState.Success.HasTransactions -> + ProxyTransactionHistoryState.Success.HasTransactions(txCount) + } + } + + private fun TransactionHistoryItem.mapToProxy() = ProxyTransactionHistoryItem( + txHash = txHash, + timestamp = timestamp, + direction = when (val direction = direction) { + is TransactionHistoryItem.TransactionDirection.Incoming -> + ProxyTransactionHistoryItem.TransactionDirection.Incoming(direction.from) + is TransactionHistoryItem.TransactionDirection.Outgoing -> + ProxyTransactionHistoryItem.TransactionDirection.Outgoing(direction.to) + }, + status = when (status) { + TransactionStatus.Confirmed -> ProxyTransactionStatus.Confirmed + TransactionStatus.Unconfirmed -> ProxyTransactionStatus.Unconfirmed + }, + type = when (type) { + TransactionHistoryItem.TransactionType.Transfer -> ProxyTransactionHistoryItem.TransactionType.Transfer + }, + amount = ProxyAmount( + currencySymbol = amount.currencySymbol, + value = requireNotNull(amount.value) { "Amount value must not be null" }, + decimals = amount.decimals, + ), + ) +} \ No newline at end of file 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..9bae439352 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,16 +2,15 @@ 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 import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.TransactionManager +import com.tangem.lib.crypto.TxHistoryManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.tap.proxy.AppStateHolder -import com.tangem.tap.proxy.DerivationManagerImpl -import com.tangem.tap.proxy.TransactionManagerImpl -import com.tangem.tap.proxy.UserWalletManagerImpl +import com.tangem.tap.proxy.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -43,8 +42,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 @@ -55,6 +59,12 @@ class ProxyModule { ) } + @Provides + @Singleton + fun provideTxHistoryManager(appStateHolder: AppStateHolder): TxHistoryManager { + return TxHistoryManagerImpl(appStateHolder = appStateHolder) + } + // regions FeatureConsumers @Provides @Singleton 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/app/src/main/res/drawable/ic_octaspace_no_color.xml b/app/src/main/res/drawable/ic_octaspace_no_color.xml new file mode 100644 index 0000000000..36751027a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_octaspace_no_color.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + 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..7f08451e5b 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -9,8 +9,10 @@ plugins { dependencies { /** Project */ - implementation(project(":core:utils")) - implementation(project(":libs:auth")) + implementation(projects.core.utils) + implementation(projects.libs.auth) + implementation(projects.domain.core) + implementation(projects.domain.wallets.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) @@ -32,7 +34,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/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 992f6d0eb5..e75594060f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -54,4 +54,10 @@ interface TangemTechApi { @GET("shops") suspend fun getShopInfo(@Query(value = "name") name: String): ShopResponse + + @GET("sales") + suspend fun getSalesInfo( + @Query(value = "locale") locale: String, + @Query(value = "shops") shops: String, + ): SalesResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt new file mode 100644 index 0000000000..1c3461f012 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SalesResponse.kt @@ -0,0 +1,44 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json + +/** + * Sales response + */ +data class SalesResponse( + @Json(name = "sales") val sales: List, +) + +/** + * Sales info + * + * @property id sales id + * @property state state as order, sold-out, pre-order + * @property product product that is sales + * @property notification optional notification for product + */ +data class Sales( + @Json(name = "id") val id: String, + @Json(name = "state") val state: String, + @Json(name = "product") val product: Product, + @Json(name = "notification") val notification: Notification?, +) + +/** + * Product + * + * @property id product id + * @property code code that shows what product it is + * @property name product name + */ +data class Product( + @Json(name = "id") val id: String, + @Json(name = "code") val code: String, + @Json(name = "name") val name: String, +) + +data class Notification( + @Json(name = "type") val type: String, + @Json(name = "title") val title: String, + @Json(name = "description") val description: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 180deb01a0..c4b95bba88 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -4,8 +4,8 @@ import com.squareup.moshi.Json data class UserTokensResponse( @Json(name = "version") val version: Int = 0, - @Json(name = "group") val group: String? = null, - @Json(name = "sort") val sort: String? = null, + @Json(name = "group") val group: GroupType, + @Json(name = "sort") val sort: SortType, @Json(name = "tokens") val tokens: List = emptyList(), ) { @@ -18,4 +18,26 @@ data class UserTokensResponse( @Json(name = "decimals") val decimals: Int, @Json(name = "contractAddress") val contractAddress: String?, ) + + enum class GroupType { + @Json(name = "none") + NONE, + + @Json(name = "token") + TOKEN, + + @Json(name = "network") + NETWORK, + } + + enum class SortType { + @Json(name = "balance") + BALANCE, + + @Json(name = "manual") + MANUAL, + + @Json(name = "marketcap") + MARKETCAP, + } } \ 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..ee39ca9041 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/CacheKeysStoreModule.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.cache.CacheKeysStore +import com.tangem.datasource.local.cache.DefaultCacheKeysStore +import com.tangem.datasource.local.datastore.RuntimeDataStore +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 DefaultCacheKeysStore( + dataStore = RuntimeDataStore(), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 0ab6faea17..65668982d4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -19,8 +19,8 @@ class MoshiModule { @NetworkMoshi fun provideNetworkMoshi(): Moshi { return Moshi.Builder() - .add(KotlinJsonAdapterFactory()) .add(BigDecimalAdapter()) + .add(KotlinJsonAdapterFactory()) .build() } 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/di/UserTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt new file mode 100644 index 0000000000..757c01cef5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.di + +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.datastore.FileDataStore +import com.tangem.datasource.local.token.DefaultUserTokensStore +import com.tangem.datasource.local.token.UserTokensStore +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 UserTokensStoreModule { + + @Provides + @Singleton + fun provideUserTokensStore(fileReader: FileReader, @NetworkMoshi moshi: Moshi): UserTokensStore { + return DefaultUserTokensStore( + dataStore = FileDataStore(fileReader, moshi.adapter(UserTokensResponse::class.java)), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/WalletManagersStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/WalletManagersStoreModule.kt new file mode 100644 index 0000000000..7d7beb051e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/WalletManagersStoreModule.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.walletmanager.DefaultWalletManagersStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +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 WalletManagersStoreModule { + + @Provides + @Singleton + fun provideWalletManagersStore(): WalletManagersStore { + return DefaultWalletManagersStore( + dataStore = RuntimeDataStore(), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt index 8ed8eb243b..5957268681 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/files/AndroidFileReader.kt @@ -5,13 +5,22 @@ import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject class AndroidFileReader @Inject constructor(@ApplicationContext private val context: Context) : FileReader { + override fun readFile(fileName: String): String { - return context.openFileInput(fileName).bufferedReader().readText() + return context.openFileInput(fileName).use { stream -> + stream.bufferedReader().use { reader -> + reader.readText() + } + } } override fun rewriteFile(content: String, fileName: String) { - context.openFileOutput(fileName, Context.MODE_PRIVATE).use { - it.write(content.toByteArray(), 0, content.length) + context.openFileOutput(fileName, Context.MODE_PRIVATE).use { stream -> + stream.write(content.toByteArray(), 0, content.length) } } + + override fun removeFile(fileName: String) { + context.deleteFile(fileName) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt b/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt index bdeb127b9f..27b0f8a8e1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/files/FileReader.kt @@ -1,6 +1,10 @@ package com.tangem.datasource.files interface FileReader { + fun readFile(fileName: String): String + fun rewriteFile(content: String, fileName: String) + + fun removeFile(fileName: String) } \ No newline at end of file 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..962c09661a --- /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 { + + suspend fun getSyncOrNull(key: String): CacheKey? + + suspend fun store(key: CacheKey) + + suspend fun remove(key: String) + + suspend fun clear() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/cache/DefaultCacheKeysStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/cache/DefaultCacheKeysStore.kt new file mode 100644 index 0000000000..a2788d6a87 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/cache/DefaultCacheKeysStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.cache + +import com.tangem.datasource.local.cache.model.CacheKey +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator + +internal class DefaultCacheKeysStore( + dataStore: StringKeyDataStore, +) : CacheKeysStore, StringKeyDataStoreDecorator(dataStore) { + + override fun provideStringKey(key: String): String { + return key + } + + override suspend fun store(key: CacheKey) { + store(key.id, key) + } +} \ 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/datastore/FileDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt new file mode 100644 index 0000000000..c69c6d5c64 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/FileDataStore.kt @@ -0,0 +1,67 @@ +package com.tangem.datasource.local.datastore + +import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.model.WriteTrigger +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.* +import timber.log.Timber + +internal class FileDataStore( + private val fileReader: FileReader, + private val adapter: JsonAdapter, +) : StringKeyDataStore { + + private val writeTrigger = MutableSharedFlow( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + override fun get(key: String): Flow { + return writeTrigger + .onEmpty { emit(WriteTrigger) } + .map { getInternal(key) } + .filterNotNull() + } + + override suspend fun getSyncOrNull(key: String): Value? { + return getInternal(key) + } + + override suspend fun store(key: String, item: Value) { + try { + val json = adapter.toJson(item) + + fileReader.rewriteFile(json, key) + writeTrigger.tryEmit(WriteTrigger) + } catch (e: Throwable) { + Timber.e(e, "Unable to write file: $key") + } + } + + override suspend fun store(items: Map) { + items.forEach { (key, item) -> + store(key, item) + } + } + + override suspend fun remove(key: String) { + fileReader.removeFile(key) + } + + override suspend fun clear() { + // TODO: Implement if needed + } + + private fun getInternal(fileName: String): Value? { + return try { + val json = fileReader.readFile(fileName) + + adapter.fromJson(json) + } catch (e: Throwable) { + Timber.e(e, "Unable to read file: $fileName") + null + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt new file mode 100644 index 0000000000..d1c4eff1eb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/RuntimeDataStore.kt @@ -0,0 +1,49 @@ +package com.tangem.datasource.local.datastore + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import kotlinx.coroutines.flow.* + +internal class RuntimeDataStore : StringKeyDataStore { + + private val store = MutableStateFlow>(hashMapOf()) + + override fun get(key: String): Flow { + return store + .map { value -> value[key] } + .filterNotNull() + } + + override suspend fun getSyncOrNull(key: String): Data? { + return store.value[key] + } + + override suspend fun store(key: String, item: Data) { + store.update { value -> + value[key] = item + + value + } + } + + override suspend fun store(items: Map) { + store.update { value -> + items.forEach { (key, item) -> + value[key] = item + } + + value + } + } + + override suspend fun remove(key: String) { + store.update { value -> + value.remove(key) + + value + } + } + + override suspend fun clear() { + store.update { hashMapOf() } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt new file mode 100644 index 0000000000..7374c56980 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/DataStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.datastore.core + +import kotlinx.coroutines.flow.Flow + +internal interface DataStore { + + fun get(key: Key): Flow + + suspend fun getSyncOrNull(key: Key): Value? + + suspend fun store(key: Key, item: Value) + + suspend fun store(items: Map) + + suspend fun remove(key: Key) + + suspend fun clear() +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStore.kt new file mode 100644 index 0000000000..4c04a89dc5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStore.kt @@ -0,0 +1,3 @@ +package com.tangem.datasource.local.datastore.core + +internal interface StringKeyDataStore : DataStore \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt new file mode 100644 index 0000000000..92daa1a731 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/core/StringKeyDataStoreDecorator.kt @@ -0,0 +1,36 @@ +package com.tangem.datasource.local.datastore.core + +import kotlinx.coroutines.flow.Flow + +internal abstract class StringKeyDataStoreDecorator( + private val dataStore: StringKeyDataStore, +) : DataStore { + + abstract fun provideStringKey(key: Key): String + + override fun get(key: Key): Flow { + return dataStore.get(provideStringKey(key)) + } + + override suspend fun getSyncOrNull(key: Key): Value? { + return dataStore.getSyncOrNull(provideStringKey(key)) + } + + override suspend fun store(key: Key, item: Value) { + dataStore.store(provideStringKey(key), item) + } + + override suspend fun store(items: Map) { + dataStore.store( + items = items.mapKeys { (key, _) -> provideStringKey(key) }, + ) + } + + override suspend fun remove(key: Key) { + dataStore.remove(provideStringKey(key)) + } + + override suspend fun clear() { + dataStore.clear() + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt new file mode 100644 index 0000000000..aaab7f83df --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/datastore/model/WriteTrigger.kt @@ -0,0 +1,3 @@ +package com.tangem.datasource.local.datastore.model + +internal typealias WriteTrigger = Unit \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt new file mode 100644 index 0000000000..43e895bc89 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator +import com.tangem.domain.wallets.models.UserWalletId + +internal class DefaultUserTokensStore( + dataStore: StringKeyDataStore, +) : UserTokensStore, StringKeyDataStoreDecorator(dataStore) { + + override fun provideStringKey(key: UserWalletId): String { + return key.stringValue + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt new file mode 100644 index 0000000000..ed175a871a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface UserTokensStore { + + fun get(key: UserWalletId): Flow + + suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? + + suspend fun store(key: UserWalletId, item: UserTokensResponse) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt new file mode 100644 index 0000000000..a42253ee50 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.local.userwallet + +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId + +interface UserWalletsStore { + + suspend fun getSyncOrNull(key: UserWalletId): UserWallet? +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt new file mode 100644 index 0000000000..bed4e6a55c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/DefaultWalletManagersStore.kt @@ -0,0 +1,43 @@ +package com.tangem.datasource.local.walletmanager + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.WalletManager +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.extensions.plusOrReplace + +internal class DefaultWalletManagersStore( + dataStore: StringKeyDataStore>, +) : WalletManagersStore, StringKeyDataStoreDecorator>(dataStore) { + + override fun provideStringKey(key: UserWalletId): String { + return key.stringValue + } + + override suspend fun getSyncOrNull( + userWalletId: UserWalletId, + blockchain: Blockchain, + derivationPath: String?, + ): WalletManager? { + val walletManagers = getSyncOrNull(userWalletId) + + return walletManagers?.singleOrNull { + it.wallet.blockchain == blockchain && + it.wallet.publicKey.derivationPath?.rawPath == derivationPath + } + } + + override suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) { + val walletManagers = getSyncOrNull(userWalletId) + + val updatedWalletManagers = walletManagers + ?.plusOrReplace(walletManager) { + it.wallet.blockchain == walletManager.wallet.blockchain && + it.wallet.publicKey == walletManager.wallet.publicKey + } + ?: listOf(walletManager) + + store(userWalletId, updatedWalletManagers) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt new file mode 100644 index 0000000000..c025cf6911 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/walletmanager/WalletManagersStore.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.local.walletmanager + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.wallets.models.UserWalletId + +interface WalletManagersStore { + + suspend fun getSyncOrNull( + userWalletId: UserWalletId, + blockchain: Blockchain, + derivationPath: String?, + ): WalletManager? + + suspend fun store(userWalletId: UserWalletId, walletManager: WalletManager) + + suspend fun clear() +} \ 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/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index b5005743ed..b2d4da3602 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -22,5 +22,9 @@ { "name": "REDESIGNED_TOKEN_DETAIL_SCREEN_ENABLED", "version": "undefined" + }, + { + "name": "SHOPIFY_DYNAMIC_ENABLED", + "version": "undefined" } ] 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..c778c6c0a4 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/HorizontalActionChips.kt @@ -0,0 +1,81 @@ +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.extensions.TextReference +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, 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 = TextReference.Str(value = "Buy"), + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ActionButtonConfig( + text = TextReference.Str(value = "Send"), + iconResId = R.drawable.ic_arrow_up_24, + onClick = {}, + ), + ActionButtonConfig( + text = TextReference.Str(value = "Receive"), + iconResId = R.drawable.ic_arrow_down_24, + onClick = {}, + ), + ActionButtonConfig( + text = TextReference.Str(value = "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/actions/ActionButtonConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt index 3e69942c7f..ae19be321b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonConfig.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.components.buttons.actions import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference /** * Action button config @@ -13,7 +14,7 @@ import androidx.annotation.DrawableRes [REDACTED_AUTHOR] */ data class ActionButtonConfig( - val text: String, + val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, val enabled: Boolean = true, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 068172b71e..af38dad59f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -19,6 +19,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.buttons.common.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme /** @@ -103,7 +105,7 @@ private fun Button( SpacerW8() Text( - text = config.text, + text = config.text.resolveReference(), color = if (config.enabled) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.disabled, overflow = TextOverflow.Ellipsis, maxLines = 1, @@ -146,7 +148,17 @@ private fun Preview_ActionButton_Dark(@PreviewParameter(ActionStateProvider::cla private class ActionStateProvider : CollectionPreviewParameterProvider( collection = listOf( - ActionButtonConfig(text = "Enabled", iconResId = R.drawable.ic_arrow_up_24, enabled = true, onClick = {}), - ActionButtonConfig(text = "Disabled", iconResId = R.drawable.ic_arrow_down_24, enabled = false, onClick = {}), + ActionButtonConfig( + text = TextReference.Str(value = "Enabled"), + iconResId = R.drawable.ic_arrow_up_24, + enabled = true, + onClick = {}, + ), + ActionButtonConfig( + text = TextReference.Str(value = "Disabled"), + iconResId = R.drawable.ic_arrow_down_24, + enabled = false, + 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/components/transactions/TransactionState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt index f0502a4a21..b5b6adaaba 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionState.kt @@ -18,7 +18,25 @@ sealed interface TransactionState { open val address: String, open val amount: String, open val timestamp: String, - ) : TransactionState + ) : TransactionState { + + fun copySealed( + address: String = this.address, + amount: String = this.amount, + timestamp: String = this.timestamp, + ): Content { + return when (this) { + is Approved -> copy(address, amount, timestamp) + is Receive -> copy(address, amount, timestamp) + is Send -> copy(address, amount, timestamp) + is Swapped -> copy(address, amount, timestamp) + is Approving -> copy(address, amount, timestamp) + is Receiving -> copy(address, amount, timestamp) + is Sending -> copy(address, amount, timestamp) + is Swapping -> copy(address, amount, timestamp) + } + } + } /** * Content state for processed transaction diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt new file mode 100644 index 0000000000..4045bf505e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionBlock.kt @@ -0,0 +1,80 @@ +package com.tangem.core.ui.components.transactions.empty + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.buttons.actions.ActionButton +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Placeholder for transaction's block without content + * + * @param state component state + * @param modifier modifier + */ +@Composable +fun EmptyTransactionBlock(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.primary) + .padding(vertical = TangemTheme.dimens.spacing24), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + modifier = Modifier.size(TangemTheme.dimens.size64), + painter = painterResource(id = state.iconRes), + contentDescription = null, + ) + + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing32), + textAlign = TextAlign.Center, + text = state.text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + + ActionButton(config = state.actionButtonConfig) + } +} + +@Preview +@Composable +private fun EmptyTransactionBlock_Light( + @PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState, +) { + TangemTheme { + EmptyTransactionBlock(state = state) + } +} + +@Preview +@Composable +private fun EmptyTransactionBlock_Dark( + @PreviewParameter(EmptyTransactionBlockStateProvider::class) state: EmptyTransactionsBlockState, +) { + TangemTheme(isDark = true) { + EmptyTransactionBlock(state = state) + } +} + +private class EmptyTransactionBlockStateProvider : CollectionPreviewParameterProvider( + collection = listOf( + EmptyTransactionsBlockState.Empty(onClick = {}), + EmptyTransactionsBlockState.FailedToLoad(onClick = {}), + EmptyTransactionsBlockState.NotImplemented(onClick = {}), + ), +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt new file mode 100644 index 0000000000..d4c4dad238 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/empty/EmptyTransactionsBlockState.kt @@ -0,0 +1,45 @@ +package com.tangem.core.ui.components.transactions.empty + +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.TextReference + +sealed class EmptyTransactionsBlockState( + val iconRes: Int, + val text: TextReference, + val actionButtonConfig: ActionButtonConfig, +) { + + class FailedToLoad(onClick: () -> Unit) : EmptyTransactionsBlockState( + actionButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_reload), + iconResId = R.drawable.ic_refresh_24, + onClick = onClick, + enabled = true, + ), + iconRes = R.drawable.ic_alert_history_64, + text = TextReference.Res(R.string.transaction_history_error_failed_to_load), + ) + + class Empty(onClick: (() -> Unit)?) : EmptyTransactionsBlockState( + actionButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_buy), + iconResId = R.drawable.ic_plus_24, + onClick = onClick ?: {}, + enabled = onClick != null, + ), + iconRes = R.drawable.img_coin_64, + text = TextReference.Res(R.string.transaction_history_empty_transactions), + ) + + class NotImplemented(onClick: () -> Unit) : EmptyTransactionsBlockState( + actionButtonConfig = ActionButtonConfig( + text = TextReference.Res(R.string.common_explore_transaction_history), + iconResId = R.drawable.ic_arrow_top_right_24, + onClick = onClick, + enabled = true, + ), + iconRes = R.drawable.ic_compass_64, + text = TextReference.Res(R.string.transaction_history_not_supported_description), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index c4eeba732a..c5e8a5e327 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -42,6 +42,7 @@ fun getActiveIconRes(blockchainId: String): Int { "cronos" -> R.drawable.img_cronos_22 "TELOS", "TELOS/test" -> R.drawable.img_telos_22 "aleph-zero", "aleph-zero/test" -> R.drawable.img_azero_22 + "octaspace", "octaspace/test" -> R.drawable.img_octaspace_22 else -> R.drawable.ic_alert_24 } } @@ -87,6 +88,7 @@ fun getActiveIconResByCoinId(coinId: String, networkId: String): Int { "terra" -> R.drawable.img_terra_22 "terra-2" -> R.drawable.img_terra2_22 "telos" -> R.drawable.img_telos_22 + "octaspace" -> R.drawable.img_octaspace_22 else -> R.drawable.ic_alert_24 } } \ 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..ddba0a20cd 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, @@ -62,6 +63,7 @@ data class TangemDimens internal constructor( val size52: Dp = 52.dp, val size56: Dp = 56.dp, val size62: Dp = 62.dp, + val size64: Dp = 64.dp, val size68: Dp = 68.dp, val size70: Dp = 70.dp, val size72: Dp = 72.dp, @@ -101,6 +103,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/ui/src/main/res/drawable/ic_alert_history_64.xml b/core/ui/src/main/res/drawable/ic_alert_history_64.xml new file mode 100644 index 0000000000..51e0bb6e57 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_alert_history_64.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_compass_64.xml b/core/ui/src/main/res/drawable/ic_compass_64.xml new file mode 100644 index 0000000000..0d10b40a2c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_compass_64.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/img_coin_64.xml b/core/ui/src/main/res/drawable/img_coin_64.xml new file mode 100644 index 0000000000..c036101c31 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_coin_64.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_octaspace_22.xml b/core/ui/src/main/res/drawable/img_octaspace_22.xml new file mode 100644 index 0000000000..21a7f76d64 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_octaspace_22.xml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts index 1e8960ce77..bf7c86b763 100644 --- a/core/utils/build.gradle.kts +++ b/core/utils/build.gradle.kts @@ -6,10 +6,16 @@ plugins { dependencies { - /** DI */ + // region DI implementation(deps.hilt.core) kapt(deps.hilt.kapt) + // endregion - /** Coroutines */ + // region Coroutines implementation(deps.kotlin.coroutines) + // endregion + + // region Time dependencies + implementation(deps.jodatime) + // endregion } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/CryptoAddressFormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoAddressFormatExtensions.kt new file mode 100644 index 0000000000..f2b5b09fce --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/CryptoAddressFormatExtensions.kt @@ -0,0 +1,14 @@ +package com.tangem.utils + +/** + * Convert address to brief format. Example, 33BddS...ga2B. + * If [this.length] is less than a sum of [startCharsCount] and [endCharsCount], return [this]. + */ +fun String.toBriefAddressFormat(startCharsCount: Int = 6, endCharsCount: Int = 4): String { + return if (startCharsCount + endCharsCount < length) { + substring(startIndex = 0, endIndex = startCharsCount) + "..." + + substring(startIndex = length - endCharsCount, endIndex = length) + } else { + this + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/FormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt similarity index 100% rename from core/utils/src/main/java/com/tangem/utils/FormatExtensions.kt rename to core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt 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/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt index 89fd92851b..36644cfda9 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt @@ -3,4 +3,110 @@ package com.tangem.utils.extensions /** [REDACTED_AUTHOR] */ -fun Collection.isSingleItem(): Boolean = this.size == 1 \ No newline at end of file + +/** + * Checks if the [Collection] contains a single item. + * + * @return [Boolean] indicating whether the [Collection] contains exactly one element. + */ +fun Collection.isSingleItem(): Boolean = this.size == 1 + +/** + * Creates a shallow copy of this [Collection]. + * + * @return A copy of the [Collection]. + */ +fun Collection.copy(): Collection { + return this.map { it } +} + +/** + * Adds the specified element to the collection or replaces an existing element. + * The predicate defines the condition to replace the existing element. + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + * @return The modified [List] after adding or replacing the element. + */ +inline fun Collection.plusOrReplace(item: T, predicate: (T) -> Boolean): List { + val mutableList = this as? MutableList ?: ArrayList(this) + + mutableList.addOrReplace(item, predicate) + + return mutableList +} + +/** + * Adds the specified element to the collection or replaces an existing element. + * The predicate defines the condition to replace the existing element. + * + * @param item The element to be added or replace the existing one. + * @param predicate The condition to replace an existing element. + */ +inline fun MutableCollection.addOrReplace(item: T, predicate: (T) -> Boolean) { + val isReplaced = replaceBy(item, predicate) + + if (!isReplaced) { + add(item) + } +} + +/** + * Removes an element from the collection based on the provided predicate. + * Uses iterator, avoid using it in COW collections + * + * @param predicate The condition to remove an element. + * @return [Boolean] indicating whether an element was removed. + */ +inline fun MutableCollection.removeByIterate(predicate: (T) -> Boolean): Boolean { + var removed = false + val iterator = this.iterator() + + for (e in iterator) { + if (predicate(e)) { + iterator.remove() + removed = true + + break + } + } + + return removed +} + +/** + * Removes an element from the collection based on the provided predicate. + * Uses removeAll() method and could be used for COW collections + * + * @param predicate The condition to remove an element. + * @return [Boolean] indicating whether an element was removed. + */ +fun MutableList.removeByReplace(predicate: (T) -> Boolean): Boolean { + val toRemove = this.filter(predicate) + this.removeAll(toRemove) + return toRemove.isNotEmpty() +} + +/** + * Replaces an element in the collection with the provided item based on the predicate. + * + * @param item The element to replace the existing one. + * @param predicate The condition to replace an existing element. + * @return [Boolean] indicating whether an element was replaced. + */ +inline fun MutableCollection.replaceBy(item: T, predicate: (T) -> Boolean): Boolean { + var replaced = false + val mutableList = this as? MutableList ?: ArrayList(this) + val iterator = mutableList.listIterator() + + for (e in iterator) { + if (predicate(e)) { + iterator.set(item) + replaced = true + + break + } + } + + return replaced +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/DateTime.kt b/core/utils/src/main/java/com/tangem/utils/extensions/DateTime.kt new file mode 100644 index 0000000000..3fad6d57d7 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/DateTime.kt @@ -0,0 +1,8 @@ +package com.tangem.utils.extensions + +import org.joda.time.DateTime +import org.joda.time.LocalDate + +fun DateTime.isToday(): Boolean = LocalDate.now().equals(LocalDate(this)) + +fun DateTime.isYesterday(): Boolean = LocalDate.now().minusDays(1).equals(LocalDate(this)) \ No newline at end of file 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..0999cc6959 --- /dev/null +++ b/data/card/build.gradle.kts @@ -0,0 +1,26 @@ +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.core.utils) + + 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/DefaultCardRepository.kt b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt new file mode 100644 index 0000000000..b1bc033cb1 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/DefaultCardRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.data.card + +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.card.repository.CardRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultCardRepository( + private val preferencesDataSource: PreferencesDataSource, + private val dispatchers: CoroutineDispatcherProvider, +) : CardRepository { + + override suspend fun wasCardScanned(cardId: String): Boolean { + return withContext(dispatchers.io) { preferencesDataSource.usedCardsPrefStorage.wasScanned(cardId) } + } +} \ 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..b5edf95c45 --- /dev/null +++ b/data/card/src/main/java/com/tangem/data/card/di/CardDataModule.kt @@ -0,0 +1,40 @@ +package com.tangem.data.card.di + +import com.tangem.data.card.DefaultCardRepository +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.CardRepository +import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +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, + ) + } + + @Provides + @Singleton + fun provideCardRepository( + preferencesDataSource: PreferencesDataSource, + dispatchers: CoroutineDispatcherProvider, + ): CardRepository { + return DefaultCardRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers) + } +} \ 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..b8380debee --- /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.getSyncOrNull(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.store( + 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/settings/.gitignore b/data/settings/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/settings/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts new file mode 100644 index 0000000000..1413053d65 --- /dev/null +++ b/data/settings/build.gradle.kts @@ -0,0 +1,26 @@ +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.settings" +} + +dependencies { + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + implementation(deps.kotlin.coroutines) + + implementation(projects.core.utils) + + implementation(projects.domain.settings) + + implementation(projects.data.source.preferences) +} diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt new file mode 100644 index 0000000000..c1ee6fc57e --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -0,0 +1,18 @@ +package com.tangem.data.settings + +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultSettingsRepository( + private val preferencesDataSource: PreferencesDataSource, + private val dispatchers: CoroutineDispatcherProvider, +) : SettingsRepository { + + override suspend fun isUserAlreadyRateApp(): Boolean { + return withContext(dispatchers.io) { + preferencesDataSource.appRatingLaunchObserver.isReadyToShow() + } + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt new file mode 100644 index 0000000000..6ef7e9a280 --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -0,0 +1,25 @@ +package com.tangem.data.settings.di + +import com.tangem.data.settings.DefaultSettingsRepository +import com.tangem.data.source.preferences.PreferencesDataSource +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +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 SettingsDataModule { + + @Provides + @Singleton + fun provideSettingsRepository( + preferencesDataSource: PreferencesDataSource, + dispatchers: CoroutineDispatcherProvider, + ): SettingsRepository { + return DefaultSettingsRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers) + } +} \ 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..5b3c4c0e7a --- /dev/null +++ b/data/tokens/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.tokens" +} + +dependencies { + + /** Project - Domain */ + implementation(projects.domain.core) + implementation(projects.domain.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.demo) + implementation(projects.domain.wallets.models) + + /** Project - Data */ + implementation(projects.core.datasource) + implementation(projects.data.common) + + /** Project - Utils */ + implementation(projects.core.utils) + // FIXME: For blockchain extensions, remove after refactoring + implementation(projects.domain.legacy) + + /** Tangem SDKs */ + implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) + + /** DI */ + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + + /** Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + implementation(deps.moshi.kotlin) + implementation(deps.jodatime) + implementation(deps.timber) +} \ 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..2b512574c4 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -0,0 +1,43 @@ +package com.tangem.data.tokens.di + +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.tokens.repository.DefaultTokensRepository +import com.tangem.data.tokens.repository.MockNetworksRepository +import com.tangem.data.tokens.repository.MockQuotesRepository +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +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.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TokensDataModule { + + @Provides + @Singleton + fun provideTokensRepository( + tangemTechApi: TangemTechApi, + userTokensStore: UserTokensStore, + userWalletsStore: UserWalletsStore, + cacheRegistry: CacheRegistry, + dispatchers: CoroutineDispatcherProvider, + ): TokensRepository { + return DefaultTokensRepository(tangemTechApi, userTokensStore, userWalletsStore, cacheRegistry, dispatchers) + } + + @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..8278cce392 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockNetworks.kt @@ -0,0 +1,58 @@ +package com.tangem.data.tokens.mock + +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network +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"), + ), + hasTransactionsInProgress = false, + ), + ) + + 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, + ), + hasTransactionsInProgress = false, + ), + ) + + 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..f89c208a52 --- /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( + currencyId = MockTokens.token1.id, + fiatRate = BigDecimal("1.23"), + priceChange = BigDecimal("0.01"), + ) + + val quote2 = Quote( + currencyId = MockTokens.token2.id, + fiatRate = BigDecimal("2.34"), + priceChange = BigDecimal("-0.02"), + ) + + val quote3 = Quote( + currencyId = MockTokens.token3.id, + fiatRate = BigDecimal("3.45"), + priceChange = BigDecimal("0.03"), + ) + + val quote4 = Quote( + currencyId = MockTokens.token4.id, + fiatRate = BigDecimal("4.56"), + priceChange = BigDecimal("-0.04"), + ) + + val quote5 = Quote( + currencyId = MockTokens.token5.id, + fiatRate = BigDecimal("5.67"), + priceChange = BigDecimal("0.05"), + ) + + val quote6 = Quote( + currencyId = MockTokens.token6.id, + fiatRate = BigDecimal("6.78"), + priceChange = BigDecimal("-0.06"), + ) + + val quote7 = Quote( + currencyId = MockTokens.token7.id, + fiatRate = BigDecimal("7.89"), + priceChange = BigDecimal("0.07"), + ) + + val quote8 = Quote( + currencyId = MockTokens.token8.id, + fiatRate = BigDecimal("8.90"), + priceChange = BigDecimal("-0.08"), + ) + + val quote9 = Quote( + currencyId = MockTokens.token9.id, + fiatRate = BigDecimal("9.01"), + priceChange = BigDecimal("0.09"), + ) + + val quote10 = Quote( + currencyId = 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..e18e4c126d --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/mock/MockTokens.kt @@ -0,0 +1,133 @@ +package com.tangem.data.tokens.mock + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +internal object MockTokens { + + val token1 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token1"), + networkId = MockNetworks.network1.id, + name = "Token 1", + symbol = "T1", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token2 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token2"), + networkId = MockNetworks.network1.id, + name = "Token 2", + symbol = "T2", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token3 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token3"), + networkId = MockNetworks.network1.id, + name = "Token 3", + symbol = "T3", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token4 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token4"), + networkId = MockNetworks.network2.id, + name = "Token 4", + symbol = "T4", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token5 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token5"), + networkId = MockNetworks.network2.id, + name = "Token 5", + symbol = "T5", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token6 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token6"), + networkId = MockNetworks.network2.id, + name = "Token 6", + symbol = "T6", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token7 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token7"), + networkId = MockNetworks.network3.id, + name = "Token 7", + symbol = "T7", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token8 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token8"), + networkId = MockNetworks.network3.id, + name = "Token 8", + symbol = "T8", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token9 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token9"), + networkId = MockNetworks.network3.id, + name = "Token 9", + symbol = "T9", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token10 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token10"), + networkId = MockNetworks.network3.id, + name = "Token 10", + symbol = "T10", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + + 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), + ) +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt new file mode 100644 index 0000000000..704e25fa68 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultTokensRepository.kt @@ -0,0 +1,154 @@ +package com.tangem.data.tokens.repository + +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.tokens.utils.CardCurrenciesFactory +import com.tangem.data.tokens.utils.ResponseCurrenciesFactory +import com.tangem.data.tokens.utils.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +internal class DefaultTokensRepository( + private val tangemTechApi: TangemTechApi, + private val userTokensStore: UserTokensStore, + private val userWalletsStore: UserWalletsStore, + private val cacheRegistry: CacheRegistry, + private val dispatchers: CoroutineDispatcherProvider, +) : TokensRepository { + + private val demoConfig = DemoConfig() + private val responseCurrenciesFactory = ResponseCurrenciesFactory(demoConfig) + private val cardCurrenciesFactory = CardCurrenciesFactory(demoConfig) + private val userTokensResponseFactory = UserTokensResponseFactory() + + override suspend fun saveTokens( + userWalletId: UserWalletId, + currencies: Set, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ) = withContext(dispatchers.io) { + val response = userTokensResponseFactory.createUserTokensResponse( + currencies = currencies, + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + + storeAndPushTokens(userWalletId, response) + } + + override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { + val userWallet = withContext(dispatchers.io) { + requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + } + require(!userWallet.isMultiCurrency) { + "Single currency wallet excepted, but multi currency wallet was found: $userWalletId" + } + + return cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + } + + override fun getMultiCurrencyWalletCurrencies( + userWalletId: UserWalletId, + refresh: Boolean, + ): Flow> { + return channelFlow { + val userWallet = withContext(dispatchers.io) { + requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + } + require(userWallet.isMultiCurrency) { + "Multi currency wallet excepted, but single currency wallet was found: $userWalletId" + } + + launch(dispatchers.io) { + getMultiCurrencyWalletCurrencies(userWallet).collectLatest(::send) + } + + launch(dispatchers.io) { + fetchTokensIfCacheExpired(userWallet, refresh) + } + } + } + + override fun isTokensGrouped(userWalletId: UserWalletId): Flow { + return userTokensStore.get(userWalletId) + .map { it.group == UserTokensResponse.GroupType.NETWORK } + .flowOn(dispatchers.io) + } + + override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { + return userTokensStore.get(userWalletId) + .map { it.sort == UserTokensResponse.SortType.BALANCE } + .flowOn(dispatchers.io) + } + + private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { + return userTokensStore.get(userWallet.walletId).map { storedTokens -> + responseCurrenciesFactory.createTokens( + response = storedTokens, + card = userWallet.scanResponse.card, + ) + } + } + + private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { + cacheRegistry.invokeOnExpire( + key = getTokensCacheKey(userWallet.walletId), + skipCache = refresh, + block = { fetchTokens(userWallet) }, + ) + } + + private suspend fun fetchTokens(userWallet: UserWallet) { + try { + val response = tangemTechApi.getUserTokens(userWallet.walletId.stringValue) + + userTokensStore.store(userWallet.walletId, response) + } catch (e: Throwable) { + handleFetchTokensErrorOrThrow(userWallet, e) + } + } + + private suspend fun storeAndPushTokens(userWalletId: UserWalletId, response: UserTokensResponse) { + userTokensStore.store(userWalletId, response) + tangemTechApi.saveUserTokens(userWalletId.stringValue, response) + } + + private suspend fun handleFetchTokensErrorOrThrow(userWallet: UserWallet, error: Throwable) { + val errorMessage = error.message ?: throw error + + if (NOT_FOUND_HTTP_CODE in errorMessage) { + val response = userTokensStore.getSyncOrNull(userWallet.walletId) + ?: userTokensResponseFactory.createUserTokensResponse( + currencies = cardCurrenciesFactory.createDefaultCoinsForMultiCurrencyCard( + userWallet.scanResponse.card, + ), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + tangemTechApi.saveUserTokens(userWallet.walletId.stringValue, response) + } else { + throw error + } + } + + private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" + + private companion object { + const val NOT_FOUND_HTTP_CODE = "404" + } +} \ 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..3455d66764 --- /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.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network +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..345f17e6bc --- /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.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +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.currencyId in tokensIds } + .toSet(), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt new file mode 100644 index 0000000000..15fd96b0aa --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CardCurrenciesFactory.kt @@ -0,0 +1,79 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.common.TapWorkarounds.isTestCard +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrency +import timber.log.Timber +import com.tangem.blockchain.common.Token as SdkToken + +internal class CardCurrenciesFactory(private val demoConfig: DemoConfig) { + + fun createDefaultCoinsForMultiCurrencyCard(card: CardDTO): Set { + var blockchains = if (demoConfig.isDemoCardId(card.cardId)) { + demoConfig.demoBlockchains + } else { + listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } + + if (card.isTestCard) { + blockchains = blockchains.mapNotNull { it.getTestnetVersion() } + } + + return blockchains.mapNotNull { createCoin(it, card) }.toSet() + } + + fun createPrimaryCurrencyForSingleCurrencyCard(scanResponse: ScanResponse): CryptoCurrency { + val card = scanResponse.card + val resolver = scanResponse.cardTypesResolver + val blockchain = resolver.getBlockchain() + + val coin = requireNotNull(createCoin(blockchain, card)) { + "Coin for the single currency card cannot be null" + } + val primaryToken = resolver.getPrimaryToken()?.let { token -> + createToken(token, blockchain, card) + } + + return primaryToken ?: coin + } + + private fun createToken(sdkToken: SdkToken, blockchain: Blockchain, card: CardDTO): CryptoCurrency.Token? { + if (blockchain != Blockchain.Unknown) { + Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + return null + } + + return CryptoCurrency.Token( + id = getTokenId(blockchain, sdkToken), + networkId = getNetworkId(blockchain), + name = sdkToken.name, + symbol = sdkToken.symbol, + iconUrl = getTokenIconUrl(blockchain, sdkToken), + decimals = sdkToken.decimals, + isCustom = false, + contractAddress = sdkToken.contractAddress, + derivationPath = getDerivationPath(blockchain, card), + ) + } + + private fun createCoin(blockchain: Blockchain, card: CardDTO): CryptoCurrency.Coin? { + if (blockchain != Blockchain.Unknown) { + Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + return null + } + + return CryptoCurrency.Coin( + id = getCoinId(blockchain), + networkId = getNetworkId(blockchain), + name = blockchain.fullName, + symbol = blockchain.currency, + iconUrl = getCoinIconUrl(blockchain), + decimals = blockchain.decimals(), + derivationPath = getDerivationPath(blockchain, card), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt new file mode 100644 index 0000000000..0a6c78b0e5 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/ResponseCurrenciesFactory.kt @@ -0,0 +1,77 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.common.extensions.fromNetworkId +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.CryptoCurrency +import timber.log.Timber +import com.tangem.blockchain.common.Token as SdkToken + +internal class ResponseCurrenciesFactory(private val demoConfig: DemoConfig) { + + fun createTokens(response: UserTokensResponse, card: CardDTO): Set { + return response.tokens.mapNotNull { createToken(it, card) }.toSet() + } + + private fun createToken(responseToken: UserTokensResponse.Token, card: CardDTO): CryptoCurrency? { + var blockchain = Blockchain.fromNetworkId(responseToken.networkId) + if (blockchain == null || blockchain == Blockchain.Unknown) { + Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") + return null + } + + if (demoConfig.isDemoCardId(card.cardId)) { + blockchain = blockchain.getTestnetVersion() ?: blockchain + } + + val sdkToken = createSdkToken(responseToken) + return if (sdkToken == null) { + createCoin(blockchain, responseToken) + } else { + createToken(blockchain, sdkToken, responseToken.derivationPath) + } + } + + private fun createSdkToken(token: UserTokensResponse.Token): SdkToken? { + return token.contractAddress?.let { contractAddress -> + SdkToken( + name = token.name, + symbol = token.symbol, + contractAddress = contractAddress, + decimals = token.decimals, + id = token.id, + ) + } + } + + private fun createCoin(blockchain: Blockchain, responseToken: UserTokensResponse.Token): CryptoCurrency.Coin { + return CryptoCurrency.Coin( + id = getCoinId(blockchain), + networkId = getNetworkId(blockchain), + name = responseToken.name, + symbol = responseToken.symbol, + decimals = responseToken.decimals, + derivationPath = responseToken.derivationPath, + iconUrl = getCoinIconUrl(blockchain), + ) + } + + private fun createToken(blockchain: Blockchain, sdkToken: Token, derivationPath: String?): CryptoCurrency.Token { + val id = getTokenId(blockchain, sdkToken) + + return CryptoCurrency.Token( + id = id, + networkId = getNetworkId(blockchain), + name = sdkToken.name, + symbol = sdkToken.symbol, + decimals = sdkToken.decimals, + derivationPath = derivationPath, + iconUrl = getTokenIconUrl(blockchain, sdkToken), + contractAddress = sdkToken.contractAddress, + isCustom = isCustomToken(id), + ) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt new file mode 100644 index 0000000000..2d0b915285 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/TokensOperations.kt @@ -0,0 +1,105 @@ +package com.tangem.data.tokens.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.IconsUtil +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.extensions.toCoinId +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.Network +import com.tangem.blockchain.common.Token as SdkToken + +private const val DEFAULT_TOKENS_ICONS_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins" +private const val TOKEN_ICON_SIZE = "large" +private const val TOKEN_ICON_EXT = "png" + +private const val COIN_ID_PREFIX = "coin_" +private const val TOKEN_ID_PREFIX = "token_" +private const val CUSTOM_TOKEN_ID_PREFIX = "custom_token_" +private const val TOKEN_ID_DELIMITER = '#' + +internal fun isCustomToken(tokenId: CryptoCurrency.ID): Boolean { + return tokenId.value.startsWith(CUSTOM_TOKEN_ID_PREFIX) +} + +internal fun getDerivationPath(blockchain: Blockchain, card: CardDTO): String? { + return if (card.settings.isHDWalletAllowed) { + blockchain.derivationPath(card.derivationStyle)?.rawPath + } else { + null + } +} + +internal fun getBlockchain(networkId: Network.ID): Blockchain { + return Blockchain.fromId(networkId.value) +} + +internal fun getNetworkId(blockchain: Blockchain): Network.ID { + val value = blockchain.id + + return Network.ID(value) +} + +internal fun getCoinId(blockchain: Blockchain): CryptoCurrency.ID { + return getTokenOrCoinId(blockchain, token = null) +} + +internal fun getTokenId(blockchain: Blockchain, token: SdkToken): CryptoCurrency.ID { + return getTokenOrCoinId(blockchain, token) +} + +internal fun getResponseTokenId(currency: CryptoCurrency): String? { + return currency.id.value.substringAfter(TOKEN_ID_DELIMITER) + .takeUnless { currency is CryptoCurrency.Token && currency.isCustom } +} + +internal fun getTokenIconUrl(blockchain: Blockchain, token: SdkToken): String? { + val tokenId = token.id + + return if (tokenId == null) { + IconsUtil.getTokenIconUri(blockchain, token)?.toString() + } else { + getTokenIconUrlFromDefaultHost(tokenId) + } +} + +internal fun getCoinIconUrl(blockchain: Blockchain): String? { + val coinId = when (blockchain) { + Blockchain.Unknown -> null + Blockchain.TerraV1, Blockchain.TerraV2 -> blockchain.toCoinId() + else -> blockchain.toNetworkId() + } + + return coinId?.let(::getTokenIconUrlFromDefaultHost) +} + +private fun getTokenOrCoinId(blockchain: Blockchain, token: SdkToken?): CryptoCurrency.ID { + val sdkTokenId = token?.id + val (prefix, suffix) = when { + token == null -> COIN_ID_PREFIX to blockchain.toCoinId() + sdkTokenId == null -> CUSTOM_TOKEN_ID_PREFIX to token.contractAddress + else -> TOKEN_ID_PREFIX to sdkTokenId + } + + val value = buildString { + append(prefix) + append(blockchain.id) + append(TOKEN_ID_DELIMITER) + append(suffix.lowercase()) + } + + return CryptoCurrency.ID(value) +} + +private fun getTokenIconUrlFromDefaultHost(tokenId: String): String { + return buildString { + append(DEFAULT_TOKENS_ICONS_HOST) + append('/') + append(TOKEN_ICON_SIZE) + append('/') + append(tokenId) + append('.') + append(TOKEN_ICON_EXT) + } +} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt new file mode 100644 index 0000000000..ffb4fc9c10 --- /dev/null +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/UserTokensResponseFactory.kt @@ -0,0 +1,42 @@ +package com.tangem.data.tokens.utils + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.domain.common.extensions.toNetworkId +import com.tangem.domain.tokens.model.CryptoCurrency + +internal class UserTokensResponseFactory { + + fun createUserTokensResponse( + currencies: Set, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ): UserTokensResponse { + return UserTokensResponse( + tokens = currencies.map(::createResponseToken), + group = if (isGroupedByNetwork) { + UserTokensResponse.GroupType.NETWORK + } else { + UserTokensResponse.GroupType.NONE + }, + sort = if (isSortedByBalance) { + UserTokensResponse.SortType.BALANCE + } else { + UserTokensResponse.SortType.MANUAL + }, + ) + } + + private fun createResponseToken(currency: CryptoCurrency): UserTokensResponse.Token { + val blockchain = getBlockchain(currency.networkId) + + return UserTokensResponse.Token( + id = getResponseTokenId(currency), + networkId = blockchain.toNetworkId(), + derivationPath = currency.derivationPath, + name = currency.name, + symbol = currency.symbol, + decimals = currency.decimals, + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, + ) + } +} \ No newline at end of file diff --git a/data/txhistory/.gitignore b/data/txhistory/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/txhistory/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts new file mode 100644 index 0000000000..d2aa98d0fd --- /dev/null +++ b/data/txhistory/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.txhistory" +} + +dependencies { + implementation(projects.domain.txhistory) + + implementation(deps.kotlin.coroutines) + implementation(deps.androidx.paging.runtime) + implementation(deps.arrow.core) + + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt new file mode 100644 index 0000000000..6fe0d348de --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.txhistory.di + +import com.tangem.data.txhistory.repository.MockTxHistoryRepository +import com.tangem.domain.txhistory.repository.TxHistoryRepository +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 TxHistoryDataModule { + + @Provides + @Singleton + fun provideTxHistoryRepository(): TxHistoryRepository = MockTxHistoryRepository() +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt new file mode 100644 index 0000000000..583fbaa5ee --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/mock/MockTxHistoryItems.kt @@ -0,0 +1,70 @@ +package com.tangem.data.txhistory.mock + +import com.tangem.domain.txhistory.model.TxHistoryItem +import java.math.BigDecimal + +internal object MockTxHistoryItems { + + private val txHistoryItem1 = TxHistoryItem( + txHash = "noster", + timestamp = System.currentTimeMillis(), + direction = TxHistoryItem.TransactionDirection.Incoming("address"), + status = TxHistoryItem.TxStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem2 = TxHistoryItem( + txHash = "noster", + timestamp = 1689844346000, + direction = TxHistoryItem.TransactionDirection.Incoming("address2"), + status = TxHistoryItem.TxStatus.Unconfirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem3 = TxHistoryItem( + txHash = "noster", + timestamp = 1689757946000, + direction = TxHistoryItem.TransactionDirection.Outgoing("address3"), + status = TxHistoryItem.TxStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem4 = TxHistoryItem( + txHash = "noster", + timestamp = 1689671546000, + direction = TxHistoryItem.TransactionDirection.Incoming("address4"), + status = TxHistoryItem.TxStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem5 = TxHistoryItem( + txHash = "noster", + timestamp = 1689585146000, + direction = TxHistoryItem.TransactionDirection.Outgoing("address5"), + status = TxHistoryItem.TxStatus.Unconfirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + private val txHistoryItem6 = TxHistoryItem( + txHash = "noster", + timestamp = 1689585146000, + direction = TxHistoryItem.TransactionDirection.Incoming("address6"), + status = TxHistoryItem.TxStatus.Confirmed, + type = TxHistoryItem.TransactionType.Transfer, + amount = BigDecimal("1000000000.5"), + ) + + val txHistoryItems = listOf( + txHistoryItem1, + txHistoryItem2, + txHistoryItem3, + txHistoryItem4, + txHistoryItem5, + txHistoryItem6, + ) +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt new file mode 100644 index 0000000000..59829686e4 --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/MockTxHistoryRepository.kt @@ -0,0 +1,25 @@ +package com.tangem.data.txhistory.repository + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import kotlinx.coroutines.flow.Flow + +internal class MockTxHistoryRepository : TxHistoryRepository { + + override suspend fun getTxHistoryItemsCount(networkId: String, derivationPath: String): Int { + return 0 + } + + override fun getTxHistoryItems(networkId: String, pageSize: Int): Flow> { + return Pager( + config = PagingConfig( + pageSize = pageSize, + ), + pagingSourceFactory = { TxHistoryPagingSource() }, + ).flow + } +} \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt new file mode 100644 index 0000000000..3eba3ed6ce --- /dev/null +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -0,0 +1,41 @@ +package com.tangem.data.txhistory.repository.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.tangem.data.txhistory.mock.MockTxHistoryItems +import com.tangem.domain.txhistory.model.TxHistoryItem + +private const val INITIAL_PAGE = 1 + +internal class TxHistoryPagingSource : PagingSource() { + + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1) + } + } + + override suspend fun load(params: LoadParams): LoadResult { + val currentPage = params.key ?: INITIAL_PAGE + return try { + // TODO: [REDACTED_JIRA] + // val result = txHistoryManager.getTxHistoryItems( + // networkId = networkId, + // derivationPath = derivationPath, + // page = currentPage, + // pageSize = params.loadSize, + // ) + val result = MockTxHistoryItems.txHistoryItems + + LoadResult.Page( + data = result, + prevKey = if (currentPage > INITIAL_PAGE) currentPage.minus(1) else null, + // TODO: handle end of reached [REDACTED_JIRA] + nextKey = null, + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} \ No newline at end of file diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index 213bd024b6..71512156cd 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -1,12 +1,22 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) id("configuration") } +android { + namespace = "com.tangem.domain.card" +} + dependencies { - implementation(project(":domain:core")) + implementation(projects.core.analytics.models) + + implementation(projects.domain.demo) + implementation(projects.domain.core) + implementation(projects.domain.legacy) // TODO: Remove after new card scan result was implemented - implementation(project(":domain:models")) + implementation(projects.domain.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/GetCardWasScannedUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/GetCardWasScannedUseCase.kt new file mode 100644 index 0000000000..91c077997d --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/GetCardWasScannedUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.card + +import com.tangem.domain.card.repository.CardRepository + +class GetCardWasScannedUseCase(private val cardRepository: CardRepository) { + + suspend operator fun invoke(cardId: String): Boolean = cardRepository.wasCardScanned(cardId) +} \ 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/CardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt new file mode 100644 index 0000000000..40ac6ec968 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/CardRepository.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.card.repository + +interface CardRepository { + + suspend fun wasCardScanned(cardId: String): Boolean +} \ 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/demo/.gitignore b/domain/demo/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/demo/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/demo/build.gradle.kts b/domain/demo/build.gradle.kts new file mode 100644 index 0000000000..f0c56e2a73 --- /dev/null +++ b/domain/demo/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.demo" +} + +dependencies { + implementation(deps.tangem.blockchain) +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt similarity index 98% rename from domain/legacy/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt rename to domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt index 670e104425..ad6e89bba0 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt +++ b/domain/demo/src/main/java/com/tangem/domain/demo/DemoConfig.kt @@ -1,10 +1,11 @@ -package com.tangem.domain.common.demo +package com.tangem.domain.demo +import com.tangem.blockchain.BuildConfig import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain -import com.tangem.domain.features.BuildConfig import java.math.BigDecimal +// FIXME: Move to :domain:demo:models @Suppress("LargeClass") class DemoConfig { @@ -15,7 +16,7 @@ class DemoConfig { Blockchain.Solana, ) - val demoCardIds: List by lazy { + private val demoCardIds: List by lazy { val demoIds = getReleaseIds().toMutableList() if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds) diff --git a/domain/demo/src/main/java/com/tangem/domain/demo/IsDemoCardUseCase.kt b/domain/demo/src/main/java/com/tangem/domain/demo/IsDemoCardUseCase.kt new file mode 100644 index 0000000000..209d7984ba --- /dev/null +++ b/domain/demo/src/main/java/com/tangem/domain/demo/IsDemoCardUseCase.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.demo + +class IsDemoCardUseCase(private val config: DemoConfig) { + + operator fun invoke(cardId: String): Boolean = config.isDemoCardId(cardId) +} \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index f618e11fd9..99665896d3 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -9,7 +9,11 @@ dependencies { implementation(project(":core:utils")) implementation(project(":common")) implementation(project(":libs:auth")) - implementation(project(":domain:models")) + implementation(projects.domain.demo) + implementation(projects.domain.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) /** Tangem libraries */ implementation(deps.tangem.blockchain) { @@ -22,12 +26,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/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt index ed40b0b3b6..10d2f401bd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt @@ -26,4 +26,20 @@ interface CardTypesResolver { fun getPrimaryToken(): Token? fun getBackupCardsCount(): Int + + fun isReleaseFirmwareType(): Boolean + + fun getRemainingSignatures(): Int? + + fun getCardId(): String + + fun isTestCard(): Boolean + + fun isAttestationFailed(): Boolean + + fun hasWalletSignedHashes(): Boolean + + fun hasBackup(): Boolean + + fun isBackupForbidden(): Boolean } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt index 9f993a407b..0eb5a72085 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt @@ -10,6 +10,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType +import com.tangem.operations.attestation.Attestation internal class TangemCardTypesResolver( private val card: CardDTO, @@ -73,6 +74,27 @@ internal class TangemCardTypesResolver( override fun getBackupCardsCount(): Int = card.wallets.size + override fun isReleaseFirmwareType(): Boolean = card.firmwareVersion.type == FirmwareVersion.FirmwareType.Release + + override fun getRemainingSignatures(): Int? = card.wallets.firstOrNull()?.remainingSignatures + + override fun getCardId(): String = card.cardId + + override fun isTestCard(): Boolean = card.isTestCard + + override fun isAttestationFailed(): Boolean = card.attestation.status == Attestation.Status.Failed + + override fun hasWalletSignedHashes(): Boolean { + return card.wallets.any { + val totalSignedHashes = it.totalSignedHashes ?: 0 + totalSignedHashes > 0 + } + } + + override fun hasBackup(): Boolean = card.backupStatus != CardDTO.BackupStatus.NoBackup + + override fun isBackupForbidden(): Boolean = !(card.settings.isBackupAllowed || card.settings.isHDWalletAllowed) + private fun Blockchain.Companion.fromBlockchainName(blockchainName: String): Blockchain { // workaround for BSC (BNB) notes cards return when (blockchainName) { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt index 9bd2291a93..c54218c39a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt @@ -1,6 +1,9 @@ package com.tangem.domain.common.extensions import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.common.card.EllipticCurve +import java.math.BigDecimal @Suppress("ComplexMethod") fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { @@ -64,6 +67,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "telos/test" -> Blockchain.TelosTestnet "aleph-zero" -> Blockchain.AlephZero "aleph-zero/test" -> Blockchain.AlephZeroTestnet + "octaspace" -> Blockchain.OctaSpace + "octaspace/test" -> Blockchain.OctaSpaceTestnet else -> null } } @@ -132,6 +137,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.TelosTestnet -> "telos/test" Blockchain.AlephZero -> "aleph-zero" Blockchain.AlephZeroTestnet -> "aleph-zero/test" + Blockchain.OctaSpace -> "octaspace" + Blockchain.OctaSpaceTestnet -> "octaspace/test" } } @@ -175,6 +182,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Cronos -> "crypto-com-chain" Blockchain.Telos, Blockchain.TelosTestnet -> "telos" Blockchain.AlephZero, Blockchain.AlephZeroTestnet -> "aleph-zero" + Blockchain.OctaSpace, Blockchain.OctaSpaceTestnet -> "octaspace" } } @@ -182,6 +190,35 @@ fun Blockchain.isSupportedInApp(): Boolean { return !excludedBlockchains.contains(this) } +fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { + return when (this) { + Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(NODL_AMOUNT_TO_CREATE_ACCOUNT) else BigDecimal.ONE + Blockchain.XRP -> BigDecimal.TEN + else -> null + } +} + +fun Blockchain.minimalAmount(): BigDecimal { + return 1.toBigDecimal().movePointLeft(decimals()) +} + +fun Blockchain.getPrimaryCurve(): EllipticCurve? { + return when { + getSupportedCurves().contains(EllipticCurve.Secp256k1) -> { + EllipticCurve.Secp256k1 + } + getSupportedCurves().contains(EllipticCurve.Ed25519) -> { + EllipticCurve.Ed25519 + } + else -> { + null + } + } +} + +private const val NODL = "NODL" +private const val NODL_AMOUNT_TO_CREATE_ACCOUNT = 1.5 + private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Ducatus, diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt similarity index 65% rename from app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt rename to domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt index c622163be0..86ce14a4f4 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/WalletManagerFactory.kt @@ -1,17 +1,14 @@ -package com.tangem.tap.domain.extensions +package com.tangem.domain.common.extensions import com.tangem.blockchain.common.* import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toMapKey -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.features.wallet.models.Currency fun WalletManagerFactory.makeWalletManagerForApp( scanResponse: ScanResponse, @@ -31,14 +28,14 @@ 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, curve = wallet.curve, ) } - scanResponse.card.isHdWalletAllowedByApp && seedKey != null && derivationParams != null -> { + scanResponse.card.settings.isHDWalletAllowed && seedKey != null && derivationParams != null -> { val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()] val derivationPath = when (derivationParams) { is DerivationParams.Default -> blockchain.derivationPath(derivationParams.style) @@ -47,7 +44,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( val derivedKey = derivedKeys?.get(derivationPath) ?: return null - makeWalletManager( + createWalletManager( blockchain = environmentBlockchain, seedKey = wallet.publicKey, derivedKey = derivedKey, @@ -55,7 +52,7 @@ fun WalletManagerFactory.makeWalletManagerForApp( ) } else -> { - makeWalletManager( + createLegacyWalletManager( blockchain = environmentBlockchain, walletPublicKey = wallet.publicKey, curve = wallet.curve, @@ -64,23 +61,8 @@ fun WalletManagerFactory.makeWalletManagerForApp( } } -fun WalletManagerFactory.makeWalletManagerForApp( - scanResponse: ScanResponse, - blockchainNetwork: BlockchainNetwork, -): WalletManager? { - return makeWalletManagerForApp( - scanResponse, - blockchain = blockchainNetwork.blockchain, - derivationParams = getDerivationParams(blockchainNetwork.derivationPath, scanResponse.card), - ) -} - -private fun getDerivationParams(derivationPath: String?, card: CardDTO): DerivationParams? { - return derivationPath?.let { - DerivationParams.Custom( - DerivationPath(it), - ) - } ?: if (!card.settings.isHDWalletAllowed) { +private fun getDerivationParams(card: CardDTO): DerivationParams? { + return if (!card.settings.isHDWalletAllowed) { null } else if (card.useOldStyleDerivation) { DerivationParams.Default(DerivationStyle.LEGACY) @@ -89,30 +71,13 @@ private fun getDerivationParams(derivationPath: String?, card: CardDTO): Derivat } } -fun WalletManagerFactory.makeWalletManagerForApp(scanResponse: ScanResponse, currency: Currency): WalletManager? { - return makeWalletManagerForApp( - scanResponse, - blockchain = currency.blockchain, - derivationParams = getDerivationParams(currency.derivationPath, scanResponse.card), - ) -} - -fun WalletManagerFactory.makeWalletManagersForApp( - scanResponse: ScanResponse, - blockchains: List, -): List { - return blockchains - .filter { it.isBlockchain() } - .mapNotNull { this.makeWalletManagerForApp(scanResponse, it) } -} - fun WalletManagerFactory.makePrimaryWalletManager(scanResponse: ScanResponse): WalletManager? { val blockchain = if (scanResponse.card.isTestCard) { scanResponse.cardTypesResolver.getBlockchain().getTestnetVersion() ?: return null } else { scanResponse.cardTypesResolver.getBlockchain() } - val derivationParams = getDerivationParams(null, scanResponse.card) + val derivationParams = getDerivationParams(scanResponse.card) return makeWalletManagerForApp( scanResponse = scanResponse, blockchain = blockchain, 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/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt new file mode 100644 index 0000000000..09bc5ac1dd --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -0,0 +1,157 @@ +package com.tangem.domain.walletmanager + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.WalletManager +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.common.TapWorkarounds.derivationStyle +import com.tangem.domain.common.util.hasDerivation +import com.tangem.domain.demo.DemoConfig +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.walletmanager.utils.SdkTokenConverter +import com.tangem.domain.walletmanager.utils.UpdateWalletManagerResultFactory +import com.tangem.domain.walletmanager.utils.WalletManagerFactory +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import timber.log.Timber + +// FIXME: Move to its own module and make internal +@Deprecated("Inject the WalletManagerFacade interface using DI instead") +class DefaultWalletManagersFacade( + private val walletManagersStore: WalletManagersStore, + private val userWalletsStore: UserWalletsStore, + configManager: ConfigManager, +) : WalletManagersFacade { + + private val demoConfig by lazy { DemoConfig() } + private val resultFactory by lazy { UpdateWalletManagerResultFactory() } + private val walletManagerFactory by lazy { WalletManagerFactory(configManager) } + private val sdkTokenConverter by lazy { SdkTokenConverter() } + + override suspend fun update( + userWalletId: UserWalletId, + networkId: Network.ID, + extraTokens: Set, + ): UpdateWalletManagerResult { + val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + val blockchain = Blockchain.fromId(networkId.value) + + return getAndUpdateWalletManager(userWallet, blockchain, extraTokens) + } + + override suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String { + val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "Unable to find a user wallet with provided ID: $userWalletId" + } + + val blockchain = Blockchain.fromId(networkId.value) + + return getOrCreateWalletManager( + userWallet = userWallet, + blockchain = blockchain, + derivationPath = blockchain.derivationPath(userWallet.scanResponse.card.derivationStyle), + ) + ?.wallet + ?.getExploreUrl() + .orEmpty() + } + + private suspend fun getAndUpdateWalletManager( + userWallet: UserWallet, + blockchain: Blockchain, + extraTokens: Set, + ): UpdateWalletManagerResult { + val scanResponse = userWallet.scanResponse + val derivationPath = blockchain.derivationPath(scanResponse.card.derivationStyle) + + if (derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath)) { + Timber.e("Derivation missed for: $blockchain") + return UpdateWalletManagerResult.MissedDerivation + } + + val walletManager = getOrCreateWalletManager(userWallet, blockchain, derivationPath) + if (walletManager == null || blockchain == Blockchain.Unknown) { + Timber.e("Unable to get a wallet manager for blockchain: $blockchain") + return UpdateWalletManagerResult.Unreachable + } + + updateWalletManagerTokensIfNeeded(walletManager, extraTokens) + + return try { + if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { + updateDemoWalletManager(walletManager, extraTokens) + } else { + updateWalletManager(walletManager) + } + } finally { + walletManagersStore.store(userWallet.walletId, walletManager) + } + } + + private fun updateDemoWalletManager( + walletManager: WalletManager, + tokens: Set, + ): UpdateWalletManagerResult { + val amount = demoConfig.getBalance(walletManager.wallet.blockchain) + walletManager.wallet.setAmount(amount) + + return resultFactory.getDemoResult(amount, tokens) + } + + private suspend fun updateWalletManager(walletManager: WalletManager): UpdateWalletManagerResult { + return try { + walletManager.update() + + resultFactory.getResult(walletManager) + } catch (e: BlockchainSdkError.AccountNotFound) { + resultFactory.getNoAccountResult(walletManager) + } catch (e: Throwable) { + Timber.e(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}") + + UpdateWalletManagerResult.Unreachable + } + } + + private suspend fun getOrCreateWalletManager( + userWallet: UserWallet, + blockchain: Blockchain, + derivationPath: DerivationPath?, + ): WalletManager? { + val userWalletId = userWallet.walletId + + var walletManager = walletManagersStore.getSyncOrNull( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = derivationPath?.rawPath, + ) + + if (walletManager == null) { + walletManager = walletManagerFactory.createWalletManager( + scanResponse = userWallet.scanResponse, + blockchain = blockchain, + derivationPath = derivationPath, + ) ?: return null + + walletManagersStore.store(userWalletId, walletManager) + } + + return walletManager + } + + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { + if (tokens.isEmpty()) return + + val tokensToAdd = sdkTokenConverter + .convertList(tokens.toList()) + .filter { it !in walletManager.cardTokens } + + walletManager.addTokens(tokensToAdd) + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt new file mode 100644 index 0000000000..a2ed074675 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.walletmanager + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import com.tangem.domain.wallets.models.UserWalletId + +// TODO: Move to its own module +/** + * A facade for managing wallets. + */ +interface WalletManagersFacade { + + /** + * Updates the wallet manager associated with a user's wallet and network. + * + * @param userWalletId The ID of the user's wallet. + * @param networkId The network ID. + * @param extraTokens Additional tokens. + * @return The result of updating the wallet manager. + */ + suspend fun update( + userWalletId: UserWalletId, + networkId: Network.ID, + extraTokens: Set, + ): UpdateWalletManagerResult + + suspend fun getExploreUrl(userWalletId: UserWalletId, networkId: Network.ID): String +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt new file mode 100644 index 0000000000..192be587e5 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/CryptoCurrencyAmount.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.walletmanager.model + +import java.math.BigDecimal + +sealed class CryptoCurrencyAmount { + + abstract val value: BigDecimal + + data class Coin(override val value: BigDecimal) : CryptoCurrencyAmount() + + data class Token( + val tokenContractAddress: String, + override val value: BigDecimal, + ) : CryptoCurrencyAmount() +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt new file mode 100644 index 0000000000..a516c03d0a --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/model/UpdateWalletManagerResult.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.walletmanager.model + +import java.math.BigDecimal + +sealed class UpdateWalletManagerResult { + + object MissedDerivation : UpdateWalletManagerResult() + + object Unreachable : UpdateWalletManagerResult() + + data class Verified( + val tokensAmounts: Set, + val hasTransactionsInProgress: Boolean, // TODO: May be add recent transactions + ) : UpdateWalletManagerResult() + + data class NoAccount(val amountToCreateAccount: BigDecimal) : UpdateWalletManagerResult() +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt new file mode 100644 index 0000000000..8fe4bdc174 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTokenConverter.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.converter.Converter +import com.tangem.blockchain.common.Token as SdkToken + +internal class SdkTokenConverter : Converter { + + override fun convert(value: CryptoCurrency.Token): SdkToken { + return SdkToken( + id = value.id.value.takeUnless { value.isCustom }, + name = value.name, + symbol = value.symbol, + contractAddress = value.contractAddress, + decimals = value.decimals, + ) + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt new file mode 100644 index 0000000000..bdf864f4fe --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -0,0 +1,87 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.TransactionStatus +import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.common.extensions.amountToCreateAccount +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.walletmanager.model.CryptoCurrencyAmount +import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult +import timber.log.Timber +import java.math.BigDecimal + +internal class UpdateWalletManagerResultFactory { + + fun getResult(walletManager: WalletManager): UpdateWalletManagerResult.Verified { + val hasNotConfirmedTransactions = walletManager.wallet + .recentTransactions + .any { it.status != TransactionStatus.Confirmed } + + val amounts = walletManager.wallet.amounts + + return UpdateWalletManagerResult.Verified( + tokensAmounts = getTokensAmounts(amounts.values.toSet()), + hasTransactionsInProgress = hasNotConfirmedTransactions, + ) + } + + fun getDemoResult(demoAmount: Amount, tokens: Set): UpdateWalletManagerResult.Verified { + return UpdateWalletManagerResult.Verified( + tokensAmounts = getDemoTokensAmounts(demoAmount, tokens), + hasTransactionsInProgress = false, + ) + } + + fun getNoAccountResult(walletManager: WalletManager): UpdateWalletManagerResult.NoAccount { + val wallet = walletManager.wallet + val blockchain = wallet.blockchain + val amountToCreateAccount = blockchain.amountToCreateAccount( + token = wallet.getTokens().firstOrNull(), + ) + + requireNotNull(amountToCreateAccount) { + "Unable to get required amount to create account for: $blockchain" + } + + return UpdateWalletManagerResult.NoAccount(amountToCreateAccount) + } + + private fun getTokensAmounts(amounts: Set): Set { + val mutableAmounts = hashSetOf() + + return amounts.mapNotNullTo(mutableAmounts, ::getTokenAmount) + } + + private fun getDemoTokensAmounts(demoAmount: Amount, tokens: Set): Set { + val amountValue = demoAmount.value ?: BigDecimal.ZERO + val demoAmounts = hashSetOf(CryptoCurrencyAmount.Coin(amountValue)) + + return tokens.mapTo(demoAmounts) { token -> + CryptoCurrencyAmount.Token(token.contractAddress, amountValue) + } + } + + private fun getTokenAmount(amount: Amount): CryptoCurrencyAmount? { + return when (val type = amount.type) { + is AmountType.Token -> CryptoCurrencyAmount.Token( + tokenContractAddress = type.token.contractAddress, + value = getAmountValue(amount) ?: return null, + ) + is AmountType.Coin -> CryptoCurrencyAmount.Coin( + value = getAmountValue(amount) ?: return null, + ) + is AmountType.Reserve -> null + } + } + + private fun getAmountValue(amount: Amount): BigDecimal? { + val value = amount.value + + if (value == null) { + Timber.e("Amount not found for currency: ${amount.currencySymbol}") + } + + return value + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt new file mode 100644 index 0000000000..f588b47702 --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt @@ -0,0 +1,47 @@ +package com.tangem.domain.walletmanager.utils + +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.datasource.config.ConfigManager +import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation +import com.tangem.domain.common.extensions.makeWalletManagerForApp +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse + +internal class WalletManagerFactory( + private val configManager: ConfigManager, +) { + + private val sdkWalletManagerFactory by lazy { + WalletManagerFactory(configManager.config.blockchainSdkConfig) + } + + fun createWalletManager( + scanResponse: ScanResponse, + blockchain: Blockchain, + derivationPath: DerivationPath?, + ): WalletManager? { + val derivationParams = getDerivationParams(derivationPath, scanResponse.card) + + return sdkWalletManagerFactory.makeWalletManagerForApp( + scanResponse = scanResponse, + blockchain = blockchain, + derivationParams = derivationParams, + ) + } + + private fun getDerivationParams(derivationPath: DerivationPath?, card: CardDTO): DerivationParams? { + val derivationStyle = when { + !card.settings.isHDWalletAllowed -> return null + card.useOldStyleDerivation -> DerivationStyle.LEGACY + else -> DerivationStyle.NEW + } + + return if (derivationPath == null) { + DerivationParams.Default(derivationStyle) + } else { + DerivationParams.Custom(derivationPath) + } + } +} \ No newline at end of file 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..1777aa498a 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 @@ -11,6 +11,7 @@ import com.tangem.operations.attestation.Attestation import java.util.* import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion +// TODO: Move to :domain:card:models /** * [Card] copy * */ @@ -304,7 +305,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/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt index 352a0564f1..c6f1d9f75c 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt @@ -5,6 +5,7 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.derivation.ExtendedPublicKeysMap +// TODO: Move to :domain:card:models /** [REDACTED_AUTHOR] */ diff --git a/domain/settings/.gitignore b/domain/settings/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/settings/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/settings/build.gradle.kts b/domain/settings/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/settings/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsUserAlreadyRateAppUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsUserAlreadyRateAppUseCase.kt new file mode 100644 index 0000000000..fa71e80df8 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsUserAlreadyRateAppUseCase.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SettingsRepository + +class IsUserAlreadyRateAppUseCase(private val settingsRepository: SettingsRepository) { + + suspend operator fun invoke(): Boolean = settingsRepository.isUserAlreadyRateApp() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt new file mode 100644 index 0000000000..c33c9d4cf7 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SettingsRepository.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.settings.repositories + +interface SettingsRepository { + + suspend fun isUserAlreadyRateApp(): Boolean +} \ No newline at end of file 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..4099f104eb --- /dev/null +++ b/domain/tokens/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + implementation(projects.domain.core) + implementation(projects.domain.tokens.models) + 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/models/.gitignore b/domain/tokens/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/tokens/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts new file mode 100644 index 0000000000..7ff7fb7522 --- /dev/null +++ b/domain/tokens/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/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt new file mode 100644 index 0000000000..48ba69226f --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/Network.kt @@ -0,0 +1,29 @@ +package com.tangem.domain.tokens.models + +/** + * Represents a blockchain network, identified by a unique ID and a human-readable name. + * + * @property id The unique identifier of the network, encapsulated as an inline value class. + * @property name The human-readable name of the network, such as "Ethereum" or "Bitcoin". + * + * @throws IllegalArgumentException If the name or ID is blank. + */ +data class Network(val id: ID, val name: String) { + + init { + require(name.isNotBlank()) { "Network name must not be blank" } + } + + /** + * Represents a unique identifier for a network. + * + * @property value The string value of the network ID. + */ + @JvmInline + value class ID(val value: String) { + + init { + require(value.isNotBlank()) { "Network ID must not be blank" } + } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt new file mode 100644 index 0000000000..89d9776a18 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt @@ -0,0 +1,90 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensureNotNull +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.repository.TokensRepository +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withContext + +class ApplyTokenListSortingUseCase( + private val tokensRepository: TokensRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + sortedTokensIds: Set>, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ): Either { + return withContext(dispatchers.default) { + either { + applySorting( + userWalletId = userWalletId, + tokens = sortTokens(sortedTokensIds, getCurrencies(userWalletId)), + isGrouped = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) + } + } + } + + private suspend fun Raise.sortTokens( + sortedTokensIds: Set>, + unsortedTokens: Set, + ): Set = withContext(dispatchers.default) { + val nonEmptySortedTokensIds = ensureNotNull(sortedTokensIds.toNonEmptySetOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + + val sortedTokens = sortedMapOf() + + unsortedTokens.forEach { token -> + val index = nonEmptySortedTokensIds.indexOfFirst { (networkId, tokenId) -> + networkId == token.networkId && tokenId == token.id + } + + if (index >= 0) { + sortedTokens[index] = token + } else { + raise(TokenListSortingError.UnableToSortTokenList) + } + } + + ensureNotNull(sortedTokens.values.toNonEmptySetOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + } + + private suspend fun Raise.getCurrencies(userWalletId: UserWalletId): Set { + val tokens = catch( + block = { tokensRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh = false).firstOrNull() }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + + return ensureNotNull(tokens?.toNonEmptySetOrNull()) { + TokenListSortingError.TokenListIsEmpty + } + } + + private suspend fun Raise.applySorting( + userWalletId: UserWalletId, + tokens: Set, + isGrouped: Boolean, + isSortedByBalance: Boolean, + ) = withContext(dispatchers.io) { + catch( + block = { tokensRepository.saveTokens(userWalletId, tokens, isGrouped, isSortedByBalance) }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt new file mode 100644 index 0000000000..bfc25b58da --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCase.kt @@ -0,0 +1,63 @@ +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.TokenError +import com.tangem.domain.tokens.error.mapper.mapToTokenError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +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.collectLatest + +class GetPrimaryCurrencyUseCase( + private val tokensRepository: TokensRepository, + private val quotesRepository: QuotesRepository, + private val networksRepository: NetworksRepository, + private val dispatchers: CoroutineDispatcherProvider, +) { + + operator fun invoke( + userWalletId: UserWalletId, + refresh: Boolean = false, + ): Flow> { + return channelFlow { + recover( + block = { + getToken(userWalletId, refresh).collectLatest { token -> + send(token.right()) + } + }, + recover = { error -> + send(error.left()) + }, + ) + } + } + + private suspend fun Raise.getToken( + userWalletId: UserWalletId, + refresh: Boolean, + ): Flow { + val operations = CurrenciesStatusesOperations( + tokensRepository = tokensRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + refresh = refresh, + dispatchers = dispatchers, + raise = this, + transformError = CurrenciesStatusesOperations.Error::mapToTokenError, + ) + + return operations.getPrimaryCurrencyStatusFlow() + } +} \ 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..0a142e1241 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -0,0 +1,80 @@ +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.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.TokenListOperations +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.collectLatest +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).collectLatest { 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 = CurrenciesStatusesOperations( + userWalletId = userWalletId, + refresh = refresh, + useCase = this@GetTokenListUseCase, + raise = this, + transformError = CurrenciesStatusesOperations.Error::mapToTokenListError, + ) + + return operations.getMultiCurrencyWalletStatusesFlow() + } + + 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/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt new file mode 100644 index 0000000000..fe70f6aacd --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt @@ -0,0 +1,81 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.Raise +import arrow.core.raise.catch +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.models.Network +import com.tangem.domain.tokens.operations.TokenListSortingOperations +import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +class ToggleTokenListGroupingUseCase( + private val networksRepository: NetworksRepository, + 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 -> ungroupTokens(tokenList) + is TokenList.Ungrouped -> groupTokens(tokenList) + is TokenList.NotInitialized -> raise(TokenListSortingError.TokenListIsEmpty) + } + } + } + } + + private suspend fun Raise.groupTokens( + tokenList: TokenList.Ungrouped, + ): TokenList.GroupedByNetwork { + val sortingOperations = getSortingOperations(tokenList) + val tokens = sortingOperations.getTokens() + + val networks = getNetworks(tokens.map { it.currency.networkId }.toSet()) + return TokenList.GroupedByNetwork( + groups = sortingOperations.getGroupedTokens(networks), + totalFiatBalance = tokenList.totalFiatBalance, + sortedBy = sortingOperations.getSortType(), + ) + } + + private suspend fun Raise.ungroupTokens( + tokenList: TokenList.GroupedByNetwork, + ): TokenList.Ungrouped { + val sortingOperations = getSortingOperations(tokenList) + + return TokenList.Ungrouped( + currencies = sortingOperations.getTokens(), + totalFiatBalance = tokenList.totalFiatBalance, + sortedBy = sortingOperations.getSortType(), + ) + } + + private fun Raise.getSortingOperations(tokenList: TokenList): TokenListSortingOperations<*> { + return TokenListSortingOperations( + tokenList = tokenList, + dispatchers = dispatchers, + raise = this, + transformError = TokenListSortingOperations.Error::mapToTokenListSortingError, + ) + } + + private suspend fun Raise.getNetworks(networksIds: Set): Set { + return withContext(dispatchers.io) { + catch( + block = { networksRepository.getNetworks(networksIds) }, + catch = { raise(TokenListSortingError.DataError(it)) }, + ) + } + } +} \ 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..b91ce9521a --- /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( + currencies = 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/TokenError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt new file mode 100644 index 0000000000..b8b3e0631b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.tokens.error + +sealed class TokenError { + + object UnableToCreateToken : TokenError() + + data class DataError(val cause: Throwable) : TokenError() +} \ 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..847432134b --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/TokenListSortingError.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.tokens.error + +sealed class TokenListSortingError { + + object TokenListIsLoading : TokenListSortingError() + + object TokenListIsEmpty : TokenListSortingError() + + object UnableToSortTokenList : TokenListSortingError() + + data class DataError(val cause: Throwable) : TokenListSortingError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt new file mode 100644 index 0000000000..e129b9fc6f --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/GetWalletTokenErrorMappers.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.tokens.error.mapper + +import com.tangem.domain.tokens.error.TokenError +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations + +internal fun CurrenciesStatusesOperations.Error.mapToTokenError(): TokenError { + return when (this) { + is CurrenciesStatusesOperations.Error.DataError -> TokenError.DataError(this.cause) + is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, + is CurrenciesStatusesOperations.Error.EmptyQuotes, + is CurrenciesStatusesOperations.Error.EmptyCurrencies, + is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, + -> TokenError.UnableToCreateToken + } +} \ 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..4796f50063 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.tokens.error.mapper + +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.TokenListOperations + +internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenListError { + return when (this) { + is CurrenciesStatusesOperations.Error.DataError -> TokenListError.DataError(this.cause) + is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, + is CurrenciesStatusesOperations.Error.EmptyQuotes, + is CurrenciesStatusesOperations.Error.EmptyCurrencies, + is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, + -> 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/CryptoCurrency.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt new file mode 100644 index 0000000000..5143f90497 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -0,0 +1,89 @@ +package com.tangem.domain.tokens.model + +import com.tangem.domain.tokens.models.Network + +/** + * Represents a generic cryptocurrency. + * + * @property id Unique identifier for the cryptocurrency. + * @property networkId Identifier for the network to which the cryptocurrency belongs. + * @property name Human-readable name of the cryptocurrency. + * @property symbol Symbol of the cryptocurrency. + * @property decimals Number of decimal places used by the cryptocurrency. + * @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found. + * @property derivationPath Optional path used for key derivation. `null` if the wallet does not support the + * [HD Wallet](https://coinsutra.com/hd-wallets-deterministic-wallet/) feature. + */ +sealed class CryptoCurrency { + + abstract val id: ID + abstract val networkId: Network.ID + abstract val name: String + abstract val symbol: String + abstract val decimals: Int + abstract val iconUrl: String? + abstract val derivationPath: String? + + /** + * Represents a native coin in the blockchain network. + */ + data class Coin( + override val id: ID, + override val networkId: Network.ID, + override val name: String, + override val symbol: String, + override val decimals: Int, + override val iconUrl: String?, + override val derivationPath: String?, + ) : CryptoCurrency() { + + init { + checkProperties() + } + } + + /** + * Represents a token in the blockchain network, typically a non-native asset. + * + * @property contractAddress Address of the contract managing the token. + * @property isCustom Indicates whether the token is a custom user-added token or not. + */ + data class Token( + override val id: ID, + override val networkId: Network.ID, + override val name: String, + override val symbol: String, + override val decimals: Int, + override val iconUrl: String?, + override val derivationPath: String?, + val contractAddress: String, + val isCustom: Boolean, + ) : CryptoCurrency() { + + init { + checkProperties() + require(contractAddress.isNotBlank()) { "Token contract address must not be blank" } + } + } + + /** + * Value class for uniquely identifying a cryptocurrency. + * + * @property value The unique identifier value. + */ + @JvmInline + value class ID(val value: String) { + + init { + require(value.isNotBlank()) { "Crypto currency ID must not be blank" } + } + } + + protected fun checkProperties() { + require(name.isNotBlank()) { "Crypto currency name must not be blank" } + require(symbol.isNotBlank()) { "Crypto currency symbol must not be blank" } + require(iconUrl?.isNotBlank() ?: true) { "Crypto currency icon URL must not be blank" } + require(decimals > 0) { "Crypto currency decimal must not be less then zero, but it is: $decimals" } + require(derivationPath?.isNotBlank() ?: true) { "Crypto currency derivation path must not be blank" } + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt new file mode 100644 index 0000000000..a735f3ed06 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -0,0 +1,88 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +/** + * Represents the status of a cryptocurrency asset within a network. + * + * This class encapsulates the details of a specific cryptocurrency, either a coin or token, + * along with its current status within the blockchain network. The status can include various states + * like Loading, Unreachable, Loaded, etc. + * + * @property currency The details of the cryptocurrency asset, including its type, name, symbol, and other properties. + * @property value The current status of the cryptocurrency, reflecting its state within the network. + */ +data class CryptoCurrencyStatus( + val currency: CryptoCurrency, + val value: Status, +) { + + /** + * Represents the various states a token can have, encapsulating different information based on the state. + */ + sealed class Status { + + /** The amount of the token. */ + open val amount: BigDecimal? = null + + /** The fiat equivalent of the token's amount. */ + open val fiatAmount: BigDecimal? = null + + /** The exchange rate used for converting the token amount to fiat. */ + open val fiatRate: BigDecimal? = null + + /** The change in price of the token. */ + open val priceChange: BigDecimal? = null + + /** Indicates if there are any transactions in progress related to the token. */ + open val hasTransactionsInProgress: Boolean = false + } + + /** Represents the Loading state of a token, typically while fetching its details. */ + object Loading : Status() + + /** Represents a state where the token is not reachable. */ + object Unreachable : Status() + + /** Represents a state where the token's derivation is missed. */ + object MissedDerivation : Status() + + /** Represents a state where there is no account associated with the token. */ + object NoAccount : Status() + + /** + * Represents a Loaded state of a token with complete information. + * + * @property amount The amount of the token. + * @property fiatAmount The fiat equivalent of the token's amount. + * @property fiatRate The exchange rate used for converting the token amount to fiat. + * @property priceChange The change in price of the token. + * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token + * network. + */ + data class Loaded( + override val amount: BigDecimal, + override val fiatAmount: BigDecimal, + override val fiatRate: BigDecimal, + override val priceChange: BigDecimal, + override val hasTransactionsInProgress: Boolean, + ) : Status() + + /** + * Represents a Custom state of a token, typically used for user-defined tokens. + * + * @property amount The amount of the token. + * @property fiatAmount The fiat equivalent of the token's amount (optional). + * @property fiatRate The exchange rate used for converting the token amount to fiat (optional). + * @property priceChange The change in price of the token (optional). + * @property hasTransactionsInProgress Indicates if there are any transactions in progress related to the token + * network. + */ + data class Custom( + override val amount: BigDecimal, + override val fiatAmount: BigDecimal?, + override val fiatRate: 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/model/NetworkGroup.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt new file mode 100644 index 0000000000..89264ac332 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkGroup.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.tokens.model + +import com.tangem.domain.tokens.models.Network + +/** + * Represents a group of cryptocurrencies associated with a specific network. + * + * This class encapsulates a collection of cryptocurrency statuses, all of which are part of the same blockchain network. + * + * @property network The blockchain network associated with the group. + * @property currencies A set of cryptocurrency statuses that belong to the network. + */ +data class NetworkGroup( + val network: Network, + val currencies: 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..17bbaa392f --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -0,0 +1,51 @@ +package com.tangem.domain.tokens.model + +import com.tangem.domain.tokens.models.Network +import java.math.BigDecimal + +/** + * Represents the status of a specific blockchain network. + * + * @property networkId The unique identifier of the network for which the status is provided. + * @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network. + */ +data class NetworkStatus( + val networkId: Network.ID, + val value: Status, +) { + + /** + * Represents the various possible statuses of a network. + * + * This sealed class includes different states like unreachable, missed derivation, verified, and no account. + */ + sealed class Status + + /** + * Represents the state where the network is unreachable. + */ + object Unreachable : Status() + + /** + * Represents the state where a derivation has been missed. + */ + object MissedDerivation : Status() + + /** + * Represents the verified state of the network, including the amounts associated with different cryptocurrencies and whether there are transactions in progress. + * + * @property amounts A map containing the amounts associated with different cryptocurrencies within the network. + * @property hasTransactionsInProgress A boolean indicating whether there are transactions in progress within the network. + */ + data class Verified( + val amounts: Map, + val hasTransactionsInProgress: Boolean, + ) : Status() + + /** + * Represents the state where there is no account, and an amount is required to create one. + * + * @property amountToCreateAccount The amount required to create an account within the network. + */ + 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..7247a75ed2 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/Quote.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +/** + * Represents a financial quote for a specific cryptocurrency, including its fiat exchange rate and price change. + * + * @property currencyId The unique identifier of the cryptocurrency for which the quote is provided. + * @property fiatRate The current fiat exchange rate for the cryptocurrency. + * @property priceChange The price change for the cryptocurrency. + */ +data class Quote( + val currencyId: CryptoCurrency.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/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt new file mode 100644 index 0000000000..55327b82da --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -0,0 +1,79 @@ +package com.tangem.domain.tokens.model + +import java.math.BigDecimal + +/** + * Represents a list of cryptocurrency tokens, which can be grouped by network or ungrouped. + * + * The tokens can be represented in two forms: either grouped by the network or as an ungrouped collection. + * Additional details like the total fiat balance and the sorting type can be associated with the list. + * + * @property totalFiatBalance The total fiat balance across all tokens, which could be in a loading state, failed, or loaded with a specific amount. + * @property sortedBy The criteria used for sorting the tokens. + */ +sealed class TokenList { + open val totalFiatBalance: FiatBalance = FiatBalance.Loading + open val sortedBy: SortType = SortType.NONE + + /** + * Represents tokens that are grouped by their network. + * + * @property groups A set of network groups containing tokens. + * @property totalFiatBalance The total fiat balance across all groups. + * @property sortedBy The criteria used for sorting the tokens within the groups. + */ + data class GroupedByNetwork( + val groups: Set, + override val totalFiatBalance: FiatBalance, + override val sortedBy: SortType, + ) : TokenList() + + /** + * Represents tokens that are not grouped by any specific criteria. + * + * @property currencies A set of cryptocurrency statuses. + * @property totalFiatBalance The total fiat balance across all currencies. + * @property sortedBy The criteria used for sorting the currencies. + */ + data class Ungrouped( + val currencies: Set, + override val totalFiatBalance: FiatBalance, + override val sortedBy: SortType, + ) : TokenList() + + /** Represents a state where the token list is not initialized. */ + object NotInitialized : TokenList() + + /** Defines the possible sorting criteria for the tokens. */ + enum class SortType { + NONE, BALANCE, + } + + /** + * Represents the possible states of the fiat balance, including loading, failure, or a loaded amount. + */ + sealed class FiatBalance { + /** + * Represents the loading state of the fiat balance. + * This state indicates that the fiat balance is currently being retrieved or calculated. + */ + object Loading : FiatBalance() + + /** + * Represents the failure state of the fiat balance. + * This state indicates that an attempt to retrieve or calculate the fiat balance has failed. + */ + object Failed : FiatBalance() + + /** + * Represents the successfully loaded state of the fiat balance. + * + * @property amount The loaded fiat balance amount. + * @property isAllAmountsSummarized Indicates whether the amount includes a summary of all underlying amounts. + */ + 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/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt new file mode 100644 index 0000000000..6ce5fc8b60 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -0,0 +1,171 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.* +import arrow.core.raise.Raise +import arrow.core.raise.catch +import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.* +import com.tangem.domain.tokens.models.Network +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 CurrenciesStatusesOperations( + 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 getMultiCurrencyWalletStatusesFlow(): Flow> { + return getMultiCurrencyWalletCurrencies().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) + } + } + } + } + + suspend fun getPrimaryCurrencyStatusFlow(): Flow { + val token = getPrimaryCurrency() + + val quoteFlow = getQuotes(nonEmptySetOf(token.id)) + .map { quotes -> + quotes.singleOrNull { it.currencyId == token.id } + } + + val statusFlow = getNetworksStatues(groupTokens(nonEmptySetOf(token))) + .map { statuses -> + statuses.singleOrNull { it.networkId == token.networkId } + } + + return combine(quoteFlow, statusFlow) { quote, networkStatus -> + createStatus(token, quote, networkStatus) + } + } + + private suspend fun createTokensStatuses( + tokens: Set, + quotes: Set, + networkStatuses: Set, + ): Set = withContext(dispatchers.default) { + tokens.mapTo(hashSetOf()) { token -> + val quote = quotes.firstOrNull { it.currencyId == token.id } + val networkStatus = networkStatuses.firstOrNull { it.networkId == token.networkId } + + createStatus(token, quote, networkStatus) + } + } + + private suspend fun createStatus( + token: CryptoCurrency, + quote: Quote?, + networkStatus: NetworkStatus?, + ): CryptoCurrencyStatus { + val currencyStatusOperations = CurrencyStatusOperations( + currency = token, + quote = quote, + networkStatus = networkStatus, + dispatchers = dispatchers, + raise = this, + transformError = { Error.UnableToCreateCurrencyStatus }, + ) + + return currencyStatusOperations.createTokenStatus() + } + + private fun getMultiCurrencyWalletCurrencies(): Flow> { + return tokensRepository.getMultiCurrencyWalletCurrencies(userWalletId, refresh) + .catch { raise(Error.DataError(it)) } + .onEmpty { raise(Error.EmptyCurrencies) } + .flowOn(dispatchers.io) + } + + private suspend fun getPrimaryCurrency(): CryptoCurrency { + return withContext(dispatchers.io) { + catch( + block = { tokensRepository.getPrimaryCurrency(userWalletId) }, + catch = { raise(Error.DataError(it)) }, + ) + } + } + + 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> { + return withContext(dispatchers.default) { + tokens + .groupBy { it.networkId } + .mapValues { (_, tokens) -> + // Can not be empty + tokens.toNonEmptySetOrNull()!! + .map { it.id } + .toNonEmptySet() + } + } + } + + sealed class Error { + + object EmptyCurrencies : Error() + + object EmptyQuotes : Error() + + object EmptyNetworksStatuses : Error() + + object UnableToCreateCurrencyStatus : 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/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt new file mode 100644 index 0000000000..50f348f3d3 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -0,0 +1,75 @@ +package com.tangem.domain.tokens.operations + +import arrow.core.raise.Raise +import arrow.core.raise.ensureNotNull +import com.tangem.domain.core.raise.DelegatedRaise +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class CurrencyStatusOperations( + private val currency: CryptoCurrency, + private val quote: Quote?, + private val networkStatus: NetworkStatus?, + private val dispatchers: CoroutineDispatcherProvider, + raise: Raise, + transformError: (Error) -> OtherError, +) : DelegatedRaise(raise, transformError) { + + suspend fun createTokenStatus(): CryptoCurrencyStatus = withContext(dispatchers.default) { + CryptoCurrencyStatus(currency, createStatus()) + } + + private fun createStatus(): CryptoCurrencyStatus.Status { + return when (val status = networkStatus?.value) { + null -> CryptoCurrencyStatus.Loading + is NetworkStatus.MissedDerivation -> CryptoCurrencyStatus.MissedDerivation + is NetworkStatus.Unreachable -> CryptoCurrencyStatus.Unreachable + is NetworkStatus.NoAccount -> CryptoCurrencyStatus.NoAccount + is NetworkStatus.Verified -> createStatus(status) + } + } + + private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Status { + val amount = ensureNotNull(status.amounts[currency.id]) { + Error.UnableToFindAmount(currency.id) + } + + return when { + currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( + amount = amount, + fiatAmount = calculateFiatAmountOrNull(amount, quote?.fiatRate), + fiatRate = quote?.fiatRate, + priceChange = quote?.priceChange, + hasTransactionsInProgress = status.hasTransactionsInProgress, + ) + quote == null -> CryptoCurrencyStatus.Loading + else -> CryptoCurrencyStatus.Loaded( + amount = amount, + fiatAmount = calculateFiatAmount(amount, quote.fiatRate), + fiatRate = quote.fiatRate, + priceChange = quote.priceChange, + hasTransactionsInProgress = status.hasTransactionsInProgress, + ) + } + } + + 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 + } + + sealed class Error { + + data class UnableToFindAmount(val currencyId: CryptoCurrency.ID) : Error() + } +} \ 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..dd3cb367d5 --- /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.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class TokenListFiatBalanceOperations( + private val currencies: 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 currencies) { + when (val status = token.value) { + is CryptoCurrencyStatus.Loading -> { + fiatBalance = TokenList.FiatBalance.Loading + break + } + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + -> { + fiatBalance = TokenList.FiatBalance.Failed + break + } + is CryptoCurrencyStatus.NoAccount -> { + fiatBalance = recalculateBalanceForNoAccountStatus(fiatBalance) + } + is CryptoCurrencyStatus.Loaded -> { + fiatBalance = recalculateBalance(status, fiatBalance) + } + is CryptoCurrencyStatus.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: CryptoCurrencyStatus.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: CryptoCurrencyStatus.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..37b96a781c --- /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.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.Network +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 CryptoCurrencyStatus.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( + currencies = 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.currency.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, + currencies = 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, + currencies = 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..06c3041651 --- /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.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.Network +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class TokenListSortingOperations( + private val currencies: 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( + currencies = when (tokenList) { + is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies }.toSet() + is TokenList.Ungrouped -> tokenList.currencies + 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(currencies.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(currencies.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 = currencies + .groupBy { it.currency.networkId } + .map { (networkId, tokens) -> + val network = ensureNotNull(networks.firstOrNull { it.id == networkId }) { + Error.NetworkNotFound(networkId) + } + + NetworkGroup( + network = network, + currencies = 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.currencies as? NonEmptySet + ?: error("Tokens can not be empty here") + group.copy(currencies = 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.currencies.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/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt new file mode 100644 index 0000000000..5acf293b11 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Repository for everything related to the blockchain networks + * */ +interface NetworksRepository { + + /** + * Retrieves the details of the specified blockchain networks, identified by their unique IDs. + * + * @param networksIds The unique identifiers of the networks to be retrieved. + * @return A set of [Network] objects corresponding to the specified network IDs. + */ + fun getNetworks(networksIds: Set): Set + + /** + * Retrieves the statuses of specified blockchain networks for a specific user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param networks A map of network IDs to sets of cryptocurrency IDs, representing the networks for which statuses are to be retrieved. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A [Flow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. + */ + 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..1c783ce342 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +import kotlinx.coroutines.flow.Flow + +/** + * Repository for everything related to the quotes of tokens + * */ +interface QuotesRepository { + + /** + * Retrieves the quotes for a set of specified cryptocurrencies, identified by their unique IDs. + * + * @param tokensIds The unique identifiers of the cryptocurrencies for which quotes are to be retrieved. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A [Flow] emitting a set of quotes corresponding to the specified cryptocurrencies. + */ + 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..865c5fc4da --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokensRepository.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +/** + * Repository for everything related to the tokens of user wallet + * */ +interface TokensRepository { + + /** + * Saves the given set of cryptocurrencies, along with the preferences for grouping and sorting, for a specific + * multi-currency user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param currencies The set of cryptocurrencies to be saved. + * @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network. + * @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance. + */ + suspend fun saveTokens( + userWalletId: UserWalletId, + currencies: Set, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ) + + /** + * Retrieves the primary cryptocurrency for a specific single-currency user wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @return The primary cryptocurrency associated with the user wallet. + */ + suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency + + /** + * Retrieves the set of cryptocurrencies within a multi-currency wallet. + * + * @param userWalletId The unique identifier of the user wallet. + * @param refresh A boolean flag indicating whether the data should be refreshed. + * @return A [Flow] emitting the set of cryptocurrencies associated with the user wallet. + */ + fun getMultiCurrencyWalletCurrencies(userWalletId: UserWalletId, refresh: Boolean): Flow> + + /** + * Determines whether the tokens within a specific multi-currency user wallet are grouped. + * + * @param userWalletId The unique identifier of the user wallet. + * @return A [Flow] emitting a boolean value indicating whether the tokens are grouped. + */ + fun isTokensGrouped(userWalletId: UserWalletId): Flow + + /** + * Determines whether the tokens within a specific multi-currency user wallet are sorted by balance. + * + * @param userWalletId The unique identifier of the user wallet. + * @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance. + */ + fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt new file mode 100644 index 0000000000..0bdf923b6b --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt @@ -0,0 +1,198 @@ +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.TokenListSortingError +import com.tangem.domain.tokens.mock.MockTokens +import com.tangem.domain.tokens.model.CryptoCurrency +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.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import kotlin.random.Random + +internal class ApplyTokenListSortingUseCaseTest { + + private val userWalletId = UserWalletId(value = null) + + @Test + fun `when tokens are empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase( + userWalletId = userWalletId, + sortedTokensIds = emptySet(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when tokens saving failed then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val repository = getTokensRepository( + sortTokensResult = DataError.NetworkError.NoInternetConnection.left(), + ) + val useCase = getUseCase(repository) + + // When + val result = useCase( + userWalletId = userWalletId, + sortedTokensIds = MockTokens.tokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when apply sorting for sorted and grouped list then correct args should be used`() = runTest { + // Given + val expectedTokens = getSortedTokens() + val expectedIsGrouped = true + val expectedIsSorted = true + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + useCase( + userWalletId = userWalletId, + sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = expectedIsGrouped, + isSortedByBalance = expectedIsSorted, + ) + + // Then + assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) + assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) + assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) + } + + @Test + fun `when apply sorting for unsorted and grouped list then correct args should be used`() = runTest { + // Given + val expectedTokens = getSortedTokens() + val expectedIsGrouped = true + val expectedIsSorted = false + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + useCase( + userWalletId = userWalletId, + sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = expectedIsGrouped, + isSortedByBalance = expectedIsSorted, + ) + + // Then + assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) + assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) + assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) + } + + @Test + fun `when apply sorting for sorted and ungrouped list then correct args should be used`() = runTest { + // Given + val expectedTokens = getSortedTokens() + val expectedIsGrouped = false + val expectedIsSorted = true + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + useCase( + userWalletId = userWalletId, + sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = expectedIsGrouped, + isSortedByBalance = expectedIsSorted, + ) + + // Then + assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) + assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) + assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) + } + + @Test + fun `when apply sorting for unsorted and ungrouped list then correct args should be used`() = runTest { + // Given + val expectedTokens = getSortedTokens() + val expectedIsGrouped = false + val expectedIsSorted = false + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + useCase( + userWalletId = userWalletId, + sortedTokensIds = expectedTokens.map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = expectedIsGrouped, + isSortedByBalance = expectedIsSorted, + ) + + // Then + assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) + assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) + assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) + } + + @Test + fun `when sorted tokens IDs do not contain all tokens IDs then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.UnableToSortTokenList.left() + + val repository = getTokensRepository() + val useCase = getUseCase(repository) + + // When + val result = useCase( + userWalletId = userWalletId, + sortedTokensIds = getSortedTokens().drop(n = 3).map { it.networkId to it.id }.toSet(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + + // Then + assertEquals(expectedResult, result) + } + + private fun getSortedTokens() = MockTokens.tokens + .sortedBy { Random.nextInt(0, MockTokens.tokens.size) } + .toSet() + + private fun getUseCase(tokensRepository: MockTokensRepository = getTokensRepository()) = + ApplyTokenListSortingUseCase( + tokensRepository = tokensRepository, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private fun getTokensRepository( + sortTokensResult: Either = Unit.right(), + tokens: Flow>> = flowOf(MockTokens.tokens.right()), + ): MockTokensRepository { + return MockTokensRepository(sortTokensResult, MockTokens.token1.right(), tokens, emptyFlow(), emptyFlow()) + } +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt new file mode 100644 index 0000000000..95026c6926 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyUseCaseTest.kt @@ -0,0 +1,167 @@ +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.TokenError +import com.tangem.domain.tokens.mock.MockNetworks +import com.tangem.domain.tokens.mock.MockQuotes +import com.tangem.domain.tokens.mock.MockTokens +import com.tangem.domain.tokens.mock.MockTokensStates +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +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.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class GetPrimaryCurrencyUseCaseTest { + + private val dispatchers = TestingCoroutineDispatcherProvider() + private val userWalletId = UserWalletId(value = null) + + @Test + fun `when all data received then token should be received`() = runTest { + // Given + val expectedResult = MockTokensStates.loadedTokensStates.first().right() + + val useCase = getUseCase() + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when token getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenError.DataError(DataError.NetworkError.NoInternetConnection).left() + + val useCase = getUseCase(token = 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 = TokenError.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 statuses getting failed then error should be received`() = runTest { + // Given + val expectedResult = TokenError.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 networks statuses flow is empty then error should be received`() = runTest { + val expectedResult = TokenError.UnableToCreateToken.left() + + val useCase = getUseCase(statuses = flowOf()) + + // 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 = TokenError.UnableToCreateToken.left() + + val useCase = getUseCase(quotes = flowOf()) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes are empty and statuses are verified then loading token should be received`() = runTest { + val expectedResult = MockTokensStates.tokenState1 + .copy(value = CryptoCurrencyStatus.Loading) + .right() + + val useCase = getUseCase( + statuses = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + quotes = flowOf(emptySet().right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when quotes are loaded and statuses are empty then loading token should be received`() = runTest { + val expectedResult = MockTokensStates.tokenState1 + .copy(value = CryptoCurrencyStatus.Loading) + .right() + + val useCase = getUseCase( + statuses = flowOf(emptySet().right()), + quotes = flowOf(MockQuotes.quotes.right()), + ) + + // When + val result = useCase(userWalletId).first() + + // Then + assertEquals(expectedResult, result) + } + + private fun getUseCase( + token: Either = MockTokens.token1.right(), + quotes: Flow>> = flowOf(MockQuotes.quotes.right()), + statuses: Flow>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()), + ) = GetPrimaryCurrencyUseCase( + dispatchers = dispatchers, + tokensRepository = MockTokensRepository( + sortTokensResult = Unit.right(), + token = token, + tokens = flowOf(), + isGrouped = flowOf(), + isSortedByBalance = flowOf(), + ), + quotesRepository = MockQuotesRepository(quotes), + networksRepository = MockNetworksRepository(MockNetworks.networks.right(), statuses), + ) +} \ 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..204bf4af9c --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -0,0 +1,339 @@ +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.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.model.Quote +import com.tangem.domain.tokens.models.Network +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.failedUngroupedTokenList.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.failedUngroupedTokenList.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.failedGroupedTokenList.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.failedUngroupedTokenList).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( + sortTokensResult = Unit.right(), + token = MockTokens.token1.right(), + tokens = tokens, + isGrouped = isGrouped, + isSortedByBalance = 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/ToggleTokenListGroupingTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt new file mode 100644 index 0000000000..8e34224394 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt @@ -0,0 +1,180 @@ +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.TokenListSortingError +import com.tangem.domain.tokens.mock.MockNetworks +import com.tangem.domain.tokens.mock.MockTokenLists +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.tokens.repository.MockNetworksRepository +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class ToggleTokenListGroupingTest { + + @Test + fun `when grouped list is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.emptyGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when ungrouped list is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.emptyUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and loading then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.loadingGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and loading then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.loadingUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is not initialized then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.notInitializedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and sorted then sorted grouped list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.sortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and unsorted then unsorted grouped list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.unsortedGroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and sorted then sorted ungrouped list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.sortedGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and unsorted then unsorted ungrouped list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.unsortedUngroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped but networks is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.UnableToSortTokenList.left() + + val useCase = getUseCase(networks = emptySet().right()) + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped but networks getting failed then error should be received`() = runTest { + // Given + val error = DataError.NetworkError.NoInternetConnection + val expectedResult = TokenListSortingError.DataError(error).left() + + val useCase = getUseCase(networks = error.left()) + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + private fun getUseCase(networks: Either> = MockNetworks.networks.right()) = + ToggleTokenListGroupingUseCase( + networksRepository = MockNetworksRepository(networks, statuses = flowOf()), + dispatchers = TestingCoroutineDispatcherProvider(), + ) +} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt new file mode 100644 index 0000000000..f8070694f7 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt @@ -0,0 +1,143 @@ +package com.tangem.domain.tokens + +import arrow.core.left +import arrow.core.right +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.domain.tokens.mock.MockTokenLists +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.test.runTest +import org.junit.Test + +internal class ToggleTokenListSortingUseCaseTest { + + @Test + fun `when grouped list is not initialized then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.notInitializedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when grouped list is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.emptyGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when ungrouped list is empty then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsEmpty.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.emptyUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and loading then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.loadingGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and loading then error should be received`() = runTest { + // Given + val expectedResult = TokenListSortingError.TokenListIsLoading.left() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.loadingUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and unsorted then grouped and sorted list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and unsorted then ungrouped and sorted list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is grouped and sorted then grouped and unsorted list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedGroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedGroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + @Test + fun `when list is ungrouped and sorted then ungrouped and unsorted list should be received`() = runTest { + // Given + val expectedResult = MockTokenLists.sortedUngroupedTokenList.right() + + val useCase = getUseCase() + + // When + val result = useCase(MockTokenLists.unsortedUngroupedTokenList) + + // Then + assertEquals(expectedResult, result) + } + + private fun getUseCase() = ToggleTokenListSortingUseCase( + dispatchers = TestingCoroutineDispatcherProvider(), + ) +} \ 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..4bf559f4c7 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -0,0 +1,93 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.NonEmptySet +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network +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, + ), + hasTransactionsInProgress = false, + ), + ) + + 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, + ), + hasTransactionsInProgress = false, + ), + ) + + 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, + ), + hasTransactionsInProgress = false, + ), + ) + + 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/MockNetworksGroups.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt new file mode 100644 index 0000000000..ebda181faf --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworksGroups.kt @@ -0,0 +1,52 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.model.NetworkGroup + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockNetworksGroups { + + val networkGroup1 = NetworkGroup( + network = MockNetworks.network1, + currencies = MockTokensStates.failedTokenStates + .filter { it.currency.networkId == MockNetworks.network1.id } + .toNonEmptySetOrNull()!!, + ) + + val networkGroup2 = NetworkGroup( + network = MockNetworks.network2, + currencies = MockTokensStates.failedTokenStates + .filter { it.currency.networkId == MockNetworks.network2.id } + .toNonEmptySetOrNull()!!, + ) + + val networkGroup3 = NetworkGroup( + network = MockNetworks.network3, + currencies = MockTokensStates.failedTokenStates + .filter { it.currency.networkId == MockNetworks.network3.id } + .toNonEmptySetOrNull()!!, + ) + + val failedNetworksGroups = nonEmptySetOf(networkGroup1, networkGroup2, networkGroup3) + + val loadedNetworksGroups = failedNetworksGroups.map { group -> + group.copy( + currencies = MockTokensStates.loadedTokensStates + .filter { it.currency.networkId == group.network.id } + .toNonEmptySetOrNull()!!, + ) + }.toNonEmptySet() + + val sortedNetworksGroups = loadedNetworksGroups.map { group -> + group.copy( + currencies = group.currencies + .sortedByDescending { it.value.fiatAmount } + .toNonEmptySetOrNull()!!, + ) + } + .sortedByDescending { group -> + group.currencies.sumOf { it.value.fiatAmount!! } + } + .toNonEmptySetOrNull()!! +} \ 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..d54da733fd --- /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( + currencyId = MockTokens.token1.id, + fiatRate = BigDecimal("1.23"), + priceChange = BigDecimal("0.01"), + ) + + val quote2 = Quote( + currencyId = MockTokens.token2.id, + fiatRate = BigDecimal("2.34"), + priceChange = BigDecimal("-0.02"), + ) + + val quote3 = Quote( + currencyId = MockTokens.token3.id, + fiatRate = BigDecimal("3.45"), + priceChange = BigDecimal("0.03"), + ) + + val quote4 = Quote( + currencyId = MockTokens.token4.id, + fiatRate = BigDecimal("4.56"), + priceChange = BigDecimal("-0.04"), + ) + + val quote5 = Quote( + currencyId = MockTokens.token5.id, + fiatRate = BigDecimal("5.67"), + priceChange = BigDecimal("0.05"), + ) + + val quote6 = Quote( + currencyId = MockTokens.token6.id, + fiatRate = BigDecimal("6.78"), + priceChange = BigDecimal("-0.06"), + ) + + val quote7 = Quote( + currencyId = MockTokens.token7.id, + fiatRate = BigDecimal("7.89"), + priceChange = BigDecimal("0.07"), + ) + + val quote8 = Quote( + currencyId = MockTokens.token8.id, + fiatRate = BigDecimal("8.90"), + priceChange = BigDecimal("-0.08"), + ) + + val quote9 = Quote( + currencyId = MockTokens.token9.id, + fiatRate = BigDecimal("9.01"), + priceChange = BigDecimal("0.09"), + ) + + val quote10 = Quote( + currencyId = 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..403dcb5046 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -0,0 +1,115 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.NonEmptySet +import arrow.core.toNonEmptySetOrNull +import com.tangem.domain.tokens.mock.MockNetworksGroups.failedNetworksGroups +import com.tangem.domain.tokens.mock.MockNetworksGroups.loadedNetworksGroups +import com.tangem.domain.tokens.mock.MockNetworksGroups.sortedNetworksGroups +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import java.math.BigDecimal + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockTokenLists { + + const val isGrouped = false + const val isSortedByBalance = false + + val notInitializedTokenList = TokenList.NotInitialized + + val emptyGroupedTokenList = TokenList.GroupedByNetwork( + groups = emptySet(), + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val emptyUngroupedTokenList = TokenList.Ungrouped( + currencies = emptySet(), + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val failedGroupedTokenList = TokenList.GroupedByNetwork( + groups = failedNetworksGroups, + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val failedUngroupedTokenList = TokenList.Ungrouped( + currencies = MockTokensStates.failedTokenStates, + totalFiatBalance = TokenList.FiatBalance.Failed, + sortedBy = TokenList.SortType.NONE, + ) + + val loadingUngroupedTokenList = with(failedUngroupedTokenList) { + copy( + currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptySetOrNull()!!, + totalFiatBalance = TokenList.FiatBalance.Loading, + ) + } + + val loadingGroupedTokenList = with(failedGroupedTokenList) { + copy( + totalFiatBalance = TokenList.FiatBalance.Loading, + groups = groups.map { group -> + group.copy( + currencies = group.currencies + .map { it.copy(value = CryptoCurrencyStatus.Loading) } + .toNonEmptySetOrNull()!!, + ) + }.toNonEmptySetOrNull()!!, + ) + } + + val unsortedUngroupedTokenList: TokenList.Ungrouped + get() { + val tokens = MockTokensStates.loadedTokensStates + + return failedUngroupedTokenList.copy( + currencies = tokens, + sortedBy = TokenList.SortType.NONE, + totalFiatBalance = TokenList.FiatBalance.Loaded( + amount = tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, + isAllAmountsSummarized = true, + ), + ) + } + + val unsortedGroupedTokenList: TokenList.GroupedByNetwork + get() { + val groups = loadedNetworksGroups + + return failedGroupedTokenList.copy( + groups = groups, + sortedBy = TokenList.SortType.NONE, + totalFiatBalance = TokenList.FiatBalance.Loaded( + amount = groups + .flatMap { it.currencies as NonEmptySet } + .sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, + isAllAmountsSummarized = true, + ), + ) + } + + val sortedUngroupedTokenList: TokenList.Ungrouped + get() { + val tokens = MockTokensStates.loadedTokensStates + .sortedByDescending { it.value.fiatAmount } + .toNonEmptySetOrNull()!! + + return unsortedUngroupedTokenList.copy( + currencies = tokens, + sortedBy = TokenList.SortType.BALANCE, + ) + } + + val sortedGroupedTokenList: TokenList.GroupedByNetwork + get() { + val groups = sortedNetworksGroups + + return unsortedGroupedTokenList.copy( + groups = groups, + sortedBy = TokenList.SortType.BALANCE, + ) + } +} \ 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..b0af037c5e --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokens.kt @@ -0,0 +1,123 @@ +package com.tangem.domain.tokens.mock + +import com.tangem.domain.tokens.model.CryptoCurrency + +internal object MockTokens { + + val token1 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token1"), + networkId = MockNetworks.network1.id, + name = "Token 1", + symbol = "T1", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token2 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token2"), + networkId = MockNetworks.network1.id, + name = "Token 2", + symbol = "T2", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token3 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token3"), + networkId = MockNetworks.network1.id, + name = "Token 3", + symbol = "T3", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token4 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token4"), + networkId = MockNetworks.network2.id, + name = "Token 4", + symbol = "T4", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token5 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token5"), + networkId = MockNetworks.network2.id, + name = "Token 5", + symbol = "T5", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token6 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token6"), + networkId = MockNetworks.network2.id, + name = "Token 6", + symbol = "T6", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token7 + get() = CryptoCurrency.Coin( + id = CryptoCurrency.ID("token7"), + networkId = MockNetworks.network3.id, + name = "Token 7", + symbol = "T7", + decimals = 8, + iconUrl = null, + derivationPath = null, + ) + val token8 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token8"), + networkId = MockNetworks.network3.id, + name = "Token 8", + symbol = "T8", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token9 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token9"), + networkId = MockNetworks.network3.id, + name = "Token 9", + symbol = "T9", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + val token10 + get() = CryptoCurrency.Token( + id = CryptoCurrency.ID("token10"), + networkId = MockNetworks.network3.id, + name = "Token 10", + symbol = "T10", + isCustom = false, + decimals = 8, + iconUrl = null, + contractAddress = "address", + derivationPath = null, + ) + + val tokens = setOf(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..4df0a5d525 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -0,0 +1,90 @@ +package com.tangem.domain.tokens.mock + +import arrow.core.nonEmptySetOf +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkStatus + +@Suppress("MemberVisibilityCanBePrivate") +internal object MockTokensStates { + + val tokenState1 = CryptoCurrencyStatus( + currency = MockTokens.token1, + value = CryptoCurrencyStatus.Unreachable, + ) + + val tokenState2 = CryptoCurrencyStatus( + currency = MockTokens.token2, + value = CryptoCurrencyStatus.Unreachable, + ) + + val tokenState3 = CryptoCurrencyStatus( + currency = MockTokens.token3, + value = CryptoCurrencyStatus.Unreachable, + ) + + val tokenState4 = CryptoCurrencyStatus( + currency = MockTokens.token4, + value = CryptoCurrencyStatus.MissedDerivation, + ) + + val tokenState5 = CryptoCurrencyStatus( + currency = MockTokens.token5, + value = CryptoCurrencyStatus.MissedDerivation, + ) + + val tokenState6 = CryptoCurrencyStatus( + currency = MockTokens.token6, + value = CryptoCurrencyStatus.MissedDerivation, + ) + + val tokenState7 = CryptoCurrencyStatus( + currency = MockTokens.token7, + value = CryptoCurrencyStatus.NoAccount, + ) + + val tokenState8 = CryptoCurrencyStatus( + currency = MockTokens.token8, + value = CryptoCurrencyStatus.NoAccount, + ) + + val tokenState9 = CryptoCurrencyStatus( + currency = MockTokens.token9, + value = CryptoCurrencyStatus.NoAccount, + ) + + val tokenState10 = CryptoCurrencyStatus( + currency = MockTokens.token10, + value = CryptoCurrencyStatus.NoAccount, + ) + + val failedTokenStates = nonEmptySetOf( + tokenState1, + tokenState2, + tokenState3, + tokenState4, + tokenState5, + tokenState6, + tokenState7, + tokenState8, + tokenState9, + tokenState10, + ) + + val loadedTokensStates = failedTokenStates.map { status -> + val networkStatus = MockNetworks.verifiedNetworksStatuses + .first { it.networkId == status.currency.networkId } + val amount = (networkStatus.value as NetworkStatus.Verified).amounts[status.currency.id]!! + val quote = MockQuotes.quotes.first { it.currencyId == status.currency.id } + val fiatAmount = amount * quote.fiatRate + + status.copy( + value = CryptoCurrencyStatus.Loaded( + amount = amount, + fiatAmount = fiatAmount, + fiatRate = quote.fiatRate, + priceChange = quote.priceChange, + hasTransactionsInProgress = false, + ), + ) + }.toNonEmptySet() +} \ 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..ced27621b7 --- /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.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.domain.tokens.models.Network +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..9ec660b113 --- /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.CryptoCurrency +import com.tangem.domain.tokens.model.Quote +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..ad5d4e0851 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockTokensRepository.kt @@ -0,0 +1,59 @@ +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.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class MockTokensRepository( + private val sortTokensResult: Either, + private val token: Either, + private val tokens: Flow>>, + private val isGrouped: Flow>, + private val isSortedByBalance: Flow>, +) : TokensRepository { + + var tokensIdsAfterSortingApply: Set? = null + private set + + var isTokensGroupedAfterSortingApply: Boolean? = null + private set + + var isTokensSortedByBalanceAfterSortingApply: Boolean? = null + private set + + override suspend fun saveTokens( + userWalletId: UserWalletId, + currencies: Set, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ) { + sortTokensResult.onLeft { throw it } + + tokensIdsAfterSortingApply = currencies + isTokensGroupedAfterSortingApply = isGroupedByNetwork + isTokensSortedByBalanceAfterSortingApply = isSortedByBalance + } + + override suspend fun getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { + return token.getOrElse { e -> throw e } + } + + override fun getMultiCurrencyWalletCurrencies( + 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/txhistory/.gitignore b/domain/txhistory/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/txhistory/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/txhistory/build.gradle.kts b/domain/txhistory/build.gradle.kts new file mode 100644 index 0000000000..1237e8f782 --- /dev/null +++ b/domain/txhistory/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.txhistory" +} + +dependencies { + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + implementation(deps.androidx.paging.runtime) + + implementation(projects.core.utils) +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt new file mode 100644 index 0000000000..1538b27e0b --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryListError.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.txhistory.error + +sealed class TxHistoryListError : Throwable() { + data class DataError(override val cause: Throwable) : TxHistoryListError() +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt new file mode 100644 index 0000000000..3f93a3cf12 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/error/TxHistoryStateError.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.txhistory.error + +sealed class TxHistoryStateError : Throwable() { + + object TxHistoryNotImplemented : TxHistoryStateError() + + object EmptyTxHistories : TxHistoryStateError() + + data class DataError(override val cause: Throwable) : TxHistoryStateError() +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt new file mode 100644 index 0000000000..41d5c76d8e --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/model/TxHistoryItem.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.txhistory.model + +import java.math.BigDecimal + +data class TxHistoryItem( + val txHash: String, + val timestamp: Long, + val direction: TransactionDirection, + val status: TxStatus, + val type: TransactionType, + val amount: BigDecimal, +) { + sealed interface TransactionDirection { + data class Incoming(val from: String) : TransactionDirection + data class Outgoing(val to: String) : TransactionDirection + } + + sealed interface TransactionType { + object Transfer : TransactionType + } + + enum class TxStatus { Confirmed, Unconfirmed } +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt new file mode 100644 index 0000000000..1ebb8ae1dd --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/repository/TxHistoryRepository.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.txhistory.repository + +import androidx.paging.PagingData +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.domain.txhistory.model.TxHistoryItem +import kotlinx.coroutines.flow.Flow + +interface TxHistoryRepository { + + @Throws(TxHistoryStateError::class) + suspend fun getTxHistoryItemsCount(networkId: String, derivationPath: String): Int + + @Throws(TxHistoryListError::class) + fun getTxHistoryItems(networkId: String, pageSize: Int): Flow> +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt new file mode 100644 index 0000000000..1ecdc72dd6 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsCountUseCase.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.txhistory.usecase + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.domain.txhistory.repository.TxHistoryRepository + +class GetTxHistoryItemsCountUseCase(private val repository: TxHistoryRepository) { + + suspend operator fun invoke(networkId: String, derivationPath: String): Either { + return either { + catch( + block = { repository.getTxHistoryItemsCount(networkId, derivationPath) }, + catch = { throwable -> + raise( + when (throwable) { + is TxHistoryStateError.TxHistoryNotImplemented -> throwable + is TxHistoryStateError.EmptyTxHistories -> throwable + else -> TxHistoryStateError.DataError(throwable) + }, + ) + }, + ) + } + } +} \ No newline at end of file diff --git a/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt new file mode 100644 index 0000000000..9f3fc2fbe0 --- /dev/null +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/usecase/GetTxHistoryItemsUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.txhistory.usecase + +import androidx.paging.PagingData +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.domain.txhistory.repository.TxHistoryRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch + +private const val DEFAULT_PAGE_SIZE = 20 + +class GetTxHistoryItemsUseCase(private val repository: TxHistoryRepository) { + + operator fun invoke( + networkId: String, + pageSize: Int = DEFAULT_PAGE_SIZE, + ): Either>> { + return either { + repository + .getTxHistoryItems(networkId = networkId, pageSize = pageSize) + .catch { raise(TxHistoryListError.DataError(it)) } + } + } +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index a9a1449956..3216145e52 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -11,9 +11,10 @@ android { dependencies { // region Domain modules - implementation(project(":domain:legacy")) - implementation(project(":domain:models")) - implementation(project(":domain:wallets:models")) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) // endregion // region Tangem libraries @@ -22,6 +23,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/GetExploreUrlUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt new file mode 100644 index 0000000000..564192c4f8 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExploreUrlUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +class GetExploreUrlUseCase(private val walletsManagersFacade: WalletManagersFacade) { + + suspend operator fun invoke(userWalletId: UserWalletId, networkId: Network.ID): String { + return walletsManagersFacade.getExploreUrl(userWalletId, networkId) + } +} \ 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 b5c1d46121..5e4a01d002 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..47cef6133f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -0,0 +1,94 @@ +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.core.ui.extensions.TextReference +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 = TextReference.Str(value = "Buy"), + iconResId = R.drawable.ic_plus_24, + onClick = {}, + ), + ActionButtonConfig( + text = TextReference.Str(value = "Send"), + iconResId = R.drawable.ic_arrow_up_24, + onClick = {}, + ), + ActionButtonConfig( + text = TextReference.Str(value = "Receive"), + iconResId = R.drawable.ic_arrow_down_24, + onClick = {}, + ), + ActionButtonConfig( + text = TextReference.Str(value = "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..265fb7ce41 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -12,21 +12,25 @@ dependencies { implementation(deps.material) /** Compose */ + implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.coil) implementation(deps.compose.constraintLayout) - implementation(deps.compose.material) implementation(deps.compose.foundation) + implementation(deps.compose.material) implementation(deps.compose.material3) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) + implementation(deps.compose.paging) + implementation(deps.compose.reorderable) + implementation(deps.compose.shimmer) implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) - implementation(deps.compose.shimmer) - implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.reorderable) /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) /** DI */ @@ -34,17 +38,24 @@ dependencies { kapt(deps.hilt.kapt) /** Core modules */ - implementation(project(":core:featuretoggles")) - implementation(project(":core:navigation")) - implementation(project(":core:ui")) - - /** Feature Apis */ - implementation(project(":features:wallet:api")) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + implementation(projects.core.ui) + implementation(projects.core.utils) /** Domain modules */ - implementation(project(":common")) - implementation(project(":domain:legacy")) - implementation(project(":domain:models")) - implementation(project(":domain:wallets")) - implementation(project(":domain:wallets:models")) + implementation(projects.common) + implementation(projects.domain.card) + implementation(projects.domain.demo) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.settings) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + + /** Feature Apis */ + implementation(projects.features.wallet.api) } \ 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..722d10e9cb 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,16 +1,22 @@ package com.tangem.feature.wallet.presentation.common +import androidx.paging.PagingData 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 import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.flowOf import java.util.UUID internal object WalletPreviewData { @@ -18,7 +24,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 +33,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 +41,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 +49,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 +204,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,102 +221,118 @@ 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, walletsListConfig = walletListConfig, - contentItems = persistentListOf( - WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle("Bitcoin"), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_1", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_2", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_3", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_4", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", - ), - ), - WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle("Ethereum"), - WalletContentItemState.MultiCurrencyItem.Token( - tokenItemVisibleState.copy( - id = "token_5", - name = "Ethereum", - tokenIconResId = R.drawable.img_eth_22, - networkIconResId = null, - amount = "1,89340821 ETH", + tokensListState = WalletTokensListState.Content( + persistentListOf( + WalletTokensListState.TokensListItemState.NetworkGroupTitle("Bitcoin"), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_1", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_2", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_3", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_4", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), + ), + WalletTokensListState.TokensListItemState.NetworkGroupTitle("Ethereum"), + WalletTokensListState.TokensListItemState.Token( + tokenItemVisibleState.copy( + id = "token_5", + name = "Ethereum", + tokenIconResId = R.drawable.img_eth_22, + networkIconResId = null, + amount = "1,89340821 ETH", + ), ), ), + onOrganizeTokensClick = {}, + ), + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, ), notifications = persistentListOf( WalletNotification.UnreachableNetworks, WalletNotification.LikeTangemApp(onClick = {}), - WalletNotification.NeedToBackup(onClick = {}), + WalletNotification.BackupCard(onClick = {}), WalletNotification.ScanCard(onClick = {}), ), - onOrganizeTokensClick = {}, + bottomSheet = bottomSheet, ) val singleWalletScreenState = WalletStateHolder.SingleCurrencyContent( onBackClick = {}, topBarConfig = walletTopBarConfig, walletsListConfig = walletListConfig, - contentItems = persistentListOf( - WalletContentItemState.SingleCurrencyItem.Title(onExploreClick = {}), - WalletContentItemState.SingleCurrencyItem.GroupTitle("Today"), - WalletContentItemState.SingleCurrencyItem.Transaction( - TransactionState.Sending( - address = "33BddS...ga2B", - amount = "-0.500913 BTC", - timestamp = "8:41", - ), + pullToRefreshConfig = WalletPullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), + 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, ), - WalletContentItemState.SingleCurrencyItem.GroupTitle("Yesterday"), - WalletContentItemState.SingleCurrencyItem.Transaction( - TransactionState.Sending( - address = "33BddS...ga2B", - amount = "-0.500913 BTC", - timestamp = "8:41", + ), + txHistoryState = WalletTxHistoryState.Content( + flowOf( + PagingData.from( + listOf( + WalletTxHistoryState.TxHistoryItemState.Title(onExploreClick = {}), + WalletTxHistoryState.TxHistoryItemState.GroupTitle("Today"), + WalletTxHistoryState.TxHistoryItemState.Transaction( + TransactionState.Sending( + address = "33BddS...ga2B", + amount = "-0.500913 BTC", + timestamp = "8:41", + ), + ), + WalletTxHistoryState.TxHistoryItemState.GroupTitle("Yesterday"), + WalletTxHistoryState.TxHistoryItemState.Transaction( + TransactionState.Sending( + address = "33BddS...ga2B", + amount = "-0.500913 BTC", + timestamp = "8:41", + ), + ), + ), ), ), ), - notifications = persistentListOf(WalletNotification.LikeTangemApp(onClick = {})), - buttons = manageButtons, - marketplaceBlockState = marketplaceBlockContent, ) } \ 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/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index d0967c3046..87f1b076b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData @@ -208,7 +209,7 @@ private fun TopBar( ) { RoundedActionButton( config = ActionButtonConfig( - text = stringResource(id = R.string.organize_tokens_sort_by_balance), + text = TextReference.Res(id = R.string.organize_tokens_sort_by_balance), iconResId = R.drawable.ic_sort_24, onClick = config.onSortByBalanceClick, ), @@ -217,7 +218,7 @@ private fun TopBar( ) RoundedActionButton( config = ActionButtonConfig( - text = stringResource(id = R.string.organize_tokens_group), + text = TextReference.Res(id = R.string.organize_tokens_group), iconResId = R.drawable.ic_group_24, onClick = config.onGroupByNetworkClick, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index e81b764058..52f391935b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -3,13 +3,16 @@ package com.tangem.feature.wallet.presentation.organizetokens import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.DragConfig import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder.HeaderConfig import com.tangem.feature.wallet.presentation.organizetokens.utils.* import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.router.WalletRoute import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.Dispatchers @@ -20,12 +23,17 @@ import kotlin.properties.Delegates // FIXME: Implemented with preview data @HiltViewModel -internal class OrganizeTokensViewModel @Inject constructor() : ViewModel() { +internal class OrganizeTokensViewModel @Inject constructor(savedStateHandle: SavedStateHandle) : ViewModel() { @Volatile private var movingItem: DraggableItem? = null var router: InnerWalletRouter by Delegates.notNull() + val userWalletId: UserWalletId by lazy { + val userWalletIdValue: String = checkNotNull(savedStateHandle[WalletRoute.userWalletIdKey]) + + UserWalletId(userWalletIdValue) + } var uiState: OrganizeTokensStateHolder by mutableStateOf(getInitialState()) private set 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..de6989f860 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 @@ -1,20 +1,22 @@ package com.tangem.feature.wallet.presentation.router -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 import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.NavigationStateHolder -import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel @@ -34,27 +36,30 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation override fun Initialize(fragmentManager: FragmentManager) { this.fragmentManager = fragmentManager - TangemTheme { - NavHost( - navController = rememberNavController().apply { navController = this }, - startDestination = WalletScreens.WALLET.name, + NavHost( + navController = rememberNavController().apply { navController = this }, + startDestination = WalletRoute.Wallet.route, + ) { + composable(WalletRoute.Wallet.route) { + val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } + LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel) + + WalletScreen(state = viewModel.uiState) + } + + composable( + WalletRoute.OrganizeTokens.route, + arguments = listOf(navArgument(WalletRoute.userWalletIdKey) { type = NavType.StringType }), ) { - composable(WalletScreens.WALLET.name) { - val viewModel = hiltViewModel().apply { router = this@DefaultWalletRouter } - WalletScreen(state = viewModel.uiState) - } + val viewModel: OrganizeTokensViewModel = hiltViewModel() + .apply { + router = this@DefaultWalletRouter + } - composable(WalletScreens.ORGANIZE_TOKENS.name) { - BackHandler(onBack = ::popBackStack) - - val viewModel: OrganizeTokensViewModel = hiltViewModel() - .apply { router = this@DefaultWalletRouter } - - OrganizeTokensScreen( - modifier = Modifier.systemBarsPadding(), - state = viewModel.uiState, - ) - } + OrganizeTokensScreen( + modifier = Modifier.systemBarsPadding(), + state = viewModel.uiState, + ) } } } @@ -73,12 +78,20 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation } } - override fun openOrganizeTokensScreen() { - navController.navigate(WalletScreens.ORGANIZE_TOKENS.name) + override fun openOrganizeTokensScreen(userWalletId: UserWalletId) { + navController.navigate(WalletRoute.OrganizeTokens.createRoute(userWalletId)) } override fun openDetailsScreen() { - navigationStateHolder.navigate(NavigationAction.NavigateTo(AppScreen.Details)) + navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.Details)) + } + + override fun openOnboardingScreen() { + navigationStateHolder.navigate(action = NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + } + + override fun openTxHistoryWebsite(url: String) { + navigationStateHolder.navigate(action = NavigationAction.OpenUrl(url)) } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 520d008d6d..5fab6367de 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.fragment.app.FragmentManager +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.wallet.navigation.WalletRouter /** @@ -29,8 +30,14 @@ internal interface InnerWalletRouter : WalletRouter { fun popBackStack() /** Open organize tokens screen */ - fun openOrganizeTokensScreen() + fun openOrganizeTokensScreen(userWalletId: UserWalletId) /** Open details screen */ fun openDetailsScreen() + + /** Open onboarding screen */ + fun openOnboardingScreen() + + /** Open transaction history website by [url] */ + fun openTxHistoryWebsite(url: String) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletRoute.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletRoute.kt new file mode 100644 index 0000000000..2cb8a4a91c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletRoute.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.wallet.presentation.router + +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Wallet feature screens + * + * @property route route string representation + * +[REDACTED_AUTHOR] + */ +internal sealed class WalletRoute(val route: String) { + + object Wallet : WalletRoute(route = "wallet") + + object OrganizeTokens : WalletRoute(route = "wallet/{$userWalletIdKey}/organize_tokens") { + + fun createRoute(userWalletId: UserWalletId) = "wallet/${userWalletId.stringValue}/organize_tokens" + } + + companion object { + const val userWalletIdKey = "userWalletId" + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletScreens.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletScreens.kt deleted file mode 100644 index 0a74b766aa..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/WalletScreens.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.feature.wallet.presentation.router - -/** - * Wallet feature screens - * -[REDACTED_AUTHOR] - */ -enum class WalletScreens { - WALLET, ORGANIZE_TOKENS -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt new file mode 100644 index 0000000000..affbdde057 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -0,0 +1,43 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.common.CardTypesResolver +import com.tangem.utils.toFormattedCurrencyString +import java.math.BigDecimal + +/** + * Wallet additional info factory + * +[REDACTED_AUTHOR] + */ +// TODO: Finalize strings [REDACTED_JIRA] +internal object WalletAdditionalInfoFactory { + + /** + * Get additional info + * + * @param cardTypesResolver card types resolver + * @param isLocked check if wallet is locked + * @param currencyAmount amount of currency + */ + fun resolve(cardTypesResolver: CardTypesResolver, isLocked: Boolean, currencyAmount: BigDecimal? = null): String { + return if (cardTypesResolver.isMultiwalletAllowed()) { + val backupInfo = "${cardTypesResolver.getBackupCardsCount()} cards" + when { + cardTypesResolver.isWallet2() && !isLocked -> "$backupInfo • Seed phrase" + cardTypesResolver.isTangemWallet() && !isLocked -> backupInfo + isLocked -> "$backupInfo • Locked" + else -> "" + } + } else { + if (isLocked) { + "Locked" + } else { + val blockchain = cardTypesResolver.getBlockchain() + currencyAmount?.toFormattedCurrencyString( + decimals = blockchain.decimals(), + currency = blockchain.currency, + ).orEmpty() + } + } + } +} \ No newline at end of file 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..5a8f12209b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletBottomSheetConfig.kt @@ -0,0 +1,99 @@ +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, + ) { + + data class ButtonConfig( + val text: TextReference, + 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 = TextReference.Str(value = "Unlock"), onClick = onUnlockClick), + secondaryButtonConfig = ButtonConfig( + text = TextReference.Str(value = "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 = TextReference.Str(value = "Rate the app"), + onClick = onRateTheAppClick, + ), + secondaryButtonConfig = ButtonConfig( + text = TextReference.Str(value = "Share feedback"), + onClick = onShareClick, + ), + ) + + data class CriticalWarningAlreadySignedHashes( + val onOkClick: () -> Unit, + val onCancelClick: () -> Unit, + ) : BottomSheetContentConfig( + title = TextReference.Res( + id = R.string.warning_important_security_info, + formatArgs = WrappedList(listOf("\u26A0")), + ), + subtitle = TextReference.Res(id = R.string.alert_signed_hashes_message), + iconResId = R.drawable.img_attention_20, + tint = null, + primaryButtonConfig = ButtonConfig( + text = TextReference.Res(id = R.string.common_ok), + onClick = onOkClick, + ), + secondaryButtonConfig = ButtonConfig( + text = TextReference.Res(id = R.string.common_cancel), + onClick = onCancelClick, + ), + ) + } +} \ 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/WalletContentItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletContentItemState.kt deleted file mode 100644 index 19142be1cd..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletContentItemState.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state - -import com.tangem.core.ui.components.transactions.TransactionState -import com.tangem.feature.wallet.presentation.common.state.TokenItemState - -/** - * Wallet screen content item state - * -[REDACTED_AUTHOR] - */ -internal sealed interface WalletContentItemState { - - /** Multi currency wallet content state */ - sealed interface MultiCurrencyItem : WalletContentItemState { - - /** - * Network group title item - * - * @property networkName network name - */ - data class NetworkGroupTitle(val networkName: String) : MultiCurrencyItem - - /** - * Token item - * - * @property state token item state - */ - data class Token(val state: TokenItemState) : MultiCurrencyItem - } - - /** Single currency wallet content state */ - sealed interface SingleCurrencyItem : WalletContentItemState { - - /** - * Title item - * - * @property onExploreClick lambda be invoke when explore button was clicked - */ - data class Title(val onExploreClick: () -> Unit) : SingleCurrencyItem - - /** - * Group title item - * - * @property title title - */ - data class GroupTitle(val title: String) : SingleCurrencyItem - - /** - * Transaction item - * - * @property state transaction state - */ - data class Transaction(val state: TransactionState) : SingleCurrencyItem - } - - object Loading : WalletContentItemState -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt index e5f6115aaa..cb07819add 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletManageButton.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R /** @@ -19,7 +20,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Buy(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Buy", + text = TextReference.Res(id = R.string.common_buy), iconResId = R.drawable.ic_plus_24, onClick = onClick, ), @@ -32,7 +33,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Send(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Send", + text = TextReference.Res(id = R.string.common_send), iconResId = R.drawable.ic_arrow_up_24, onClick = onClick, ), @@ -45,7 +46,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Receive(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Receive", + text = TextReference.Res(id = R.string.common_receive), iconResId = R.drawable.ic_arrow_down_24, onClick = onClick, ), @@ -58,7 +59,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Exchange(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Exchange", + text = TextReference.Res(id = R.string.common_exchange), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, ), @@ -71,7 +72,7 @@ sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class CopyAddress(val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = "Copy address", + text = TextReference.Res(id = R.string.common_copy_address), iconResId = R.drawable.ic_copy_24, onClick = onClick, ), 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..842f92ccfe 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,124 @@ 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, + ), + ) + + /** + * "Remaining signatures left" notification + * + * @param count number of remaining signatures + */ + class RemainingSignaturesLeft(count: Int) : WalletNotification( + state = NotificationState.Simple( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res( + id = R.string.warning_low_signatures_format, + formatArgs = WrappedList(data = listOf(count)), + ), + iconResId = R.drawable.ic_alert_circle_24, + tint = TangemColorPalette.Amaranth, + ), + ) + + /** + * "Already topped up and signed hashes" warning notification + * + * @property onClick lambda be invoked when notification's close button is clicked + */ + data class WarningAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification( + state = NotificationState.Closable( + title = TextReference.Res(id = R.string.common_warning), + subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions), + iconResId = R.drawable.img_attention_20, + tint = null, + onCloseClick = onClick, + ), + ) + + /** + * "Already signed hashes" critical warning notification + * + * @property onClick lambda be invoked when notification is clicked + */ + data class CriticalWarningAlreadySignedHashes(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 +141,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 +155,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..216dc94148 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,22 @@ 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 com.tangem.feature.wallet.presentation.wallet.state.content.WalletLockedContentState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState 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 notifications notifications * [REDACTED_AUTHOR] */ @@ -17,47 +24,165 @@ internal sealed class WalletStateHolder( open val onBackClick: () -> Unit, open val topBarConfig: WalletTopBarConfig, open val walletsListConfig: WalletsListConfig, - open val contentItems: ImmutableList, + open val pullToRefreshConfig: WalletPullToRefreshConfig, 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, + notifications: ImmutableList = this.notifications, + bottomSheet: WalletBottomSheetConfig? = this.bottomSheet, + ): WalletStateHolder { + return when (this) { + is MultiCurrencyContent -> this.copy( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + notifications = notifications, + bottomSheet = bottomSheet, + ) + is SingleCurrencyContent -> this.copy( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + 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 tokensListState token list state + * @property notifications notifications */ data class MultiCurrencyContent( override val onBackClick: () -> Unit, override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, - override val contentItems: ImmutableList, + override val pullToRefreshConfig: WalletPullToRefreshConfig, override val notifications: ImmutableList, - val onOrganizeTokensClick: () -> Unit, - ) : WalletStateHolder(onBackClick, topBarConfig, walletsListConfig, contentItems, notifications) + override val bottomSheet: WalletBottomSheetConfig? = null, + val tokensListState: WalletTokensListState, + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + notifications = notifications, + bottomSheet = bottomSheet, + ) /** * 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 notifications notifications + * @property buttons manage buttons + * @property marketPriceBlockState market price block state + * @property txHistoryState transactions history state */ data class SingleCurrencyContent( override val onBackClick: () -> Unit, override val topBarConfig: WalletTopBarConfig, override val walletsListConfig: WalletsListConfig, - override val contentItems: ImmutableList, + override val pullToRefreshConfig: WalletPullToRefreshConfig, 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, + val txHistoryState: WalletTxHistoryState, + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + 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 pullToRefreshConfig pull to refresh config + * @property lockedContentState locked content state + * @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 lockedContentState: WalletLockedContentState, + val onUnlockWalletsNotificationClick: () -> Unit, + val onBottomSheetDismissRequest: () -> Unit, + val onUnlockClick: () -> Unit, + val onScanClick: () -> Unit, + ) : WalletStateHolder( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + 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 = {}), + 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/content/WalletLockedContentState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt new file mode 100644 index 0000000000..9867aed606 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletLockedContentState.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.wallet.presentation.wallet.state.content + +/** + * Wallet locked content state. + * It allows to divide the locked content of multi-currency and single-currency wallets. + * +[REDACTED_AUTHOR] + */ +internal sealed interface WalletLockedContentState \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt new file mode 100644 index 0000000000..4f7e51d4d3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTokensListState.kt @@ -0,0 +1,60 @@ +package com.tangem.feature.wallet.presentation.wallet.state.content + +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Wallet tokens list state + * + * @property items content items + * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + * +[REDACTED_AUTHOR] + */ +// TODO: Finalize strings [REDACTED_JIRA] +internal sealed class WalletTokensListState( + open val items: ImmutableList, + open val onOrganizeTokensClick: (() -> Unit)?, +) { + + /** + * Content state + * + * @property items content items + * @property onOrganizeTokensClick lambda be invoked when organize tokens button is clicked + */ + data class Content( + override val items: ImmutableList, + override val onOrganizeTokensClick: (() -> Unit)?, + ) : WalletTokensListState(items, onOrganizeTokensClick) + + /** Locked content state */ + object Locked : + WalletTokensListState( + items = persistentListOf( + TokensListItemState.NetworkGroupTitle(networkName = "Tokens"), + TokensListItemState.Token(state = TokenItemState.Loading), + ), + onOrganizeTokensClick = null, + ), + WalletLockedContentState + + /** Tokens list item state */ + sealed interface TokensListItemState { + + /** + * Network group title item + * + * @property networkName network name + */ + data class NetworkGroupTitle(val networkName: String) : TokensListItemState + + /** + * Token item + * + * @property state token item state + */ + data class Token(val state: TokenItemState) : TokensListItemState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt new file mode 100644 index 0000000000..5252fa8981 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/content/WalletTxHistoryState.kt @@ -0,0 +1,92 @@ +package com.tangem.feature.wallet.presentation.wallet.state.content + +import androidx.paging.PagingData +import com.tangem.core.ui.components.transactions.TransactionState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf + +/** + * Wallet transaction history state + * +[REDACTED_AUTHOR] + */ +internal sealed interface WalletTxHistoryState { + + /** + * Wallet transaction history state with content + * + * @property items content items + */ + sealed class ContentState(open val items: Flow>) : WalletTxHistoryState + + /** + * Content state + * + * @property items content items + */ + data class Content(override val items: Flow>) : ContentState(items) + + /** + * Locked state + * + * @property onExploreClick lambda be invoke when explore button was clicked + */ + data class Locked(val onExploreClick: () -> Unit) : + ContentState( + items = flowOf( + PagingData.from( + listOf( + TxHistoryItemState.Title(onExploreClick = onExploreClick), + TxHistoryItemState.Transaction(state = TransactionState.Loading), + ), + ), + ), + ), + WalletLockedContentState + + /** + * Empty state + * + * @property onBuyClick lambda be invoke when buy button was clicked + */ + data class Empty(val onBuyClick: () -> Unit) : WalletTxHistoryState + + /** + * Not supported tx history state + * + * @property onExploreClick lambda be invoke when explore button was clicked + */ + data class NotSupported(val onExploreClick: () -> Unit) : WalletTxHistoryState + + /** + * Error state + * + * @property onReloadClick lambda be invoke when reload button was clicked + */ + data class Error(val onReloadClick: () -> Unit) : WalletTxHistoryState + + /** Transactions history item state */ + sealed interface TxHistoryItemState { + + /** + * Title item + * + * @property onExploreClick lambda be invoke when explore button was clicked + */ + data class Title(val onExploreClick: () -> Unit) : TxHistoryItemState + + /** + * Group title item + * + * @property title title + */ + data class GroupTitle(val title: String) : TxHistoryItemState + + /** + * Transaction item + * + * @property state transaction state + */ + data class Transaction(val state: TransactionState) : TxHistoryItemState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt new file mode 100644 index 0000000000..293d66e1bc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletLoadedTokensListConverter.kt @@ -0,0 +1,61 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListErrorToWalletStateConverter +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter + +/** + * Converter from loaded [TokenListError] or [TokenList] to [WalletStateHolder] + * + * @property currentStateProvider current ui state provider + * @param cardTypeResolverProvider card type resolver + * @param clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletLoadedTokensListConverter( + private val currentStateProvider: Provider, + cardTypeResolverProvider: Provider, + clickIntents: WalletClickIntents, +) : Converter { + + private val tokenListStateConverter = TokenListToWalletStateConverter( + currentStateProvider = currentStateProvider, + cardTypeResolverProvider = cardTypeResolverProvider, + isWalletContentHidden = false, // TODO: [REDACTED_JIRA] + fiatCurrencyCode = "USD", // TODO: [REDACTED_JIRA] + fiatCurrencySymbol = "$", // TODO: [REDACTED_JIRA] + clickIntents = clickIntents, + ) + + private val tokenListErrorStateConverter = TokenListErrorToWalletStateConverter( + currentStateProvider = currentStateProvider, + ) + + override fun convert(value: LoadedTokensListModel): WalletStateHolder { + return value.tokenListEither.fold( + ifLeft = tokenListErrorStateConverter::convert, + ifRight = { + tokenListStateConverter.convert( + value = TokenListToWalletStateConverter.TokensListModel( + tokenList = it, + isRefreshing = value.isRefreshing, + ), + ) + }, + ) + } + + data class LoadedTokensListModel( + val tokenListEither: Either, + val isRefreshing: Boolean, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt new file mode 100644 index 0000000000..72419fce8b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletSkeletonStateConverter.kt @@ -0,0 +1,106 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import androidx.paging.PagingData +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.* +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.flow + +/** + * Converter from loaded list of [UserWallet] to skeleton state of screen [WalletStateHolder] + * + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletSkeletonStateConverter( + private val clickIntents: WalletClickIntents, +) : Converter, WalletStateHolder> { + + override fun convert(value: List): WalletStateHolder { + val cardTypeResolver = requireNotNull(value.firstOrNull()).scanResponse.cardTypesResolver + + return if (cardTypeResolver.isMultiwalletAllowed()) { + createMultiCurrencyState(value) + } else { + createSingleCurrencyState(value, cardTypeResolver) + } + } + + private fun createMultiCurrencyState(wallets: List): WalletStateHolder.MultiCurrencyContent { + return WalletStateHolder.MultiCurrencyContent( + onBackClick = clickIntents::onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(wallets), + pullToRefreshConfig = createPullToRefreshConfig(), + tokensListState = WalletTokensListState.Content( + items = persistentListOf(), + onOrganizeTokensClick = clickIntents::onOrganizeTokensClick, + ), + notifications = persistentListOf(), + bottomSheet = null, + ) + } + + private fun createSingleCurrencyState( + wallets: List, + cardTypeResolver: CardTypesResolver, + ): WalletStateHolder.SingleCurrencyContent { + return WalletStateHolder.SingleCurrencyContent( + onBackClick = clickIntents::onBackClick, + topBarConfig = createTopBarConfig(), + walletsListConfig = createWalletsListConfig(wallets), + pullToRefreshConfig = createPullToRefreshConfig(), + notifications = persistentListOf(), + bottomSheet = null, + buttons = WalletPreviewData.singleWalletScreenState.buttons, // TODO: create buttons + marketPriceBlockState = MarketPriceBlockState.Loading( + currencyName = cardTypeResolver.getBlockchain().currency, + ), + txHistoryState = WalletTxHistoryState.Content( + items = flow { PagingData.empty() }, + ), + ) + } + + private fun createTopBarConfig(): WalletTopBarConfig { + return WalletTopBarConfig( + onScanCardClick = clickIntents::onScanCardClick, + onMoreClick = clickIntents::onDetailsClick, + ) + } + + private fun createWalletsListConfig(wallets: List): WalletsListConfig { + return WalletsListConfig( + selectedWalletIndex = 0, + wallets = wallets.map { wallet -> + val cardTypeResolver = wallet.scanResponse.cardTypesResolver + WalletCardState.Loading( + id = wallet.walletId, + title = wallet.name, + additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolver, + isLocked = wallet.isLocked, + ), + imageResId = WalletImageResolver.resolve(cardTypesResolver = cardTypeResolver), + ) + }.toImmutableList(), + onWalletChange = clickIntents::onWalletChange, + ) + } + + private fun createPullToRefreshConfig(): WalletPullToRefreshConfig { + return WalletPullToRefreshConfig(isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt new file mode 100644 index 0000000000..453c182e49 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/WalletStateFactory.kt @@ -0,0 +1,112 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.factory.WalletLoadedTokensListConverter.LoadedTokensListModel +import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadedTxHistoryConverter +import com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory.WalletLoadingTxHistoryConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.Flow + +/** + * Main factory for creating [WalletStateHolder] + * + * @property currentStateProvider current ui state provider + * @param currentCardTypeResolverProvider current card type resolver + * @property clickIntents screen click intents + */ +internal class WalletStateFactory( + private val currentStateProvider: Provider, + currentCardTypeResolverProvider: Provider, + private val clickIntents: WalletClickIntents, +) { + + private val skeletonConverter by lazy { WalletSkeletonStateConverter(clickIntents = clickIntents) } + + private val loadedTokensListConverter by lazy { + WalletLoadedTokensListConverter( + currentStateProvider = currentStateProvider, + cardTypeResolverProvider = currentCardTypeResolverProvider, + clickIntents = clickIntents, + ) + } + + private val loadingTransactionsStateConverter by lazy { + WalletLoadingTxHistoryConverter( + currentStateProvider = currentStateProvider, + currentCardTypeResolverProvider = currentCardTypeResolverProvider, + clickIntents = clickIntents, + ) + } + + private val loadedTxHistoryConverter by lazy { + WalletLoadedTxHistoryConverter( + currentStateProvider = currentStateProvider, + currentCardTypeResolverProvider = currentCardTypeResolverProvider, + clickIntents = clickIntents, + ) + } + + fun getInitialState(): WalletStateHolder = WalletStateHolder.Loading(onBackClick = clickIntents::onBackClick) + + fun getSkeletonState(wallets: List): WalletStateHolder = skeletonConverter.convert(wallets) + + fun getStateByTokensList( + tokenListEither: Either, + isRefreshing: Boolean, + ): WalletStateHolder { + return loadedTokensListConverter.convert( + value = LoadedTokensListModel(tokenListEither = tokenListEither, isRefreshing = isRefreshing), + ) + } + + fun getStateByNotifications(notifications: ImmutableList): WalletStateHolder { + return currentStateProvider().copySealed(notifications = notifications) + } + + fun getStateAfterWalletChanging(index: Int): WalletStateHolder { + return currentStateProvider().let { stateHolder -> + stateHolder.copySealed(walletsListConfig = stateHolder.walletsListConfig.copy(selectedWalletIndex = index)) + } + } + + fun getStateAfterContentRefreshing(): WalletStateHolder { + return currentStateProvider().let { state -> + state.copySealed(pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = true)) + } + } + + fun getStateWithOpenBottomSheet(content: WalletBottomSheetConfig.BottomSheetContentConfig): WalletStateHolder { + return currentStateProvider().let { state -> + state.copySealed( + bottomSheet = WalletBottomSheetConfig( + isShow = true, + onDismissRequest = { state.copySealed(bottomSheet = state.bottomSheet?.copy(isShow = false)) }, + content = content, + ), + ) + } + } + + fun getLoadingTxHistoryState(itemsCountEither: Either): WalletStateHolder { + return loadingTransactionsStateConverter.convert(value = itemsCountEither) + } + + fun getLoadedTxHistoryState( + txHistoryEither: Either>>, + ): WalletStateHolder { + return loadedTxHistoryConverter.convert(txHistoryEither) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt new file mode 100644 index 0000000000..5ed89bf2e9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadedTxHistoryConverter.kt @@ -0,0 +1,95 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.txhistory.error.TxHistoryListError +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow + +/** + * Converter from loaded tx history to [WalletTxHistoryState] + * + * @property currentStateProvider current state provider + * @property currentCardTypeResolverProvider current card type resolver provider + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletLoadedTxHistoryConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter>>, WalletStateHolder> { + + private val walletTxHistoryItemFlowConverter by lazy { + WalletTxHistoryItemFlowConverter( + blockchain = currentCardTypeResolverProvider().getBlockchain(), + clickIntents = clickIntents, + ) + } + + override fun convert(value: Either>>): WalletStateHolder { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convertError(error: TxHistoryListError): WalletStateHolder { + return currentStateProvider().copySingleCurrencyContent( + txHistoryState = when (error) { + is TxHistoryListError.DataError -> { + WalletTxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + }, + ) + } + + private fun convert(items: Flow>): WalletStateHolder { + return currentStateProvider().copySingleCurrencyContent( + txHistoryState = walletTxHistoryItemFlowConverter.convert(value = items), + ) + } + + private fun WalletStateHolder.copySingleCurrencyContent( + txHistoryState: WalletTxHistoryState, + ): WalletStateHolder.SingleCurrencyContent { + return WalletStateHolder.SingleCurrencyContent( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + notifications = notifications, + bottomSheet = bottomSheet, + buttons = getButtons(), + marketPriceBlockState = getLoadingMarketPriceBlockState(), + txHistoryState = txHistoryState, + ) + } + + // TODO: [REDACTED_JIRA] + private fun getButtons(): ImmutableList { + return persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) + .map(WalletManageButton::config) + .toImmutableList() + } + + private fun getLoadingMarketPriceBlockState(): MarketPriceBlockState { + return MarketPriceBlockState.Loading(currencyName = currentCardTypeResolverProvider().getBlockchain().currency) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt new file mode 100644 index 0000000000..da2936afcc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletLoadingTxHistoryConverter.kt @@ -0,0 +1,102 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory + +import androidx.paging.PagingData +import arrow.core.Either +import com.tangem.common.Provider +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.txhistory.error.TxHistoryStateError +import com.tangem.feature.wallet.presentation.wallet.state.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.flowOf + +/** + * Converter from loading tx history to [WalletTxHistoryState] + * + * @property currentStateProvider current state provider + * @property currentCardTypeResolverProvider current card type resolver provider + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletLoadingTxHistoryConverter( + private val currentStateProvider: Provider, + private val currentCardTypeResolverProvider: Provider, + private val clickIntents: WalletClickIntents, +) : Converter, WalletStateHolder> { + + override fun convert(value: Either): WalletStateHolder { + return value.fold(ifLeft = ::convertError, ifRight = ::convert) + } + + private fun convert(value: Int): WalletStateHolder { + return currentStateProvider().copySingleCurrencyContent( + txHistoryState = WalletTxHistoryState.Content( + items = flowOf( + value = PagingData.from( + data = buildList(capacity = value) { + add(WalletTxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading)) + }, + ), + ), + ), + ) + } + + private fun convertError(error: TxHistoryStateError): WalletStateHolder { + return currentStateProvider().copySingleCurrencyContent( + txHistoryState = when (error) { + is TxHistoryStateError.EmptyTxHistories -> { + WalletTxHistoryState.Empty(onBuyClick = clickIntents::onBuyClick) + } + is TxHistoryStateError.DataError -> { + WalletTxHistoryState.Error(onReloadClick = clickIntents::onReloadClick) + } + is TxHistoryStateError.TxHistoryNotImplemented -> { + WalletTxHistoryState.NotSupported(onExploreClick = clickIntents::onExploreClick) + } + }, + ) + } + + private fun WalletStateHolder.copySingleCurrencyContent( + txHistoryState: WalletTxHistoryState, + ): WalletStateHolder.SingleCurrencyContent { + return WalletStateHolder.SingleCurrencyContent( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + notifications = notifications, + bottomSheet = bottomSheet, + buttons = getButtons(), + marketPriceBlockState = getLoadingMarketPriceBlockState(), + txHistoryState = txHistoryState, + ) + } + + // TODO: [REDACTED_JIRA] + private fun getButtons(): ImmutableList { + return persistentListOf( + WalletManageButton.Buy(onClick = {}), + WalletManageButton.Send(onClick = {}), + WalletManageButton.Receive(onClick = {}), + WalletManageButton.Exchange(onClick = {}), + WalletManageButton.CopyAddress(onClick = {}), + ) + .map(WalletManageButton::config) + .toImmutableList() + } + + private fun getLoadingMarketPriceBlockState(): MarketPriceBlockState { + return MarketPriceBlockState.Loading(currencyName = currentCardTypeResolverProvider().getBlockchain().currency) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt new file mode 100644 index 0000000000..2fe01c3e0e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/txhistory/WalletTxHistoryItemFlowConverter.kt @@ -0,0 +1,216 @@ +package com.tangem.feature.wallet.presentation.wallet.state.factory.txhistory + +import android.text.format.DateUtils +import androidx.paging.* +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.components.transactions.TransactionState +import com.tangem.domain.txhistory.model.TxHistoryItem +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday +import com.tangem.utils.toBriefAddressFormat +import com.tangem.utils.toFormattedCurrencyString +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder +import java.math.BigDecimal +import java.util.Locale + +/** + * Convert from [Flow] of [TxHistoryItem] to [WalletTxHistoryState] + * + * @property blockchain blockchain of transactions history + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletTxHistoryItemFlowConverter( + private val blockchain: Blockchain, + private val clickIntents: WalletClickIntents, +) : Converter>, WalletTxHistoryState> { + + /** Example, 2 Aug, 2023 */ + private val dateFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(1) + .appendLiteral(' ') + .appendMonthOfYearShortText() + .appendLiteral(", ") + .appendYear(4, 4) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + /** Example, 13:35 */ + private val timeFormatter by lazy { + DateTimeFormatterBuilder() + .appendHourOfDay(1) + .appendLiteral(':') + .appendMinuteOfHour(2) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: Flow>): WalletTxHistoryState { + return WalletTxHistoryState.Content( + items = value + .map { pagingData -> + pagingData + .map { item -> + // [createTransactionState] returns timestamp without formatting + TxHistoryItemState.Transaction(state = createTransactionState(item)) + } + .insertHeaderItem( + terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE, + item = TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick), + ) + .insertGroupTitle() // method uses the raw timestamp + .formatTransactionsTimestamp() // method formats the timestamp + }, + ) + } + + private fun createTransactionState(item: TxHistoryItem): TransactionState { + return when (item.type) { + TxHistoryItem.TransactionType.Transfer -> { + when (val direction = item.direction) { + is TxHistoryItem.TransactionDirection.Incoming -> { + createIncomingTransferTransaction(item, direction, blockchain) + } + is TxHistoryItem.TransactionDirection.Outgoing -> { + createOutgoingTransferTransaction(item, direction, blockchain) + } + } + } + } + } + + private fun createIncomingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Incoming, + blockchain: Blockchain, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Receive( + address = direction.from.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Receiving( + address = direction.from.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun createOutgoingTransferTransaction( + item: TxHistoryItem, + direction: TxHistoryItem.TransactionDirection.Outgoing, + blockchain: Blockchain, + ): TransactionState { + return when (item.status) { + TxHistoryItem.TxStatus.Confirmed -> TransactionState.Send( + address = direction.to.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), + timestamp = item.getRawTimestamp(), + ) + TxHistoryItem.TxStatus.Unconfirmed -> TransactionState.Sending( + address = direction.to.toBriefAddressFormat(), + amount = item.amount.toCryptoCurrencyFormat(blockchain = blockchain), + timestamp = item.getRawTimestamp(), + ) + } + } + + private fun BigDecimal.toCryptoCurrencyFormat(blockchain: Blockchain): String { + return toFormattedCurrencyString(currency = blockchain.currency, decimals = blockchain.decimals()) + } + + private fun PagingData.insertGroupTitle(): PagingData { + return insertSeparators(terminalSeparatorType = TerminalSeparatorType.SOURCE_COMPLETE) { before, after -> + // Use raw timestamp to get date + + // If [afterDate] is the first transaction in the flow, add the group title + val afterDate = after.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + if (before is TxHistoryItemState.Title) { + return@insertSeparators TxHistoryItemState.GroupTitle(afterDate) + } + + /* + * If [beforeDate] is not equals to [afterDate], then [afterDate] is first transaction in + * the new group + */ + val beforeDate = before.getTimestamp()?.toDateFormat() ?: return@insertSeparators null + return@insertSeparators if (beforeDate != afterDate) { + TxHistoryItemState.GroupTitle(afterDate) + } else { + null + } + } + } + + /** + * Map the [PagingData] to format the [TxHistoryItemState] timestamp + */ + private fun PagingData.formatTransactionsTimestamp(): PagingData { + return map { txHistoryItemState -> + if (txHistoryItemState is TxHistoryItemState.Transaction && + txHistoryItemState.state is TransactionState.Content + ) { + txHistoryItemState.copy( + state = txHistoryItemState.state.copySealed( + timestamp = txHistoryItemState.state.timestamp.toTimeFormat(), + ), + ) + } else { + txHistoryItemState + } + } + } + + /** + * Get timestamp without formatting. + * It's life hack that help us to add transaction's group title to flow. + * + * @see [convert] + */ + private fun TxHistoryItem.getRawTimestamp() = this.timestamp.toString() + + private fun TxHistoryItemState?.getTimestamp(): Long? { + return if (this is TxHistoryItemState.Transaction && this.state is TransactionState.Content) { + requireNotNull(this.state.timestamp.toLongOrNull()) { "Timestamp must be Long type" } + } else { + null + } + } + + /** + * If [this] timestamp is today or yesterday, returns relative date, + * otherwise returns formatting date by [dateFormatter] + */ + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + dateFormatter.print(localDate) + } + } + + private fun String.toTimeFormat(): String { + return timeFormatter.print( + DateTime(this.toLong(), DateTimeZone.getDefault()), + ) + } +} \ 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..05f45bd9e4 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 @@ -4,37 +4,35 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.* import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -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.foundation.lazy.* +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.PullRefreshState +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.actions.ActionButtonConfig -import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.core.ui.components.buttons.HorizontalActionChips +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 -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem -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.state.content.WalletTxHistoryState +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.components.multicurrency.OrganizeTokensButton +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems +import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.txHistoryItems +import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator /** @@ -44,134 +42,144 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimat * [REDACTED_AUTHOR] */ +@OptIn(ExperimentalMaterialApi::class) @Suppress("LongMethod") @Composable internal fun WalletScreen(state: WalletStateHolder) { BackHandler(onBack = state.onBackClick) + val walletsListState = rememberLazyListState() Scaffold( topBar = { WalletTopBar(config = state.topBarConfig) }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> - 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, - ) + val txHistoryItems = if (state is WalletStateHolder.SingleCurrencyContent) { + if (state.txHistoryState is WalletTxHistoryState.ContentState) { + state.txHistoryState.items.collectAsLazyPagingItems() + } else { + null + } + } else { + null } - 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), + ) + } + } + + contentItems(state = state, txHistoryItems = txHistoryItems, modifier = changeableItemModifier) + + if (state is WalletStateHolder.MultiCurrencyContent) { + item { + OrganizeTokensButton( + onClick = state.tokensListState.onOrganizeTokensClick, + modifier = changeableItemModifier + .padding(top = TangemTheme.dimens.spacing14) + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + } } } + + PullToRefreshIndicator( + isRefreshing = state.pullToRefreshConfig.isRefreshing, + state = pullRefreshState, + modifier = Modifier.align(Alignment.TopCenter), + ) } } + + state.bottomSheet?.let { bottomSheetConfig -> + if (bottomSheetConfig.isShow) WalletBottomSheet(config = bottomSheetConfig) + } + + LaunchedEffect(key1 = walletsListState, key2 = state.walletsListConfig.onWalletChange) { + snapshotFlow { walletsListState.layoutInfo.visibleItemsInfo } + .collect(collector = ScrollOffsetCollector(callback = state.walletsListConfig.onWalletChange)) + } } -@Composable -private fun ContentItem(item: WalletContentItemState, modifier: Modifier = Modifier) { - when (item) { - is WalletContentItemState.MultiCurrencyItem.NetworkGroupTitle -> { - NetworkGroupItem(networkName = item.networkName, modifier = modifier) +private fun LazyListScope.contentItems( + state: WalletStateHolder, + txHistoryItems: LazyPagingItems?, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletStateHolder.MultiCurrencyContent -> { + tokensListItems(state = state.tokensListState, modifier = modifier) } - is WalletContentItemState.MultiCurrencyItem.Token -> { - TokenItem(state = item.state, modifier = modifier) - } - is WalletContentItemState.SingleCurrencyItem.Title -> { - TransactionsBlockTitle(config = item, modifier = modifier) - } - is WalletContentItemState.SingleCurrencyItem.GroupTitle -> { - TransactionsBlockGroupTitle(config = item, modifier = modifier) - } - is WalletContentItemState.SingleCurrencyItem.Transaction -> { - Transaction(state = item.state, modifier = modifier) - } - WalletContentItemState.Loading -> { - TokenItem(state = TokenItemState.Loading, modifier = modifier) + is WalletStateHolder.SingleCurrencyContent -> { + txHistoryItems( + state = state.txHistoryState, + txHistoryItems = txHistoryItems, + modifier = modifier, + ) } + is WalletStateHolder.Loading, + is WalletStateHolder.UnlockWalletContent, + -> Unit } } +@OptIn(ExperimentalMaterialApi::class) @Composable -private fun OrganizeTokensButton(onClick: () -> Unit, modifier: Modifier = Modifier) { - RoundedActionButton( - config = ActionButtonConfig( - text = stringResource(id = R.string.organize_tokens_title), - iconResId = R.drawable.ic_filter_24, - onClick = onClick, - ), +private fun PullToRefreshIndicator(isRefreshing: Boolean, state: PullRefreshState, modifier: Modifier = Modifier) { + PullRefreshIndicator( + refreshing = isRefreshing, + state = state, modifier = modifier, ) } 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..fb7a83e1bf --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletBottomSheet.kt @@ -0,0 +1,160 @@ +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) + + 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.resolveReference(), + onClick = config.onClick, + modifier = modifier, + ) + } else { + PrimaryButtonIconStart( + text = config.text.resolveReference(), + 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.resolveReference(), + onClick = config.onClick, + modifier = modifier, + ) + } else { + SecondaryButtonIconStart( + text = config.text.resolveReference(), + 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..34dc515850 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 @@ -11,18 +11,13 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration 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 +27,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,15 +40,10 @@ 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)) } } - - LaunchedEffect(lazyListState) { - snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo } - .collect(collector = ScrollOffsetCollector(callback = config.onWalletChange)) - } } @Preview diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt new file mode 100644 index 0000000000..c8fc7782f3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.ui.Modifier +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration + +/** + * LazyList extension for [WalletTokensListState] + * + * @param state state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +internal fun LazyListScope.tokensListItems(state: WalletTokensListState, modifier: Modifier = Modifier) { + itemsIndexed( + items = state.items, + key = { index, _ -> index }, + itemContent = { index, item -> + MultiCurrencyContentItem( + state = item, + modifier = modifier.walletContentItemDecoration( + currentIndex = index, + lastIndex = state.items.lastIndex, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt new file mode 100644 index 0000000000..a387c5f956 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.feature.wallet.presentation.common.component.NetworkGroupItem +import com.tangem.feature.wallet.presentation.common.component.TokenItem +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState + +/** + * Multi-currency content item + * + * @param state item state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun MultiCurrencyContentItem(state: WalletTokensListState.TokensListItemState, modifier: Modifier = Modifier) { + when (state) { + is WalletTokensListState.TokensListItemState.NetworkGroupTitle -> { + NetworkGroupItem(networkName = state.networkName, modifier = modifier) + } + is WalletTokensListState.TokensListItemState.Token -> { + TokenItem(state = state.state, modifier = modifier) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt new file mode 100644 index 0000000000..53aeef026b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/OrganizeTokensButton.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.components.buttons.actions.RoundedActionButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.wallet.impl.R + +/** + * Organize tokens button + * + * @param onClick callback, if null button is disabled + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun OrganizeTokensButton(onClick: (() -> Unit)?, modifier: Modifier = Modifier) { + RoundedActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.organize_tokens_title), + iconResId = R.drawable.ic_filter_24, + onClick = onClick ?: {}, + enabled = onClick != null, + ), + modifier = modifier, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt new file mode 100644 index 0000000000..3684c90d62 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContent.kt @@ -0,0 +1,85 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemsIndexed +import com.tangem.core.ui.components.transactions.empty.EmptyTransactionBlock +import com.tangem.core.ui.components.transactions.empty.EmptyTransactionsBlockState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState +import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration + +/** + * LazyList extension for transactions history [WalletTxHistoryState] + * + * @param state state + * @param txHistoryItems transactions + * @param modifier modifier + */ +internal fun LazyListScope.txHistoryItems( + state: WalletTxHistoryState, + txHistoryItems: LazyPagingItems?, + modifier: Modifier = Modifier, +) { + when (state) { + is WalletTxHistoryState.ContentState -> { + contentItems( + txHistoryItems = requireNotNull(txHistoryItems), + modifier = modifier, + ) + } + is WalletTxHistoryState.Empty -> { + nonContentItem( + state = EmptyTransactionsBlockState.Empty(onClick = state.onBuyClick), + modifier = modifier, + ) + } + is WalletTxHistoryState.Error -> { + nonContentItem( + state = EmptyTransactionsBlockState.FailedToLoad(onClick = state.onReloadClick), + modifier = modifier, + ) + } + is WalletTxHistoryState.NotSupported -> { + nonContentItem( + state = EmptyTransactionsBlockState.NotImplemented(onClick = state.onExploreClick), + modifier = modifier, + ) + } + } +} + +private fun LazyListScope.contentItems( + txHistoryItems: LazyPagingItems, + modifier: Modifier = Modifier, +) { + itemsIndexed( + items = txHistoryItems, + key = { index, _ -> index }, + itemContent = { index, item -> + if (item == null) return@itemsIndexed + + SingleCurrencyContentItem( + state = item, + modifier = modifier.walletContentItemDecoration( + currentIndex = index, + lastIndex = txHistoryItems.itemSnapshotList.lastIndex, + ), + ) + }, + ) +} + +private fun LazyListScope.nonContentItem(state: EmptyTransactionsBlockState, modifier: Modifier = Modifier) { + item { + EmptyTransactionBlock( + state = state, + modifier = modifier + .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt new file mode 100644 index 0000000000..58489e3a3d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/SingleCurrencyContentItem.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.transactions.Transaction +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState + +/** + * Single currency content item + * + * @param state state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun SingleCurrencyContentItem(state: WalletTxHistoryState.TxHistoryItemState, modifier: Modifier = Modifier) { + when (state) { + is WalletTxHistoryState.TxHistoryItemState.GroupTitle -> { + TxHistoryGroupTitle(config = state, modifier = modifier) + } + is WalletTxHistoryState.TxHistoryItemState.Title -> { + TxHistoryTitle(config = state, modifier = modifier) + } + is WalletTxHistoryState.TxHistoryItemState.Transaction -> { + Transaction(state = state.state, modifier = modifier) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockGroupTitle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt similarity index 73% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockGroupTitle.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt index eed98b7ebf..275f2fce6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockGroupTitle.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryGroupTitle.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState /** * Transactions block group title @@ -18,10 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemStat * @param modifier modifier */ @Composable -internal fun TransactionsBlockGroupTitle( - config: WalletContentItemState.SingleCurrencyItem.GroupTitle, - modifier: Modifier = Modifier, -) { +internal fun TxHistoryGroupTitle(config: TxHistoryItemState.GroupTitle, modifier: Modifier = Modifier) { Text( text = config.title, modifier = modifier @@ -41,7 +38,7 @@ internal fun TransactionsBlockGroupTitle( @Composable private fun Preview_TransactionsBlockGroupTitle_Light() { TangemTheme(isDark = false) { - TransactionsBlockGroupTitle(config = WalletContentItemState.SingleCurrencyItem.GroupTitle(title = "Today")) + TxHistoryGroupTitle(config = TxHistoryItemState.GroupTitle(title = "Today")) } } @@ -49,6 +46,6 @@ private fun Preview_TransactionsBlockGroupTitle_Light() { @Composable private fun Preview_TransactionsBlockGroupTitle_Dark() { TangemTheme(isDark = true) { - TransactionsBlockGroupTitle(config = WalletContentItemState.SingleCurrencyItem.GroupTitle(title = "Today")) + TxHistoryGroupTitle(config = TxHistoryItemState.GroupTitle(title = "Today")) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockTitle.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt similarity index 81% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockTitle.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt index 58e675f1fb..a71ec939db 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TransactionsBlockTitle.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/singlecurrency/TxHistoryTitle.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTxHistoryState.TxHistoryItemState /** * Transactions block title @@ -21,10 +21,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletContentItemStat * @param modifier modifier */ @Composable -internal fun TransactionsBlockTitle( - config: WalletContentItemState.SingleCurrencyItem.Title, - modifier: Modifier = Modifier, -) { +internal fun TxHistoryTitle(config: TxHistoryItemState.Title, modifier: Modifier = Modifier) { Row( modifier = modifier .background(TangemTheme.colors.background.primary) @@ -62,9 +59,7 @@ internal fun TransactionsBlockTitle( @Composable private fun Preview_TransactionsBlockTitle_Light() { TangemTheme(isDark = false) { - TransactionsBlockTitle( - config = WalletContentItemState.SingleCurrencyItem.Title(onExploreClick = {}), - ) + TxHistoryTitle(config = TxHistoryItemState.Title(onExploreClick = {})) } } @@ -72,8 +67,6 @@ private fun Preview_TransactionsBlockTitle_Light() { @Composable private fun Preview_TransactionsBlockTitle_Dark() { TangemTheme(isDark = true) { - TransactionsBlockTitle( - config = WalletContentItemState.SingleCurrencyItem.Title(onExploreClick = {}), - ) + TxHistoryTitle(config = TxHistoryItemState.Title(onExploreClick = {})) } } \ No newline at end of file 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/CryptoCurrencyStatusToTokenItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt new file mode 100644 index 0000000000..75084dad74 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/CryptoCurrencyStatusToTokenItemConverter.kt @@ -0,0 +1,106 @@ +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.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +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 CryptoCurrencyStatusToTokenItemConverter( + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter { + + private val CryptoCurrencyStatus.networkIconResId: Int? + @DrawableRes get() { + // TODO: [REDACTED_JIRA] + return if (currency is CryptoCurrency.Token) null else R.drawable.img_eth_22 + } + + private val CryptoCurrencyStatus.tokenIconResId: Int + @DrawableRes get() { + // TODO: [REDACTED_JIRA] + return R.drawable.img_eth_22 + } + + override fun convert(value: CryptoCurrencyStatus): TokenItemState { + return when (value.value) { + is CryptoCurrencyStatus.Loading -> TokenItemState.Loading + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + -> value.mapToTokenItemState() + // TODO: Add other token item states, currently not designed + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAccount, + is CryptoCurrencyStatus.Unreachable, + -> value.mapToUnreachableTokenItemState() + } + } + + private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content { + return TokenItemState.Content( + id = currency.id.value, + name = currency.name, + tokenIconUrl = currency.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 CryptoCurrencyStatus.getFormattedAmount(): String { + val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + } + + private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { + val fiatAmount = value.fiatAmount ?: return UNKNOWN_AMOUNT_SIGN + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, fiatCurrencyCode, fiatCurrencySymbol) + } + + private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState() = TokenItemState.Unreachable( + id = currency.id.value, + name = currency.name, + tokenIconUrl = currency.iconUrl, + tokenIconResId = this.tokenIconResId, + networkIconResId = this.networkIconResId, + ) + + private fun CryptoCurrencyStatus.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/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..b197bf76e8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/FiatBalanceToWalletCardConverter.kt @@ -0,0 +1,48 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.common.Provider +import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletCardState +import com.tangem.utils.converter.Converter + +internal class FiatBalanceToWalletCardConverter( + private val currentState: WalletCardState, + private val cardTypeResolverProvider: Provider, + private val isLockedState: Boolean, + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, +) : Converter { + + override fun convert(value: TokenList.FiatBalance): WalletCardState { + val additionalInfo = WalletAdditionalInfoFactory.resolve( + cardTypesResolver = cardTypeResolverProvider(), + isLocked = isLockedState, + ) + 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..067db48037 --- /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.content.WalletTokensListState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal object LoadingItemsProvider { + + fun getLoadingMultiCurrencyTokens(): ImmutableList { + return buildList(capacity = 5) { + add(WalletTokensListState.TokensListItemState.Token(state = TokenItemState.Loading)) + }.toImmutableList() + } +} \ 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..b93109b6f6 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListErrorToWalletStateConverter.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.common.Provider +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class TokenListErrorToWalletStateConverter( + private val currentStateProvider: Provider, +) : Converter { + + // TODO: [REDACTED_JIRA] + override fun convert(value: TokenListError): WalletStateHolder { + val state = currentStateProvider() + return WalletStateHolder.MultiCurrencyContent( + onBackClick = state.onBackClick, + topBarConfig = state.topBarConfig, + walletsListConfig = state.walletsListConfig, + pullToRefreshConfig = state.pullToRefreshConfig, + notifications = state.notifications, + bottomSheet = state.bottomSheet, + tokensListState = WalletTokensListState.Content(items = persistentListOf(), onOrganizeTokensClick = null), + ) + } +} \ 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..114cd724f9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToContentItemsConverter.kt @@ -0,0 +1,72 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState.TokensListItemState +import com.tangem.feature.wallet.presentation.wallet.utils.LoadingItemsProvider.getLoadingMultiCurrencyTokens +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.persistentListOf + +internal class TokenListToContentItemsConverter( + isWalletContentHidden: Boolean, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + private val clickIntents: WalletClickIntents, +) : Converter { + + private val tokenStatusConverter = CryptoCurrencyStatusToTokenItemConverter( + isWalletContentHidden = isWalletContentHidden, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + ) + + override fun convert(value: TokenList): WalletTokensListState { + return WalletTokensListState.Content( + items = when (value) { + is TokenList.GroupedByNetwork -> value.mapToMultiCurrencyItems() + is TokenList.Ungrouped -> value.mapToMultiCurrencyItems() + is TokenList.NotInitialized -> getLoadingMultiCurrencyTokens() + }, + onOrganizeTokensClick = if (value.totalFiatBalance is TokenList.FiatBalance.Loaded) { + clickIntents::onOrganizeTokensClick + } else { + null + }, + ) + } + + 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 currencies.fold(initial = persistentListOf()) { acc, token -> + acc.mutate { it.addToken(token) } + } + } + + private fun MutableList.addGroup(group: NetworkGroup): List { + this.add(TokensListItemState.NetworkGroupTitle(group.network.name)) + + group.currencies.forEach { token -> + this.addToken(token) + } + + return this + } + + private fun MutableList.addToken(token: CryptoCurrencyStatus): List { + val tokenItemState = tokenStatusConverter.convert(token) + + this.add(TokensListItemState.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..06ec3d5b8a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/TokenListToWalletStateConverter.kt @@ -0,0 +1,89 @@ +package com.tangem.feature.wallet.presentation.wallet.utils + +import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +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.WalletsListConfig +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.utils.TokenListToWalletStateConverter.TokensListModel +import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class TokenListToWalletStateConverter( + private val currentStateProvider: Provider, + private val cardTypeResolverProvider: Provider, + private val isWalletContentHidden: Boolean, + private val fiatCurrencyCode: String, + private val fiatCurrencySymbol: String, + clickIntents: WalletClickIntents, +) : Converter { + + private val tokenListToContentConverter = TokenListToContentItemsConverter( + isWalletContentHidden = isWalletContentHidden, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + clickIntents = clickIntents, + ) + + override fun convert(value: TokensListModel): WalletStateHolder { + val state = currentStateProvider() + return state + .updateWithTokenList(tokenList = value.tokenList) + .copySealed( + walletsListConfig = state.updateSelectedWallet(value.tokenList.totalFiatBalance), + pullToRefreshConfig = if (value.isRefreshing) { + state.pullToRefreshConfig.copy(isRefreshing = state.getRefreshingStatus()) + } else { + state.pullToRefreshConfig + }, + ) + } + + private fun WalletStateHolder.updateWithTokenList(tokenList: TokenList): MultiCurrencyContent { + return MultiCurrencyContent( + onBackClick = onBackClick, + topBarConfig = topBarConfig, + walletsListConfig = walletsListConfig, + pullToRefreshConfig = pullToRefreshConfig, + notifications = notifications, + bottomSheet = bottomSheet, + tokensListState = tokenListToContentConverter.convert(value = tokenList), + ) + } + + private fun WalletStateHolder.updateSelectedWallet(fiatBalance: TokenList.FiatBalance): WalletsListConfig { + val selectedWalletIndex = walletsListConfig.selectedWalletIndex + val selectedWalletCard = walletsListConfig.wallets[selectedWalletIndex] + val converter = FiatBalanceToWalletCardConverter( + currentState = selectedWalletCard, + isLockedState = this is WalletStateHolder.UnlockWalletContent, + cardTypeResolverProvider = cardTypeResolverProvider, + isWalletContentHidden = isWalletContentHidden, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + ) + + return walletsListConfig.copy( + wallets = walletsListConfig.wallets + .toPersistentList() + .set(index = selectedWalletIndex, element = converter.convert(fiatBalance)), + ) + } + + private fun WalletStateHolder.getRefreshingStatus(): Boolean { + return if (this is MultiCurrencyContent) { + tokensListState.items.any { tokensListItemState -> + tokensListItemState is WalletTokensListState.TokensListItemState.Token && + tokensListItemState.state is TokenItemState.Loading + } + } else { + false + } + } + + data class TokensListModel(val tokenList: TokenList, val isRefreshing: Boolean) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt new file mode 100644 index 0000000000..94c0e30ade --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/JobHolder.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import kotlinx.coroutines.Job + +/** + * Job holder. It is automatically finished old job if new one is started + * +[REDACTED_AUTHOR] + */ +internal class JobHolder { + + private var job: Job? = null + + /** Update current job */ + fun update(job: Job) { + this.job?.cancel() + this.job = job + } +} + +internal fun Job.saveIn(jobHolder: JobHolder) = jobHolder.update(job = this) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt new file mode 100644 index 0000000000..503b697419 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -0,0 +1,34 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +internal interface WalletClickIntents { + + fun onBackClick() + + fun onScanCardClick() + + fun onDetailsClick() + + fun onBackupCardClick() + + fun onCriticalWarningAlreadySignedHashesClick() + + fun onCloseWarningAlreadySignedHashesClick() + + fun onLikeTangemAppClick() + + fun onRateTheAppClick() + + fun onShareClick() + + fun onWalletChange(index: Int) + + fun onRefreshSwipe() + + fun onOrganizeTokensClick() + + fun onBuyClick() + + fun onReloadClick() + + fun onExploreClick() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt new file mode 100644 index 0000000000..aace48a09d --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -0,0 +1,137 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels + +import com.tangem.common.Provider +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.NetworkGroup +import com.tangem.domain.tokens.model.TokenList +import com.tangem.feature.wallet.presentation.common.state.TokenItemState +import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateHolder +import com.tangem.feature.wallet.presentation.wallet.state.content.WalletTokensListState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * Wallet notifications list factory + * + * @property wasCardScannedCallback callback that check if card was scanned + * @property isUserAlreadyRateAppCallback callback that check if card is user already rate app + * @property isDemoCardCallback callback that check if card is demo + * @property clickIntents screen click intents + * +[REDACTED_AUTHOR] + */ +internal class WalletNotificationsListFactory( + private val currentStateProvider: Provider, + private val wasCardScannedCallback: suspend (String) -> Boolean, + private val isUserAlreadyRateAppCallback: suspend () -> Boolean, + private val isDemoCardCallback: (String) -> Boolean, + private val clickIntents: WalletClickIntents, +) { + + fun create(cardTypesResolver: CardTypesResolver, tokenList: TokenList?): Flow> { + // TODO: [REDACTED_JIRA] order + return flow { + emit( + buildList { + if (cardTypesResolver.isTestCard()) { + add(element = WalletNotification.TestCard) + return@buildList + } + + addRemainingSignaturesLeftNotifications(cardTypesResolver) + + val isDemo = isDemoCardCallback(cardTypesResolver.getCardId()) + if (!cardTypesResolver.isReleaseFirmwareType()) { + add(element = WalletNotification.DevCard) + } else { + addReleaseSpecialNotifications(cardTypesResolver = cardTypesResolver, isDemo = isDemo) + } + + if (isDemo) { + add(element = WalletNotification.DemoCard) + } + + if (hasUnreachableNetworks()) { + add(element = WalletNotification.UnreachableNetworks) + } + + if (!cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup()) { + add(element = WalletNotification.BackupCard(onClick = clickIntents::onBackupCardClick)) + } + + if (tokenList != null && tokenList.hasMissedDerivations()) { + add(element = WalletNotification.ScanCard(onClick = clickIntents::onScanCardClick)) + } + + if (isUserAlreadyRateAppCallback()) { + add(element = WalletNotification.LikeTangemApp(onClick = clickIntents::onLikeTangemAppClick)) + } + }.toImmutableList(), + ) + } + } + + private fun MutableList.addRemainingSignaturesLeftNotifications( + cardTypesResolver: CardTypesResolver, + ) { + val remainingSignatures = cardTypesResolver.getRemainingSignatures() + if (remainingSignatures != null && remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT) { + add(element = WalletNotification.RemainingSignaturesLeft(remainingSignatures)) + } + } + + private suspend fun MutableList.addReleaseSpecialNotifications( + cardTypesResolver: CardTypesResolver, + isDemo: Boolean, + ) { + if (!wasCardScannedCallback(cardTypesResolver.getCardId()) && cardTypesResolver.isMultiwalletAllowed() && + !isDemo + ) { + if (cardTypesResolver.isBackupForbidden() && cardTypesResolver.hasWalletSignedHashes()) { + add( + element = WalletNotification.CriticalWarningAlreadySignedHashes( + onClick = clickIntents::onCriticalWarningAlreadySignedHashesClick, + ), + ) + } else if (cardTypesResolver.hasWalletSignedHashes()) { + add( + element = WalletNotification.WarningAlreadySignedHashes( + onClick = clickIntents::onCloseWarningAlreadySignedHashesClick, + ), + ) + } + } + + if (cardTypesResolver.isAttestationFailed()) { + add(element = WalletNotification.CardVerificationFailed) + } + } + + private fun hasUnreachableNetworks(): Boolean { + val isUnreachableState = { item: WalletTokensListState.TokensListItemState -> + (item as? WalletTokensListState.TokensListItemState.Token)?.state is TokenItemState.Unreachable + } + + return currentStateProvider().let { state -> + state is WalletStateHolder.MultiCurrencyContent && state.tokensListState.items.any(isUnreachableState) + } + } + + private fun TokenList.hasMissedDerivations(): Boolean { + val statuses = when (this) { + is TokenList.GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies).map(CryptoCurrencyStatus::value) + is TokenList.Ungrouped -> currencies.map(CryptoCurrencyStatus::value) + TokenList.NotInitialized -> emptyList() + } + + return statuses.any { it is CryptoCurrencyStatus.MissedDerivation } + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ 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..4c1275a2bd 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,36 @@ 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 androidx.paging.cachedIn +import com.tangem.blockchain.common.DerivationStyle +import com.tangem.common.Provider +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess +import com.tangem.domain.card.* +import com.tangem.domain.common.CardTypesResolver +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.tokens.models.Network +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.userwallets.UserWalletBuilder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase 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.factory.WalletStateFactory +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject import kotlin.properties.Delegates @@ -18,41 +39,242 @@ import kotlin.properties.Delegates * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") @HiltViewModel -internal class WalletViewModel @Inject constructor() : ViewModel() { +internal class WalletViewModel @Inject constructor( + private val getWalletsUseCase: GetWalletsUseCase, + private val saveWalletUseCase: SaveWalletUseCase, + private val getBiometricsStatusUseCase: GetBiometricsStatusUseCase, + private val setAccessCodeRequestPolicyUseCase: SetAccessCodeRequestPolicyUseCase, + private val getAccessCodeSavingStatusUseCase: GetAccessCodeSavingStatusUseCase, + private val getTokenListUseCase: GetTokenListUseCase, + private val getCardWasScannedUseCase: GetCardWasScannedUseCase, + private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase, + private val isDemoCardUseCase: IsDemoCardUseCase, + private val scanCardProcessor: ScanCardProcessor, + private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val getExploreUrlUseCase: GetExploreUrlUseCase, + private val dispatchers: CoroutineDispatcherProvider, +) : ViewModel(), DefaultLifecycleObserver, WalletClickIntents { /** 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 notificationsListFactory = WalletNotificationsListFactory( + currentStateProvider = Provider { uiState }, + wasCardScannedCallback = getCardWasScannedUseCase::invoke, + isUserAlreadyRateAppCallback = isUserAlreadyRateAppUseCase::invoke, + isDemoCardCallback = isDemoCardUseCase::invoke, + clickIntents = this, ) - // TODO: [REDACTED_TASK_KEY] Use production data instead of WalletPreviewData - private fun selectWallet(index: Int) { + private val stateFactory = WalletStateFactory( + currentStateProvider = Provider { uiState }, + currentCardTypeResolverProvider = Provider { + getCardTypeResolver(index = uiState.walletsListConfig.selectedWalletIndex) + }, + clickIntents = this, + ) + + /** Screen state */ + var uiState: WalletStateHolder by mutableStateOf(stateFactory.getInitialState()) + private set + + private var wallets: List by Delegates.notNull() + + private val tokensJobHolder = JobHolder() + private val notificationsJobHolder = JobHolder() + + override fun onCreate(owner: LifecycleOwner) { + getWalletsUseCase() + .flowWithLifecycle(owner.lifecycle) + .distinctUntilChanged() + .onEach { wallets -> + if (wallets.isEmpty()) return@onEach + this.wallets = wallets + + uiState = stateFactory.getSkeletonState(wallets = wallets) + + updateContentItems(index = 0) + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + } + + private fun updateContentItems(index: Int, isRefreshing: Boolean = false) { + val cardTypeResolver = getCardTypeResolver(index) + if (cardTypeResolver.isMultiwalletAllowed()) { + updateByTokensList(index, isRefreshing) + } else { + updateByTxHistory(index) + } + } + + private fun updateByTokensList(index: Int, isRefreshing: Boolean) { + getTokenListUseCase(userWalletId = uiState.walletsListConfig.wallets[index].id) + .distinctUntilChanged() + .onEach { tokenListEither -> + uiState = stateFactory.getStateByTokensList( + tokenListEither = tokenListEither, + isRefreshing = isRefreshing, + ) + + tokenListEither.onRight { updateNotifications(index = index, tokenList = it) } + } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(tokensJobHolder) + } + + private fun updateByTxHistory(index: Int) { + viewModelScope.launch(dispatchers.io) { + val blockchain = getWallet(index).scanResponse.cardTypesResolver.getBlockchain() + + val txHistoryItemsCountEither = txHistoryItemsCountUseCase( + networkId = blockchain.id, + derivationPath = requireNotNull(blockchain.derivationPath(style = DerivationStyle.LEGACY)).rawPath, + ) + + uiState = stateFactory.getLoadingTxHistoryState(itemsCountEither = txHistoryItemsCountEither) + + txHistoryItemsCountEither.onRight { updateTxHistory(networkId = blockchain.id) } + updateNotifications(index) + } + } + + private fun updateTxHistory(networkId: String) { + uiState = stateFactory.getLoadedTxHistoryState( + txHistoryEither = txHistoryItemsUseCase(networkId = networkId).map { it.cachedIn(viewModelScope) }, + ) + } + + private fun updateNotifications(index: Int, tokenList: TokenList? = null) { + notificationsListFactory.create( + cardTypesResolver = getCardTypeResolver(index = index), + tokenList = tokenList, + ) + .distinctUntilChanged() + .onEach { uiState = stateFactory.getStateByNotifications(notifications = it) } + .flowOn(dispatchers.io) + .launchIn(viewModelScope) + .saveIn(jobHolder = notificationsJobHolder) + } + + private fun getWallet(index: Int): UserWallet { + return requireNotNull( + value = wallets.getOrNull(index), + lazyMessage = { "WalletsList doesn't contain element with index = $index" }, + ) + } + + private fun getCardTypeResolver(index: Int): CardTypesResolver = getWallet(index).scanResponse.cardTypesResolver + + override fun onBackClick() = router.popBackStack() + + override 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) + } + } + } + + override fun onDetailsClick() = router.openDetailsScreen() + + override fun onBackupCardClick() = router.openOnboardingScreen() + + override fun onCriticalWarningAlreadySignedHashesClick() { + uiState = stateFactory.getStateWithOpenBottomSheet( + content = WalletBottomSheetConfig.BottomSheetContentConfig.CriticalWarningAlreadySignedHashes( + onOkClick = {}, + onCancelClick = {}, + ), + ) + } + + override fun onCloseWarningAlreadySignedHashesClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onLikeTangemAppClick() { + uiState = stateFactory.getStateWithOpenBottomSheet( + content = WalletBottomSheetConfig.BottomSheetContentConfig.LikeTangemApp( + onRateTheAppClick = ::onRateTheAppClick, + onShareClick = ::onShareClick, + ), + ) + } + + override fun onRateTheAppClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onShareClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onWalletChange(index: Int) { if (uiState.walletsListConfig.selectedWalletIndex == index) return - Log.i("WalletViewModel", "selectWallet: $index") + uiState = stateFactory.getStateAfterWalletChanging(index = index) - uiState = if (index % 2 == 0) { - WalletPreviewData.multicurrencyWalletScreenState.copy( - walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), - ) - } else { - WalletPreviewData.singleWalletScreenState.copy( - walletsListConfig = uiState.walletsListConfig.copy(selectedWalletIndex = index), + updateContentItems(index = index) + } + + override fun onRefreshSwipe() { + uiState = stateFactory.getStateAfterContentRefreshing() + updateContentItems(index = uiState.walletsListConfig.selectedWalletIndex, isRefreshing = true) + } + + override fun onOrganizeTokensClick() { + val index = uiState.walletsListConfig.selectedWalletIndex + val walletId = uiState.walletsListConfig.wallets[index].id + + router.openOrganizeTokensScreen(walletId) + } + + override fun onBuyClick() { + // TODO: [REDACTED_JIRA] + } + + override fun onReloadClick() { + uiState = stateFactory.getStateAfterContentRefreshing() + updateByTxHistory(index = uiState.walletsListConfig.selectedWalletIndex) + } + + override fun onExploreClick() { + viewModelScope.launch(dispatchers.io) { + val wallet = getWallet(uiState.walletsListConfig.selectedWalletIndex) + router.openTxHistoryWebsite( + url = getExploreUrlUseCase( + userWalletId = wallet.walletId, + networkId = Network.ID( + value = wallet.scanResponse.cardTypesResolver.getBlockchain().id, + ), + ), ) } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 0482375c9e..edb6ad70d8 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-293" +tangemBlockchainSdk = "develop-297" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_4.9-277" +tangemCardSdk = "develop-280" #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/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt new file mode 100644 index 0000000000..de50d8b4ee --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TxHistoryManager.kt @@ -0,0 +1,18 @@ +package com.tangem.lib.crypto + +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryItem +import com.tangem.lib.crypto.models.txhistory.ProxyTransactionHistoryState + +interface TxHistoryManager { + + @Throws(IllegalStateException::class) + suspend fun checkTxHistoryState(networkId: String, derivationPath: String?): ProxyTransactionHistoryState + + @Throws(IllegalStateException::class) + suspend fun getTxHistoryItems( + networkId: String, + derivationPath: String?, + page: Int, + pageSize: Int, + ): List +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt new file mode 100644 index 0000000000..387a7518e6 --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryItem.kt @@ -0,0 +1,21 @@ +package com.tangem.lib.crypto.models.txhistory + +import com.tangem.lib.crypto.models.ProxyAmount + +data class ProxyTransactionHistoryItem( + val txHash: String, + val timestamp: Long, + val direction: TransactionDirection, + val status: ProxyTransactionStatus, + val type: TransactionType, + val amount: ProxyAmount, +) { + sealed interface TransactionDirection { + data class Incoming(val from: String) : TransactionDirection + data class Outgoing(val to: String) : TransactionDirection + } + + sealed interface TransactionType { + object Transfer : TransactionType + } +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt new file mode 100644 index 0000000000..ddeeba4d3f --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionHistoryState.kt @@ -0,0 +1,15 @@ +package com.tangem.lib.crypto.models.txhistory + +sealed class ProxyTransactionHistoryState { + + sealed class Success : ProxyTransactionHistoryState() { + object Empty : Success() + data class HasTransactions(val txCount: Int) : Success() + } + + sealed class Failed : ProxyTransactionHistoryState() { + data class FetchError(val exception: Exception) : Failed() + } + + object NotImplemented : ProxyTransactionHistoryState() +} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt new file mode 100644 index 0000000000..2e5ffd5aaf --- /dev/null +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/txhistory/ProxyTransactionStatus.kt @@ -0,0 +1,3 @@ +package com.tangem.lib.crypto.models.txhistory + +enum class ProxyTransactionStatus { Confirmed, Unconfirmed } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 523476db95..7378b7b744 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,18 @@ pluginManagement { includeBuild("plugins/configuration") } +val properties = java.util.Properties() +val propertiesFile = File(rootDir.absolutePath, "local.properties") +if (propertiesFile.exists()) { + properties.load(propertiesFile.inputStream()) + println("Authenticating user: " + properties.getProperty("gpr.user")) +} else { + println( + "local.properties not found, please create it next to build.gradle and set gpr.user and gpr.key (Create a GitHub package read only + non expiration token at https://github.com/settings/tokens)\n" + + "Or set GITHUB_ACTOR and GITHUB_TOKEN environment variables" + ) +} + dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) @@ -17,6 +29,15 @@ dependencyResolutionManagement { mavenLocal() jcenter() // unable to replace with mavenCentral() due to rekotlin and com.otaliastudios:cameraview maven("https://nexus.tangem-tech.com/repository/maven-releases/") + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/blockchain-sdk-kotlin") + credentials { + println(System.getenv("GITHUB_ACTOR")) + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + } maven("https://jitpack.io") maven("https://zendesk.jfrog.io/zendesk/repo") } @@ -35,6 +56,7 @@ include(":common") // region Core modules include(":core:analytics") +include(":core:analytics:models") include(":core:datasource") include(":core:featuretoggles") include(":core:navigation") @@ -76,12 +98,22 @@ include(":features:learn2earn:impl") include(":domain:models") include(":domain:legacy") -include(":domain:core") include(":domain:card") +include(":domain:core") +include(":domain:demo") +include(":domain:settings") +include(":domain:tokens") +include(":domain:tokens:models") include(":domain:wallets") include(":domain:wallets:models") +include(":domain:txhistory") // endregion Domain modules // region Data modules +include(":data:common") +include(":data:card") +include(":data:tokens") include(":data:source:preferences") +include(":data:settings") +include(":data:txhistory") // endregion Data modules \ No newline at end of file