diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e5a1e6a705..3312184dff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -100,6 +100,7 @@ dependencies { implementation(projects.domain.qrScanning.models) implementation(projects.domain.staking) implementation(projects.domain.walletConnect) + implementation(projects.domain.walletConnect.models) implementation(projects.domain.markets) implementation(projects.domain.manageTokens) implementation(projects.domain.nft) @@ -169,6 +170,8 @@ dependencies { implementation(projects.features.manageTokens.impl) implementation(projects.features.send.api) implementation(projects.features.send.impl) + implementation(projects.features.sendV2.api) + implementation(projects.features.sendV2.impl) implementation(projects.features.qrScanning.api) implementation(projects.features.qrScanning.impl) implementation(projects.features.staking.api) @@ -195,6 +198,8 @@ dependencies { implementation(projects.features.biometry.impl) implementation(projects.features.nft.api) implementation(projects.features.nft.impl) + implementation(projects.features.walletconnect.api) + implementation(projects.features.walletconnect.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 2b76d7c7eb..ee95e2ba40 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -3,7 +3,6 @@ package com.tangem.tap import androidx.hilt.work.HiltWorkerFactory import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory -import com.tangem.data.card.TransactionSignerFactory import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.filter.OneTimeEventFilter @@ -15,6 +14,7 @@ import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -32,13 +32,13 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles @@ -94,8 +94,6 @@ interface ApplicationEntryPoint { fun getSaveTwinsOnboardingShownUseCase(): SaveTwinsOnboardingShownUseCase - fun getWalletNameGenerateUseCase(): GenerateWalletNameUseCase - fun getCardRepository(): CardRepository fun getTangemSdkLogger(): TangemSdkLogger @@ -118,8 +116,6 @@ interface ApplicationEntryPoint { fun getTransactionSignerFactory(): TransactionSignerFactory - fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase - fun getOnrampFeatureToggles(): OnrampFeatureToggles fun getOnboardingV2FeatureToggles(): OnboardingV2FeatureToggles @@ -142,4 +138,8 @@ interface ApplicationEntryPoint { fun getUiMessageSender(): UiMessageSender fun getWorkerFactory(): HiltWorkerFactory + + fun getOnlineCardVerifier(): OnlineCardVerifier + + fun getUserWalletBuilderFactory(): UserWalletBuilder.Factory } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 32e3b46f66..256468df9e 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -8,6 +8,7 @@ import coil.ImageLoaderFactory import com.chuckerteam.chucker.api.ChuckerInterceptor import com.tangem.Log import com.tangem.TangemSdkLogger +import com.tangem.blockchain.common.ExceptionHandler import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.utils.ExcludedBlockchains @@ -46,15 +47,16 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.operations.attestation.OnlineCardVerifier 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.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.images.createCoilImageLoader @@ -146,9 +148,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase get() = entryPoint.getSaveTwinsOnboardingShownUseCase() - private val generateWalletNameUseCase: GenerateWalletNameUseCase - get() = entryPoint.getWalletNameGenerateUseCase() - private val cardRepository: CardRepository get() = entryPoint.getCardRepository() @@ -182,9 +181,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val transactionSignerFactory: TransactionSignerFactory get() = entryPoint.getTransactionSignerFactory() - private val getUserCountryUseCase: GetUserCountryUseCase - get() = entryPoint.getGetUserCountryCodeUseCase() - private val onrampFeatureToggles: OnrampFeatureToggles get() = entryPoint.getOnrampFeatureToggles() @@ -212,6 +208,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val uiMessageSender: UiMessageSender get() = entryPoint.getUiMessageSender() + private val blockchainExceptionHandler: BlockchainExceptionHandler + get() = entryPoint.getBlockchainExceptionHandler() + private val workerFactory: HiltWorkerFactory get() = entryPoint.getWorkerFactory() @@ -220,6 +219,12 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat .setWorkerFactory(workerFactory) .build() + private val onlineCardVerifier: OnlineCardVerifier + get() = entryPoint.getOnlineCardVerifier() + + private val userWalletBuilderFactory: UserWalletBuilder.Factory + get() = entryPoint.getUserWalletBuilderFactory() + // endregion override fun onCreate() { @@ -265,7 +270,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat } loadNativeLibraries() - // ExceptionHandler.append(blockchainExceptionHandler) // TODO [REDACTED_TASK_KEY] Send only to Firebase + ExceptionHandler.append(blockchainExceptionHandler) if (LogConfig.network.blockchainSdkNetwork) { BlockchainSdkRetrofitBuilder.interceptors = listOf( createNetworkLoggingInterceptor(), @@ -301,7 +306,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat generalUserWalletsListManager = generalUserWalletsListManager, wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, - generateWalletNameUseCase = generateWalletNameUseCase, cardRepository = cardRepository, settingsRepository = settingsRepository, blockchainSDKFactory = blockchainSDKFactory, @@ -312,7 +316,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat shareManager = shareManager, appRouter = appRouter, transactionSignerFactory = transactionSignerFactory, - getUserCountryUseCase = getUserCountryUseCase, onrampFeatureToggles = onrampFeatureToggles, environmentConfigStorage = environmentConfigStorage, onboardingV2FeatureToggles = onboardingV2FeatureToggles, @@ -322,6 +325,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat clipboardManager = clipboardManager, settingsManager = settingsManager, uiMessageSender = uiMessageSender, + onlineCardVerifier = onlineCardVerifier, + userWalletBuilderFactory = userWalletBuilderFactory, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt index 39291d12f6..29b584fa05 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt @@ -1,20 +1,20 @@ package com.tangem.tap.common.analytics import com.tangem.common.json.MoshiJsonConverter -import com.tangem.core.analytics.api.ErrorEventLogger +import com.tangem.core.analytics.api.ExceptionLogger import com.tangem.core.analytics.api.EventLogger import timber.log.Timber class AnalyticsEventsLogger( private val name: String, private val jsonConverter: MoshiJsonConverter, -) : EventLogger, ErrorEventLogger { +) : EventLogger, ExceptionLogger { override fun logEvent(event: String, params: Map) { Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params))) } - override fun logErrorEvent(error: Throwable, params: Map) { + override fun logException(error: Throwable, params: Map) { Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params))) } } 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/BlockchainApiExceptionEvent.kt similarity index 92% rename from app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt rename to app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt index 385ab3e517..3e687ae5fd 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainExceptionEvent.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/BlockchainApiExceptionEvent.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common.analytics.events import com.tangem.core.analytics.models.AnalyticsEvent -class BlockchainExceptionEvent( +class BlockchainApiExceptionEvent( selectedHost: String, exceptionHost: String, error: String, diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt index 46c647b06f..c4345c597f 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt @@ -6,7 +6,6 @@ internal sealed class Push(event: String) : AnalyticsEvent( category = "Push", event = event, params = emptyMap(), - error = null, ) { data object PushNotificationOpened : Push(event = "Push Notification Opened") 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 98b0289289..4234256e8c 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 @@ -9,8 +9,7 @@ sealed class Settings( category: String = "Settings", event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent(category, event, params, error) { +) : AnalyticsEvent(category, event, params) { class ScreenOpened : Settings(event = "Settings Screen Opened") class ButtonStartWalletConnectSession : Settings(event = "Button - Start Wallet Connect Session") @@ -19,16 +18,14 @@ sealed class Settings( sealed class CardSettings( event: String, params: Map = mapOf(), - error: Throwable? = null, - ) : Settings("Settings / Card Settings", event, params, error) { + ) : Settings("Settings / Card Settings", event, params) { class ButtonFactoryReset : CardSettings("Button - Factory Reset") - class FactoryResetFinished(cardsCount: Int? = null, error: Throwable? = null) : CardSettings( + class FactoryResetFinished(cardsCount: Int? = null) : CardSettings( event = "Factory Reset Finished", params = buildMap { cardsCount?.let { put("Cards Count", "$it") } }, - error = error, ) class FactoryResetCanceled(cardsCount: Int) : CardSettings( @@ -44,10 +41,9 @@ sealed class Settings( params = mapOf("Type" to type.value), ) - class SecurityModeChanged(mode: AnalyticsParam.SecurityMode, error: Throwable? = null) : CardSettings( + class SecurityModeChanged(mode: AnalyticsParam.SecurityMode) : CardSettings( event = "Security Mode Changed", params = mapOf("Mode" to mode.value), - error = error, ) class AccessCodeRecoveryChanged(status: AnalyticsParam.AccessCodeRecoveryStatus) : CardSettings( 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 212969481a..a9a52c5e25 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 @@ -8,8 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent sealed class SignIn( event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent("Sign In", event, params, error) { +) : AnalyticsEvent("Sign In", event, params) { class ScreenOpened : SignIn(event = "Sign In Screen Opened") diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/TangemSdkErrorEvent.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/TangemSdkErrorEvent.kt new file mode 100644 index 0000000000..bfd7f4bcc9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/TangemSdkErrorEvent.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.common.analytics.events + +import com.tangem.common.core.TangemSdkError +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter + +class TangemSdkErrorEvent( + val exception: TangemSdkError, +) : AnalyticsEvent( + category = "TangemSdk", + event = "Tangem Sdk Error", + params = errorConverter.convert(exception), +) { + + companion object { + private val errorConverter = AnalyticsErrorConverter() + } +} \ No newline at end of file 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 bf0bdc539a..9f6214a78a 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 @@ -10,8 +10,7 @@ sealed class Token( category: String, event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent(category, event, params, error) { +) : AnalyticsEvent(category, event, params) { sealed class Receive( event: String, 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 1725eabaaa..a6c39d4f3c 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 @@ -9,8 +9,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.O internal sealed class WalletConnect( event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent("Wallet Connect", event, params, error) { +) : AnalyticsEvent("Wallet Connect", event, params) { class ScreenOpened : WalletConnect(event = "WC Screen Opened") class NewSessionInitiated(source: SourceType) : WalletConnect( 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 index b227a5f1f1..a1931b993e 100644 --- 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 @@ -1,16 +1,16 @@ 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 com.tangem.core.analytics.api.AnalyticsErrorHandler +import com.tangem.tap.common.analytics.events.BlockchainApiExceptionEvent import javax.inject.Inject class BlockchainExceptionHandler @Inject constructor( - private val analyticsHandler: AnalyticsEventHandler, + private val analyticsErrorHandler: AnalyticsErrorHandler, ) : ExceptionHandlerOutput { override fun handleApiSwitch(currentHost: String, nextHost: String, message: String) { - analyticsHandler.send( - BlockchainExceptionEvent( + analyticsErrorHandler.sendErrorEvent( + BlockchainApiExceptionEvent( selectedHost = nextHost, exceptionHost = currentHost, error = message, 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 10b399382c..298874dcef 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,16 +1,18 @@ package com.tangem.tap.common.analytics.handlers.firebase -import com.google.firebase.analytics.FirebaseAnalytics +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsHandler -import com.tangem.core.analytics.api.ErrorEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter -import com.tangem.tap.common.analytics.events.Shop class FirebaseAnalyticsHandler( private val client: FirebaseAnalyticsClient, -) : AnalyticsHandler, ErrorEventHandler { +) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler { + + private val errorConverter = AnalyticsErrorConverter() override fun id(): String = ID @@ -18,30 +20,17 @@ class FirebaseAnalyticsHandler( client.logEvent(eventId, params) } - override fun send(event: AnalyticsEvent) { - val error = event.error - when { - error != null -> { - val errorConverter = AnalyticsErrorConverter() - if (!errorConverter.canBeHandled(error)) return + override fun sendException(event: ExceptionAnalyticsEvent) { + if (!errorConverter.canBeHandled(event.exception)) return - val errorParams = errorConverter.convert(error).toMutableMap() - errorParams["Category"] = event.category - errorParams["Event"] = event.event - errorParams.putAll(event.params) - send(error, errorParams) - } - event is Shop.Purchased -> { - send(FirebaseAnalytics.Event.PURCHASE, event.params) - } - else -> { - super.send(event) - } - } + val errorParams = errorConverter.convert(event.exception).toMutableMap() + errorParams.putAll(event.params) + + client.logException(event.exception, event.params) } - override fun send(error: Throwable, params: Map) { - client.logErrorEvent(error, params) + override fun sendErrorEvent(event: AnalyticsEvent) { + send(event) } companion object { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt index 7e4e5085bc..06ccf50709 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt @@ -4,14 +4,15 @@ import android.os.Bundle import androidx.core.os.bundleOf import com.google.firebase.analytics.ktx.analytics import com.google.firebase.crashlytics.ktx.crashlytics +import com.google.firebase.crashlytics.recordException import com.google.firebase.ktx.Firebase -import com.tangem.core.analytics.api.ErrorEventLogger +import com.tangem.core.analytics.api.ExceptionLogger import com.tangem.core.analytics.api.EventLogger /** [REDACTED_AUTHOR] */ -interface FirebaseAnalyticsClient : EventLogger, ErrorEventLogger +interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger internal class FirebaseClient : FirebaseAnalyticsClient { @@ -27,11 +28,11 @@ internal class FirebaseClient : FirebaseAnalyticsClient { ) } - override fun logErrorEvent(error: Throwable, params: Map) { - eventConverter.convertEventParams(params) - .forEach { fbCrashlytics.setCustomKey(it.key, it.value) } - - fbCrashlytics.recordException(error) + override fun logException(error: Throwable, params: Map) { + fbCrashlytics.recordException(error) { + eventConverter.convertEventParams(params) + .forEach { key(it.key, it.value) } + } } private fun Map.toBundle(): Bundle = bundleOf(*this.toList().toTypedArray()) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt index e460f9dbf9..4b818bdf51 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt @@ -16,7 +16,7 @@ internal class FirebaseLogClient( logger.logEvent(event, params) } - override fun logErrorEvent(error: Throwable, params: Map) { - logger.logErrorEvent(error, params) + override fun logException(error: Throwable, params: Map) { + logger.logException(error, params) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt index a1751aff85..4c9bbe446d 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultCardSdkProvider.kt @@ -10,11 +10,13 @@ import com.tangem.common.authentication.AuthenticationManager import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.Config import com.tangem.common.services.secure.SecureStorage -import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.crypto.bip39.Wordlist import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.sdk.DefaultSessionViewDelegate +import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles import com.tangem.sdk.extensions.* import com.tangem.sdk.nfc.AndroidNfcAvailabilityProvider import com.tangem.sdk.nfc.NfcManager @@ -32,8 +34,9 @@ import javax.inject.Singleton */ @Singleton internal class DefaultCardSdkProvider @Inject constructor( - private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val dispatchers: CoroutineDispatcherProvider, + private val cardSdkFeatureToggles: CardSdkFeatureToggles, ) : CardSdkProvider, CardSdkOwner { private val observer = Observer() @@ -46,7 +49,12 @@ internal class DefaultCardSdkProvider @Inject constructor( override fun register(activity: FragmentActivity) = runBlocking(dispatchers.mainImmediate) { if (activity.isDestroyed || activity.isFinishing || activity.isChangingConfigurations) { val message = "Tangem SDK owner registration skipped: activity is destroyed or finishing" - analyticsEventHandler.send(TangemSdkWarningEvent(message)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException(message), + params = errorParams, + ), + ) Log.info { message } return@runBlocking } @@ -64,14 +72,24 @@ internal class DefaultCardSdkProvider @Inject constructor( private fun tryToRegisterWithForegroundActivity(): TangemSdk = runBlocking(dispatchers.mainImmediate) { val warning = "Tangem SDK holder is null, trying to recreate it with foreground activity" - analyticsEventHandler.send(TangemSdkWarningEvent(warning)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException(warning), + params = errorParams, + ), + ) Log.warning { warning } val activity = foregroundActivityObserver.foregroundActivity if (activity == null) { val error = "Tangem SDK holder is null and foreground activity is null" - analyticsEventHandler.send(TangemSdkWarningEvent(error)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException(error), + params = errorParams, + ), + ) Log.error { error } error(error) } @@ -82,7 +100,12 @@ internal class DefaultCardSdkProvider @Inject constructor( if (sdk == null) { val error = "Tangem SDK is null after re-registering with foreground activity" - analyticsEventHandler.send(TangemSdkWarningEvent(error)) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException(error), + params = errorParams, + ), + ) Log.error { error } error(error) } @@ -108,7 +131,9 @@ internal class DefaultCardSdkProvider @Inject constructor( authenticationManager = authenticationManager, keystoreManager = keystoreManager, wordlist = Wordlist.getWordlist(activity), - config = config, + config = config.apply { + isNewOnlineAttestationEnabled = cardSdkFeatureToggles.isNewAttestationEnabled + }, ) holder = Holder( @@ -170,5 +195,10 @@ internal class DefaultCardSdkProvider @Inject constructor( ), ), ) + + val errorParams = mapOf( + "Category" to "Tangem SDK", + "Event" to "Warning", + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt b/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt deleted file mode 100644 index 1393bfbf57..0000000000 --- a/app/src/main/java/com/tangem/tap/data/TangemSdkWarningEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.data - -import com.tangem.core.analytics.models.AnalyticsEvent - -internal class TangemSdkWarningEvent(message: String) : AnalyticsEvent( - category = "Tangem SDK", - event = "Warning", - error = IllegalStateException(message), -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt new file mode 100644 index 0000000000..039fa951ff --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.nft.FetchNFTCollectionsUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +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 NFTDomainModule { + + @Provides + @Singleton + fun providesGetNFTCollectionsUseCase( + currenciesRepository: CurrenciesRepository, + nftRepository: NFTRepository, + ): GetNFTCollectionsUseCase { + return GetNFTCollectionsUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) + } + + @Provides + @Singleton + fun providesFetchNFTCollectionsUseCase( + currenciesRepository: CurrenciesRepository, + nftRepository: NFTRepository, + ): FetchNFTCollectionsUseCase { + return FetchNFTCollectionsUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 87afb55912..77bd0c1526 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -1,5 +1,6 @@ package com.tangem.tap.di.domain +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository import com.tangem.domain.onramp.* import com.tangem.domain.onramp.repositories.HotCryptoRepository import com.tangem.domain.onramp.repositories.OnrampErrorResolver @@ -242,4 +243,10 @@ internal object OnrampDomainModule { fun provideFetchHotCryptoUseCase(hotCryptoRepository: HotCryptoRepository): FetchHotCryptoUseCase { return FetchHotCryptoUseCase(hotCryptoRepository) } + + @Provides + @Singleton + fun provideGetLegacyTopUpUrlUseCase(legacyTopUpRepository: LegacyTopUpRepository): GetLegacyTopUpUrlUseCase { + return GetLegacyTopUpUrlUseCase(legacyTopUpRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index db77c4ceba..cb6976b9b8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.* +import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -209,4 +210,12 @@ internal object StakingDomainModule { fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase { return GetStakingIntegrationIdUseCase(stakingRepository) } + + @Provides + @Singleton + fun provideCheckAccountInitializedUseCase( + walletManagersFacade: WalletManagersFacade, + ): CheckAccountInitializedUseCase { + return CheckAccountInitializedUseCase(walletManagersFacade) + } } \ 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 index 73ecbf344e..248192b95d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -5,10 +5,11 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.promo.PromoRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.operations.* +import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations +import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -209,7 +210,6 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, stakingRepository: StakingRepository, promoRepository: PromoRepository, - swapFeatureToggles: SwapFeatureToggles, dispatchers: CoroutineDispatcherProvider, currencyStatusOperations: BaseCurrencyStatusOperations, ): GetCryptoCurrencyActionsUseCase { @@ -219,7 +219,6 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, stakingRepository = stakingRepository, promoRepository = promoRepository, - swapFeatureToggles = swapFeatureToggles, dispatchers = dispatchers, currencyStatusOperations = currencyStatusOperations, ) @@ -351,21 +350,13 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, ): BaseCurrenciesStatusesOperations { - return if (tokensFeatureToggles.isBalancesCachingEnabled) { - CachedCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } else { - LceCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } + return CachedCurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + tokensFeatureToggles = tokensFeatureToggles, + ) } @Provides @@ -377,20 +368,12 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, ): BaseCurrencyStatusOperations { - return if (tokensFeatureToggles.isBalancesCachingEnabled) { - CachedCurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } else { - CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - } + return CachedCurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + stakingRepository = stakingRepository, + tokensFeatureToggles = tokensFeatureToggles, + ) } } \ 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 index 49fdf2c1be..9be71f685e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -10,7 +10,10 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* +import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase +import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -18,6 +21,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object WalletsDomainModule { @@ -165,4 +169,22 @@ internal object WalletsDomainModule { fun providesSeedPhraseNotificationUseCase(walletsRepository: WalletsRepository): SeedPhraseNotificationUseCase { return SeedPhraseNotificationUseCase(walletsRepository = walletsRepository) } + + @Provides + @Singleton + fun provideGetCardImageUseCase(onlineCardVerifier: OnlineCardVerifier): GetCardImageUseCase { + return GetCardImageUseCase(verifier = onlineCardVerifier) + } + + @Provides + @Singleton + fun providesIsWalletNFTEnabledSyncUseCase( + walletsRepository: WalletsRepository, + nftFeatureToggles: NFTFeatureToggles, + ): IsWalletNFTEnabledSyncUseCase { + return IsWalletNFTEnabledSyncUseCase( + walletsRepository = walletsRepository, + nftFeatureToggles = nftFeatureToggles, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt index 4f4aa7e79b..3bfc0b3a44 100644 --- a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt +++ b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.routing import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.tap.routing.ProxyAppRouter import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.MutableAppRouterConfig @@ -17,8 +18,15 @@ internal object AppRouterModule { @Provides @Singleton - fun provideAppRouter(config: AppRouterConfig, dispatchers: CoroutineDispatcherProvider): AppRouter = - ProxyAppRouter(config, dispatchers) + fun provideAppRouter( + config: AppRouterConfig, + dispatchers: CoroutineDispatcherProvider, + analyticsExceptionHandler: AnalyticsExceptionHandler, + ): AppRouter = ProxyAppRouter( + config = config, + dispatchers = dispatchers, + analyticsExceptionHandler = analyticsExceptionHandler, + ) @Provides @Singleton 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 9497f54791..7117819925 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 @@ -6,14 +6,17 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.getTwinCardNumber import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.Artwork -import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo fun CardDTO.signedHashesCount(): Int { return wallets.sumOf { it.totalSignedHashes ?: 0 } } -suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result? = null): String { +suspend fun CardDTO.getOrLoadCardArtworkUrl( + cardInfo: Result? = null, + onlineCardVerifier: OnlineCardVerifier, +): String { fun ifAnyError(): String { return when { cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL @@ -28,7 +31,7 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result { val artworkId = cardInfoResult.data.artwork?.id if (artworkId.isNullOrEmpty()) { 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 27fa8ead9e..6ea8247faf 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 @@ -235,13 +235,25 @@ internal class LegacyScanProcessor @Inject constructor( } else { Analytics.setContext(scanResponse) + val isTwinRefactoringEnabled = + store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() - store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) - navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } + if (isTwinRefactoringEnabled) { + navigateTo( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, + ), + ) { onProgressStateChange(it) } + } else { + store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) + navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } + } } else { delay(DELAY_SDK_DIALOG_CLOSE) onSuccess(scanResponse) 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 09fe732b6d..abf3f1f23d 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 @@ -7,9 +7,11 @@ import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog +import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.inject @@ -34,7 +36,7 @@ internal object UseCaseScanProcessor { ifLeft = { val error = scanCardExceptionConverter.convertBack(it) - Analytics.send(Basic.ScanError(error)) + Analytics.sendErrorEvent(TangemSdkErrorEvent(error)) CompletionResult.Failure(error) }, ifRight = { CompletionResult.Success(it) }, @@ -97,7 +99,12 @@ internal object UseCaseScanProcessor { -> { val error = scanCardExceptionConverter.convertBack(exception) - Analytics.send(Basic.ScanError(error)) + Analytics.sendException( + ExceptionAnalyticsEvent( + exception = error, + params = mapOf("Event" to "Scan"), + ), + ) onFailure(error) } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 7bd5572200..b9d6aff5fd 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -59,15 +59,27 @@ class CheckForOnboardingChain( else -> { Analytics.setContext(previousChainResult) + val isTwinRefactoringEnabled = + store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled + val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) .invokeSync() // If twins was twinned previously but twins welcome not shown if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) { - store.dispatchOnMain( - TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), - ) - ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() + if (isTwinRefactoringEnabled) { + ScanChainException.OnboardingNeeded( + AppRoute.Onboarding( + scanResponse = previousChainResult, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, + ), + ).left() + } else { + store.dispatchOnMain( + TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), + ) + ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() + } } else { delay(DELAY_SDK_DIALOG_CLOSE) previousChainResult.right() diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/utils/ScanCardExceptionConverter.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/utils/ScanCardExceptionConverter.kt index 53062e6918..add75d87d0 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/utils/ScanCardExceptionConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/utils/ScanCardExceptionConverter.kt @@ -12,7 +12,7 @@ internal class ScanCardExceptionConverter : TwoWayConverter ScanCardException.UnknownException(value) } - override fun convertBack(value: ScanCardException): TangemError { + override fun convertBack(value: ScanCardException): TangemSdkError { return when (value) { is ScanCardException.UnknownException -> TangemSdkError.ExceptionError(value) is ScanCardException.UserCancelled -> TangemSdkError.UserCancelled() @@ -31,7 +31,7 @@ internal class ScanCardExceptionConverter : TwoWayConverter ScanCardException.UnknownException(value) } - private fun concertScanChainException(value: ScanCardException.ChainException): TangemError { + private fun concertScanChainException(value: ScanCardException.ChainException): TangemSdkError { return when (val e = value as? ScanChainException) { is ScanChainException.DisclaimerWasCanceled -> TangemSdkError.UserCancelled() is ScanChainException.OnboardingNeeded, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index abf3ce3f3c..7ef7432c75 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -14,7 +14,6 @@ import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.Basic import com.tangem.core.res.getStringSafe import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath @@ -41,6 +40,7 @@ import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode +import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent import com.tangem.tap.derivationsFinder import com.tangem.tap.domain.tasks.product.CreateProductWalletTask import com.tangem.tap.domain.tasks.product.ResetBackupCardTask @@ -224,7 +224,7 @@ internal class DefaultTangemSdkManager( private fun sendScanResultsToAnalytics(result: CompletionResult) { if (result is CompletionResult.Failure) { (result.error as? TangemSdkError)?.let { error -> - Analytics.send(Basic.ScanError(error)) + Analytics.sendErrorEvent(TangemSdkErrorEvent(error)) } } } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 0aae1ffb84..2927924f8c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -7,6 +7,6 @@ internal class DefaultTokensFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TokensFeatureToggles { - override val isBalancesCachingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "BALANCES_CACHING_ENABLED") + override val isNetworksLoadingRefactoringEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED") } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt index 07700916ac..95046dfdb8 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -5,17 +5,10 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper import com.tangem.tap.domain.walletconnect2.domain.models.Account -import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles -internal class TangemWcBlockchainHelper( - featureToggles: WalletConnectFeatureToggles, -) : WcBlockchainHelper { +internal class TangemWcBlockchainHelper : WcBlockchainHelper { - private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) { - setOf(Blockchain.Solana, Blockchain.SolanaTestnet) - } else { - emptySet() - } + private val supportedNonEvmBlockchains = setOf(Blockchain.Solana, Blockchain.SolanaTestnet) override fun chainIdToNetworkIdOrNull(chainId: String): String? { val parsedId = chainId.parseId() ?: return null diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 0f673b9ba6..4ebc531bed 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -8,11 +8,14 @@ import com.reown.android.relay.ConnectionType import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.data.walletconnect.pair.unsupportedDApps import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper -import com.tangem.tap.domain.walletconnect2.domain.* +import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +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.* -import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow @@ -24,7 +27,6 @@ internal class DefaultLegacyWalletConnectRepository( private val application: Application, private val wcRequestDeserializer: WcJrpcRequestsDeserializer, private val analyticsHandler: AnalyticsEventHandler, - private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) : LegacyWalletConnectRepository { private var sessionProposal: Wallet.Model.SessionProposal? = null @@ -36,7 +38,7 @@ internal class DefaultLegacyWalletConnectRepository( private val _activeSessions: MutableSharedFlow> = MutableSharedFlow() override val activeSessions: Flow> = _activeSessions - private val blockchainHelper by lazy { TangemWcBlockchainHelper(walletConnectFeatureToggles) } + private val blockchainHelper by lazy { TangemWcBlockchainHelper() } override var currentSessions: List = emptyList() private set @@ -555,9 +557,4 @@ internal class DefaultLegacyWalletConnectRepository( val userChains = userNamespaces.flatMap { it.value.map { account -> account.chainId } } return wcProvidedChains.intersect(userChains.toSet()) } - - private companion object { - - val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 80319c8ce3..3df4ed99be 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -33,7 +33,6 @@ internal object WalletConnectInteractorModule { fun provideWalletConnectInteractor( wcRepository: LegacyWalletConnectRepository, wcSessionsRepository: WalletConnectSessionsRepository, - walletConnectFeatureToggles: WalletConnectFeatureToggles, currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, userWalletsListManager: UserWalletsListManager, @@ -44,7 +43,7 @@ internal object WalletConnectInteractorModule { walletConnectRepository = wcRepository, sessionsRepository = wcSessionsRepository, sdkHelper = WalletConnectSdkHelper(), - blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles), + blockchainHelper = TangemWcBlockchainHelper(), currenciesRepository = currenciesRepository, walletManagersFacade = walletManagersFacade, userWalletsListManager = userWalletsListManager, @@ -69,13 +68,11 @@ internal object WalletConnectModule { application: Application, wcRequestDeserializer: WcJrpcRequestsDeserializer, analyticsHandler: AnalyticsEventHandler, - walletConnectFeatureToggles: WalletConnectFeatureToggles, ): LegacyWalletConnectRepository { return DefaultLegacyWalletConnectRepository( application = application, wcRequestDeserializer = wcRequestDeserializer, analyticsHandler = analyticsHandler, - walletConnectFeatureToggles = walletConnectFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt index a6da04800a..48b84481db 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt @@ -6,6 +6,6 @@ internal class WalletConnectFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) { - val isSolanaTxSignEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "WC_SOLANA_TX_SIGN_ENABLED") + val isRedesignedWalletConnectEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED") } \ No newline at end of file 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 8535bc008f..5239a2cbd9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -3,7 +3,6 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.WalletManager import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.common.routing.AppRoute -import com.tangem.domain.qrscanning.models.SourceType import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject @@ -61,7 +60,11 @@ class WalletConnectMiddleware { if (uri != null && isWalletConnectUri(uri)) { store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) } else { - store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) } + store.dispatchNavigationAction { + push( + AppRoute.QrScanning(AppRoute.QrScanning.Source.WALLET_CONNECT), + ) + } } } is WalletConnectAction.ShowClipboardOrScanQrDialog -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 773aa32d80..894ab1be67 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings @@ -55,6 +56,7 @@ internal class CardSettingsModel @Inject constructor( private val getUserWalletUseCase: GetUserWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, + private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -198,13 +200,24 @@ internal class CardSettingsModel @Inject constructor( } if (scanResponse.cardTypesResolver.isTangemTwins()) { - // needs to prepare twin state if it's twin cards (depends on onboarding refactoring) - store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) - store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) + if (onboardingV2FeatureToggles.isTwinRefactoringEnabled) { + store.dispatchNavigationAction { + push( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.RecreateWalletTwin, + ), + ) + } + } else { + // needs to prepare twin state if it's twin cards (depends on onboarding refactoring) + store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) + store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) - cardSettingsInteractor.clear() + cardSettingsInteractor.clear() - store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } + store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } + } } else { val card = scanResponse.card diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt index f68ae0c367..dd3093deb9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt @@ -140,9 +140,9 @@ private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) Icon( painter = painterResource( if (checked) { - R.drawable.ic_accepted + R.drawable.ic_accepted_20 } else { - R.drawable.ic_unticked + R.drawable.ic_unticked_20 }, ), contentDescription = null, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt index 39ae0e13fb..8f4cf86296 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/model/SecurityModeModel.kt @@ -4,13 +4,15 @@ import androidx.compose.runtime.Stable import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsErrorHandler +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.common.util.cardTypesResolver import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor @@ -30,6 +32,8 @@ internal class SecurityModeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, private val cardSettingsInteractor: CardSettingsInteractor, + private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsErrorHandler: AnalyticsErrorHandler, ) : Model() { private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value @@ -85,14 +89,14 @@ internal class SecurityModeModel @Inject constructor( val paramValue = AnalyticsParam.SecurityMode.from(selectedOption) when (result) { is CompletionResult.Success -> { - Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue)) + analyticsEventHandler.send(Settings.CardSettings.SecurityModeChanged(paramValue)) store.dispatchNavigationAction(AppRouter::pop) } is CompletionResult.Failure -> { val error = result.error if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) { - Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error)) + analyticsErrorHandler.sendErrorEvent(TangemSdkErrorEvent(error)) } } else -> Unit diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt index a6ac03c6c7..3a1c1dd7ea 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/dialogs/ClipboardOrScanQrDialog.kt @@ -21,7 +21,7 @@ object ClipboardOrScanQrDialog { } setNegativeButton(context.getText(R.string.wallet_connect_scan_new_code)) { _, _ -> store.dispatchNavigationAction { - push(AppRoute.QrScanning(source = SourceType.WALLET_CONNECT)) + push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.WALLET_CONNECT)) } } setOnDismissListener { diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt index 5230ab1717..58f8a67687 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeModel.kt @@ -19,7 +19,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.TokensAction import com.tangem.domain.wallets.builder.UserWalletBuilder -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.IntroductionProcess @@ -44,12 +43,12 @@ import javax.inject.Inject internal class HomeModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val scanCardProcessor: ScanCardProcessor, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, private val saveWalletUseCase: SaveWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, ) : Model() { private val tangemErrorHandler = TangemTangemErrorsHandler(store) @@ -100,10 +99,7 @@ internal class HomeModel @Inject constructor( } private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) { - val userWallet = UserWalletBuilder( - scanResponse = scanResponse, - generateWalletNameUseCase = generateWalletNameUseCase, - ).build() + val userWallet = userWalletBuilderFactory.create(scanResponse = scanResponse).build() if (userWallet == null) { Timber.e("User wallet not created") 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 b16af4eab3..ccae0217e2 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 @@ -10,7 +10,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.IntroductionProcess import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -91,8 +90,9 @@ private suspend fun readCard() { } private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build().guard { + val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse) + + val userWallet = userWalletBuilder.build().guard { Timber.e("User wallet not created") return@launch } diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 143a8d92a4..859d82a082 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -30,8 +30,6 @@ import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.analytics.StoriesEvents -import com.tangem.features.onramp.OnrampFeatureToggles -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.tap.common.extensions.setContext import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel @@ -60,8 +58,6 @@ internal class MainViewModel @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val imagePreloader: ImagePreloader, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, - private val swapFeatureToggles: SwapFeatureToggles, - onrampFeatureToggles: OnrampFeatureToggles, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -82,9 +78,7 @@ internal class MainViewModel @Inject constructor( } } - if (onrampFeatureToggles.isHotTokensEnabled) { - viewModelScope.launch { fetchHotCryptoUseCase() } - } + viewModelScope.launch { fetchHotCryptoUseCase() } updateAppCurrencies() observeFlips() @@ -301,23 +295,21 @@ internal class MainViewModel @Inject constructor( private fun preloadImages() { try { viewModelScope.launch { - if (swapFeatureToggles.isPromoStoriesEnabled) { - val swapStories = getStoryContentUseCase.invokeSync( - id = StoryContentIds.STORY_FIRST_TIME_SWAP.id, - refresh = true, - ).getOrNull() + val swapStories = getStoryContentUseCase.invokeSync( + id = StoryContentIds.STORY_FIRST_TIME_SWAP.id, + refresh = true, + ).getOrNull() - if (swapStories == null) { - analyticsEventHandler.send( - StoriesEvents.Error( - type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, - ), - ) - } else { - val storiesImages = swapStories.getImageUrls() - // try to preload images for stories - storiesImages.forEach(imagePreloader::preload) - } + if (swapStories == null) { + analyticsEventHandler.send( + StoriesEvents.Error( + type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, + ), + ) + } else { + val storiesImages = swapStories.getImageUrls() + // try to preload images for stories + storiesImages.forEach(imagePreloader::preload) } } } catch (ex: Exception) { 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 b7642d1a29..7eda3eb6c5 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 @@ -14,7 +14,6 @@ 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.visa.model.VisaCardActivationStatus -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter @@ -70,6 +69,7 @@ object OnboardingHelper { } } + @Suppress("CyclomaticComplexMethod") fun whereToNavigate(scanResponse: ScanResponse): AppRoute { val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 || scanResponse.productType == ProductType.Ring || @@ -85,7 +85,7 @@ object OnboardingHelper { scanResponse = scanResponse, mode = when (backupState.startedSource) { BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding - BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackup + BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackupWallet1 }, ) } @@ -104,7 +104,11 @@ object OnboardingHelper { } else { AppRoute.OnboardingOther } - ProductType.Twins -> AppRoute.OnboardingTwins + ProductType.Twins -> if (featureToggles.isTwinRefactoringEnabled) { + AppRoute.Onboarding(scanResponse = scanResponse) + } else { + AppRoute.OnboardingTwins + } ProductType.Start2Coin, ProductType.Visa, -> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported") @@ -250,8 +254,9 @@ object OnboardingHelper { hasBackupError: Boolean, alreadyCreatedWallet: UserWallet? = null, ) { - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = alreadyCreatedWallet ?: UserWalletBuilder(scanResponse, walletNameGenerateUseCase) + val userWalletBuilderFactory = store.inject(DaggerGraphState::userWalletBuilderFactory) + + val userWallet = alreadyCreatedWallet ?: userWalletBuilderFactory.create(scanResponse) .hasBackupError(hasBackupError) .backupCardsIds(backupCardsIds?.toSet()) .build() diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index a0db9b19f8..e982cd5f2e 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -7,8 +7,8 @@ import com.tangem.common.extensions.isZero import com.tangem.common.services.Result import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.models.scan.ScanResponse -import com.tangem.operations.attestation.CardVerifyAndGetInfo import com.tangem.operations.attestation.OnlineCardVerifier +import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo import com.tangem.tap.common.entities.ProgressState import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate @@ -26,16 +26,22 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ +@Deprecated("Remove when navigation refactoring will be implemented") class OnboardingManager(var scanResponse: ScanResponse) { - private val cardRepository: CardRepository = store.inject(DaggerGraphState::cardRepository) + private val cardRepository: CardRepository by lazy { store.inject(DaggerGraphState::cardRepository) } + + private val onlineCardVerifier: OnlineCardVerifier by lazy { store.inject(DaggerGraphState::onlineCardVerifier) } + private var cardInfo: Result? = null suspend fun loadArtworkUrl(): String { val cardInfo = cardInfo - ?: OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey) + ?: onlineCardVerifier.getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey) + this.cardInfo = cardInfo - return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo) + + return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo, onlineCardVerifier) } suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index 97746d2d70..8346f444cd 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -26,9 +26,7 @@ import com.tangem.tap.store import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.wallet.R -/** -[REDACTED_AUTHOR] - */ +@Deprecated("Use OnboardingNoteComponent") class OnboardingNoteFragment : BaseOnboardingFragment() { private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None 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 855d95ce35..984f638acc 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 @@ -1,7 +1,6 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.net.Uri -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.core.TangemSdkError @@ -12,6 +11,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList @@ -24,7 +24,6 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.Artwork @@ -243,7 +242,9 @@ private suspend fun readCard(): CompletionResult { } private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri { - return when (val cardInfo = OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) { + val onlineCardVerifier = store.inject(DaggerGraphState::onlineCardVerifier) + + return when (val cardInfo = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) { is Result.Success -> { val artworkId = cardInfo.data.artwork?.id if (artworkId.isNullOrEmpty()) { @@ -453,8 +454,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) store.dispatchOnMain(BackupAction.AddBackupCard.Success(result.data)) } is CompletionResult.Failure -> { - val crashlytics = FirebaseCrashlytics.getInstance() - when (val error = result.error) { is TangemSdkError.CardVerificationFailed -> { Analytics.send( @@ -491,7 +490,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) GlobalAction.ShowDialog(BackupDialog.AttestationFailed), ) } - else -> crashlytics.recordException(error) + else -> Analytics.sendException(ExceptionAnalyticsEvent(error)) } } } @@ -782,9 +781,10 @@ private suspend fun updateWallet( } private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: BackupState): UserWallet { - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) + val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse) + return requireNotNull( - value = UserWalletBuilder(scanResponse, walletNameGenerateUseCase) + value = userWalletBuilder .backupCardsIds(backupState.backupCardIds.toSet()) .hasBackupError(backupState.hasBackupError) .build(), diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt index ec80c29c24..fbeff5f6f4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -5,7 +5,7 @@ import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.tangem.core.analytics.Analytics import com.tangem.domain.models.scan.ScanResponse -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog 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 02fb7c271c..d8f15fd3d7 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 @@ -6,7 +6,6 @@ import com.tangem.common.extensions.guard import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding @@ -70,8 +69,10 @@ internal class SaveWalletMiddleware { scope.launch { val backupInfo = state.backupInfo ?: error("Backup info is null") - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = UserWalletBuilder(backupInfo.scanResponse, walletNameGenerateUseCase) + val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory) + .create(scanResponse = backupInfo.scanResponse) + + val userWallet = userWalletBuilder .backupCardsIds(state.backupInfo.backupCardsIds) .hasBackupError(hasBackupError) .build() 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 b9f8660cf1..f03175888a 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 @@ -11,10 +11,8 @@ import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic - import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType import com.tangem.domain.wallets.legacy.unlockIfLockable import com.tangem.tap.backupService @@ -118,9 +116,9 @@ internal class WelcomeMiddleware { ) scanCardInternal { scanResponse -> - val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase) - val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build() - ?: return@scanCardInternal + val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse) + + val userWallet = userWalletBuilder.build() ?: return@scanCardInternal val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) userWalletsListManager.save(userWallet, canOverride = true) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index a892310a6f..b398116c05 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -6,6 +6,7 @@ import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest +import com.tangem.data.onramp.legacy.mercuryoNetwork import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading 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 08b6c9ba8a..2525bdd3cd 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 @@ -26,13 +26,13 @@ import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.onramp.OnrampFeatureToggles +import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -56,7 +56,6 @@ data class DaggerGraphState( val generalUserWalletsListManager: UserWalletsListManager? = null, val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, - val generateWalletNameUseCase: GenerateWalletNameUseCase? = null, val cardRepository: CardRepository? = null, val settingsRepository: SettingsRepository? = null, val blockchainSDKFactory: BlockchainSDKFactory? = null, @@ -67,7 +66,6 @@ data class DaggerGraphState( val shareManager: ShareManager? = null, val appRouter: AppRouter? = null, val transactionSignerFactory: TransactionSignerFactory? = null, - val getUserCountryUseCase: GetUserCountryUseCase? = null, val onrampFeatureToggles: OnrampFeatureToggles? = null, val environmentConfigStorage: EnvironmentConfigStorage? = null, val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null, @@ -77,4 +75,6 @@ data class DaggerGraphState( val clipboardManager: ClipboardManager? = null, val settingsManager: SettingsManager? = null, val uiMessageSender: UiMessageSender? = null, + val onlineCardVerifier: OnlineCardVerifier? = null, + val userWalletBuilderFactory: UserWalletBuilder.Factory? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index d744032000..bcebe2fe83 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -1,8 +1,9 @@ package com.tangem.tap.routing -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.navigation.Router import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -15,6 +16,7 @@ import kotlin.reflect.KClass internal class ProxyAppRouter( private val config: AppRouterConfig, private val dispatchers: CoroutineDispatcherProvider, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : AppRouter { private val routerScope: CoroutineScope @@ -76,7 +78,7 @@ internal class ProxyAppRouter( override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) { if (!isSuccess) { - FirebaseCrashlytics.getInstance().recordException(RuntimeException(errorMessage)) + analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage))) Timber.w(errorMessage) with(receiver = config.snackbarHandler ?: return) { diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index f437e1e6e9..56711d9800 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -2,6 +2,7 @@ package com.tangem.tap.routing.utils import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.QrScanningComponent import com.tangem.feature.referral.api.ReferralComponent import com.tangem.feature.stories.api.StoriesComponent @@ -15,11 +16,13 @@ import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.PushNotificationsComponent import com.tangem.features.send.api.SendComponent -import com.tangem.features.swap.SwapComponent +import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.staking.api.StakingComponent +import com.tangem.features.swap.SwapComponent import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent +import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent @@ -40,6 +43,7 @@ import com.tangem.utils.Provider import dagger.hilt.android.scopes.ActivityScoped import java.util.WeakHashMap import javax.inject.Inject +import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedisegnedWalletConnectComponent @ActivityScoped @Suppress("LongParameterList", "LargeClass") @@ -73,8 +77,12 @@ internal class ChildFactory @Inject constructor( private val referralComponentFactory: ReferralComponent.Factory, private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory, private val walletComponentFactory: WalletEntryComponent.Factory, + private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory, + private val sendFeatureToggles: SendFeatureToggles, + private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory, private val testerRouter: TesterRouter, private val routingFeatureToggles: RoutingFeatureToggles, + private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) { fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { @@ -201,9 +209,12 @@ internal class ChildFactory @Inject constructor( contextProvider = contextProvider(route, contextFactory), params = OnboardingEntryComponent.Params( scanResponse = route.scanResponse, - multiWalletMode = when (route.mode) { - AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding - AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup + mode = when (route.mode) { + AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding + AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1 + AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin + AppRoute.Onboarding.Mode.RecreateWalletTwin -> + OnboardingEntryComponent.Mode.RecreateWalletTwin }, ), componentFactory = onboardingEntryComponentFactory, @@ -255,18 +266,33 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.Send -> { - createComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SendComponent.Params( - userWalletId = route.userWalletId, - currency = route.currency, - transactionId = route.transactionId, - amount = route.amount, - tag = route.tag, - destinationAddress = route.destinationAddress, - ), - componentFactory = sendComponentFactory, - ) + if (sendFeatureToggles.isSendV2Enabled) { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = com.tangem.features.send.v2.api.SendComponent.Params( + userWalletId = route.userWalletId, + currency = route.currency, + transactionId = route.transactionId, + amount = route.amount, + tag = route.tag, + destinationAddress = route.destinationAddress, + ), + componentFactory = sendComponentFactoryV2, + ) + } else { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SendComponent.Params( + userWalletId = route.userWalletId, + currency = route.currency, + transactionId = route.transactionId, + amount = route.amount, + tag = route.tag, + destinationAddress = route.destinationAddress, + ), + componentFactory = sendComponentFactory, + ) + } } is AppRoute.Home -> { createComponentChild( @@ -276,17 +302,29 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.WalletConnectSessions -> { - createComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = walletConnectComponentFactory, - ) + if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = Unit, + componentFactory = redesignedWalletConnectComponentFactory, + ) + } else { + createComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = Unit, + componentFactory = walletConnectComponentFactory, + ) + } } is AppRoute.QrScanning -> { + val source = when (route.source) { + AppRoute.QrScanning.Source.SEND -> SourceType.SEND + AppRoute.QrScanning.Source.WALLET_CONNECT -> SourceType.WALLET_CONNECT + } createComponentChild( contextProvider = contextProvider(route, contextFactory), params = QrScanningComponent.Params( - source = route.source, + source = source, networkName = route.networkName, ), componentFactory = qrScanningComponentFactory, @@ -399,18 +437,33 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() }) } is AppRoute.Send -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SendComponent.Params( - userWalletId = route.userWalletId, - currency = route.currency, - transactionId = route.transactionId, - amount = route.amount, - tag = route.tag, - destinationAddress = route.destinationAddress, - ), - componentFactory = sendComponentFactory, - ) + if (sendFeatureToggles.isSendV2Enabled) { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = com.tangem.features.send.v2.api.SendComponent.Params( + userWalletId = route.userWalletId, + currency = route.currency, + transactionId = route.transactionId, + amount = route.amount, + tag = route.tag, + destinationAddress = route.destinationAddress, + ), + componentFactory = sendComponentFactoryV2, + ) + } else { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SendComponent.Params( + userWalletId = route.userWalletId, + currency = route.currency, + transactionId = route.transactionId, + amount = route.amount, + tag = route.tag, + destinationAddress = route.destinationAddress, + ), + componentFactory = sendComponentFactory, + ) + } } is AppRoute.AppSettings -> { route.asComponentChild( @@ -482,10 +535,14 @@ internal class ChildFactory @Inject constructor( route.asFragmentChild(Provider { OnboardingWalletFragment() }) } is AppRoute.QrScanning -> { + val source = when (route.source) { + AppRoute.QrScanning.Source.SEND -> SourceType.SEND + AppRoute.QrScanning.Source.WALLET_CONNECT -> SourceType.WALLET_CONNECT + } route.asComponentChild( contextProvider = contextProvider(route, contextFactory), params = QrScanningComponent.Params( - source = route.source, + source = source, networkName = route.networkName, ), componentFactory = qrScanningComponentFactory, @@ -531,11 +588,19 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.WalletConnectSessions -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = walletConnectComponentFactory, - ) + if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = Unit, + componentFactory = redesignedWalletConnectComponentFactory, + ) + } else { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = Unit, + componentFactory = walletConnectComponentFactory, + ) + } } is AppRoute.CurrencyDetails -> { route.asComponentChild( @@ -645,9 +710,12 @@ internal class ChildFactory @Inject constructor( contextProvider = contextProvider(route, contextFactory), params = OnboardingEntryComponent.Params( scanResponse = route.scanResponse, - multiWalletMode = when (route.mode) { - AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding - AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup + mode = when (route.mode) { + AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding + AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1 + AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin + AppRoute.Onboarding.Mode.RecreateWalletTwin -> + OnboardingEntryComponent.Mode.RecreateWalletTwin }, ), componentFactory = onboardingEntryComponentFactory, diff --git a/app/src/main/res/layout/layout_backup_access_code_info.xml b/app/src/main/res/layout/layout_backup_access_code_info.xml index c53bb8fc3d..5095c1eca0 100644 --- a/app/src/main/res/layout/layout_backup_access_code_info.xml +++ b/app/src/main/res/layout/layout_backup_access_code_info.xml @@ -9,7 +9,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/access_code_info_protect_margin_top" - android:src="@drawable/ic_access_code" + android:src="@drawable/ic_access_code_119" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index f033d76c1e..1238ad4305 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -9,7 +9,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import kotlinx.serialization.Serializable @@ -147,9 +146,15 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class QrScanning( - val source: SourceType, + val source: Source, val networkName: String? = null, - ) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}") + ) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}") { + + enum class Source { + WALLET_CONNECT, + SEND, + } + } @Serializable data class ReferralProgram( @@ -255,7 +260,9 @@ sealed class AppRoute(val path: String) : Route { enum class Mode { Onboarding, // general Mode - AddBackup, // continue backup process for existing wallet 1 + AddBackupWallet1, // continue backup process for existing wallet 1 + WelcomeOnlyTwin, // show welcome screen and then navigate to wallet for twins + RecreateWalletTwin, // reset twins } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index cfd16d8ee2..d8a18ff233 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -40,7 +40,12 @@ fun AmountScreenContent( bottom = TangemTheme.dimens.spacing16, ), ) { - amountField(amountState = amountState, isBalanceHidden = isBalanceHidden) + amountField( + amountState = amountState, + isBalanceHidden = isBalanceHidden, + onValueChange = clickIntents::onAmountValueChange, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + ) buttons( segmentedButtonConfig = amountState.segmentedButtonConfig, clickIntents = clickIntents, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt index ebf69ed80d..ad6a224264 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt @@ -6,7 +6,7 @@ import com.tangem.utils.transformer.Transformer /** * Dismisses indication on pasted value */ -class AmountPastedTriggerDismissTransformer : Transformer { +object AmountPastedTriggerDismissTransformer : Transformer { override fun transform(prevState: AmountState): AmountState { if (prevState !is AmountState.Data) return prevState diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index f91822b7b7..94981c571b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -3,6 +3,7 @@ package com.tangem.common.ui.amountScreen.converters import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverterV2 import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState @@ -30,6 +31,7 @@ import kotlinx.collections.immutable.persistentListOf * @property cryptoCurrencyStatusProvider current cryptocurrency status provider * @property iconStateConverter currency icon converter */ +@Deprecated("Use AmountStateConverterV2") class AmountStateConverter( private val clickIntents: AmountScreenClickIntents, private val appCurrencyProvider: Provider, @@ -80,4 +82,63 @@ class AmountStateConverter( selectedButton = 0, ) } +} + +/** + * Converts initial [String] to [AmountState] + * + * @property clickIntents amount screen clicks + * @property appCurrency selected app currency + * @property maxEnterAmount max enter amount data + * @property cryptoCurrencyStatus current cryptocurrency status + * @property iconStateConverter currency icon converter + */ +class AmountStateConverterV2( + private val clickIntents: AmountScreenClickIntents, + private val appCurrency: AppCurrency, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val maxEnterAmount: EnterAmountBoundary, + private val iconStateConverter: CryptoCurrencyToIconStateConverter, +) : Converter { + + private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { + AmountFieldConverterV2( + clickIntents = clickIntents, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrency = appCurrency, + ) + } + + override fun convert(value: AmountParameters): AmountState { + val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } + val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } + val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() + + return AmountState.Data( + title = value.title, + availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), + tokenIconState = iconStateConverter.convert(cryptoCurrencyStatus), + amountTextField = amountFieldConverter.convert(value.value), + isPrimaryButtonEnabled = false, + appCurrencyCode = appCurrency.code, + segmentedButtonConfig = persistentListOf( + AmountSegmentedButtonsConfig( + title = stringReference(cryptoCurrencyStatus.currency.symbol), + iconState = iconStateConverter.convertCustom( + value = cryptoCurrencyStatus, + forceGrayscale = noFeeRate, + showCustomTokenBadge = false, + ), + isFiat = false, + ), + AmountSegmentedButtonsConfig( + title = stringReference(appCurrency.code), + iconUrl = appCurrency.iconSmallUrl, + isFiat = true, + ), + ), + isSegmentedButtonsEnabled = !noFeeRate, + selectedButton = 0, + ) + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index 392fa817ba..4094aba847 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -31,6 +31,7 @@ class AmountFieldChangeTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val maxEnterAmount: EnterAmountBoundary, private val minimumTransactionAmount: EnterAmountBoundary?, + private val reduceAmountBy: BigDecimal = BigDecimal.ZERO, private val value: String, ) : Transformer { @@ -58,7 +59,7 @@ class AmountFieldChangeTransformer( val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField) - val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false + val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } == true val isZero = if (amountTextField.isFiatValue) { decimalFiatValue.isNullOrZero() } else { @@ -67,6 +68,7 @@ class AmountFieldChangeTransformer( val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( isPrimaryButtonEnabled = !isZero && !isCheckFailed, + reduceAmountBy = reduceAmountBy, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, @@ -74,10 +76,11 @@ class AmountFieldChangeTransformer( error = when { isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isLessThanMinimumIfProvided -> { - val minimumAmount = minimumTransactionAmount - ?.amount - ?.format { crypto(cryptoCurrencyStatus.currency) } - .orEmpty() + val minimumAmount = + minimumTransactionAmount.amount.format { + crypto(cryptoCurrencyStatus.currency) + } + resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), @@ -98,6 +101,7 @@ class AmountFieldChangeTransformer( private fun AmountState.Data.emptyState(): AmountState.Data { return copy( isPrimaryButtonEnabled = false, + reduceAmountBy = BigDecimal.ZERO, amountTextField = amountTextField.copy( value = "", fiatValue = "", diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index d460c0ca5d..ce11f41fce 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -19,12 +19,13 @@ import com.tangem.utils.isNullOrZero import java.math.BigDecimal /** - * Converts initial [String] to [AmountField] + * Converts initial [String] to [AmountFieldModel] * * @property clickIntents amount screen clicks * @property appCurrencyProvider selected app currency provider * @property cryptoCurrencyStatusProvider current cryptocurrency status provider */ +@Deprecated("Use AmountFieldConverterV2") class AmountFieldConverter( private val clickIntents: AmountScreenClickIntents, private val cryptoCurrencyStatusProvider: Provider, @@ -76,6 +77,68 @@ class AmountFieldConverter( type = AmountType.FiatType(appCurrency.code), ) + private companion object { + private const val FIAT_DECIMALS = 2 + } +} + +/** + * Converts initial [String] to [AmountFieldModel] + * + * @property clickIntents amount screen clicks + * @property appCurrency selected app currency + * @property cryptoCurrencyStatus current cryptocurrency status + */ +class AmountFieldConverterV2( + private val clickIntents: AmountScreenClickIntents, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: String): AmountFieldModel { + val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO + val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val (fiatValue, fiatDecimal) = when { + fiatRate.isNullOrZero() -> "" to null + value.isEmpty() -> "" to BigDecimal.ZERO + else -> { + val fiatDecimal = fiatRate?.multiply(cryptoDecimal) + val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty() + fiatValue to fiatDecimal + } + } + val isDoneActionEnabled = !cryptoDecimal.isNullOrZero() + return AmountFieldModel( + value = value, + fiatValue = fiatValue, + onValueChange = clickIntents::onAmountValueChange, + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + keyboardActions = KeyboardActions( + onDone = { clickIntents.onAmountNext() }, + ), + isFiatValue = false, + cryptoAmount = cryptoAmount, + fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrency), + isError = false, + isWarning = false, + error = TextReference.EMPTY, + isFiatUnavailable = fiatRate == null, + isValuePasted = false, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + ) + } + + private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount( + currencySymbol = appCurrency.symbol, + value = fiatValue, + decimals = FIAT_DECIMALS, + type = AmountType.FiatType(appCurrency.code), + ) + private companion object { private const val FIAT_DECIMALS = 2 } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt index 990e027cdf..62280c0ebe 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -15,6 +15,7 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.extensions.isZero import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal import java.math.RoundingMode /** @@ -26,6 +27,7 @@ class AmountFieldSetMaxAmountTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val maxAmount: EnterAmountBoundary, private val minAmount: EnterAmountBoundary?, + private val reduceAmountBy: BigDecimal = BigDecimal.ZERO, ) : Transformer { override fun transform(prevState: AmountState): AmountState { @@ -42,9 +44,10 @@ class AmountFieldSetMaxAmountTransformer( val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() - val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } ?: false + val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } == true return prevState.copy( isPrimaryButtonEnabled = !isLessThanMinimumIfProvided, + reduceAmountBy = reduceAmountBy, amountTextField = amountTextField.copy( isValuePasted = true, value = cryptoValue, @@ -52,10 +55,7 @@ class AmountFieldSetMaxAmountTransformer( isError = isLessThanMinimumIfProvided, error = when { isLessThanMinimumIfProvided -> { - val minimumAmount = minAmount - ?.amount - ?.format { crypto(cryptoCurrencyStatus.currency) } - .orEmpty() + val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index 52758a02cc..3833714e62 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.PersistentList +import java.math.BigDecimal /** Model for amount state */ @Stable @@ -21,6 +22,8 @@ sealed class AmountState { * @param isSegmentedButtonsEnabled indicates if currency switches is enabled * @param amountTextField amount field state * @param appCurrencyCode app currency code + * @param isEditingDisabled indicated whether amount is editable + * @param reduceAmountBy reduces amount to be sent by specified value */ data class Data( override val isPrimaryButtonEnabled: Boolean, @@ -32,6 +35,8 @@ sealed class AmountState { val isSegmentedButtonsEnabled: Boolean, val amountTextField: AmountFieldModel, val appCurrencyCode: String, + val isEditingDisabled: Boolean = false, + val reduceAmountBy: BigDecimal = BigDecimal.ZERO, ) : AmountState() data class Empty( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index f2f8e078f9..70d51c2503 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -25,12 +25,13 @@ import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTr import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.rememberDecimalFormat import kotlinx.coroutines.delay +@Deprecated("Use AmountField with clicks") @Composable internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) { val decimalFormat = rememberDecimalFormat() @@ -80,6 +81,60 @@ internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) AmountSecondary(amountField, appCurrencyCode) } +@Composable +internal fun AmountField( + amountField: AmountFieldModel, + appCurrencyCode: String, + onValueChange: (String) -> Unit, + onValuePastedTriggerDismiss: () -> Unit, +) { + val decimalFormat = rememberDecimalFormat() + val isFiatValue = amountField.isFiatValue + val currencyCode = if (isFiatValue) appCurrencyCode else null + val (primaryAmount, primaryValue) = if (isFiatValue) { + amountField.fiatAmount to amountField.fiatValue + } else { + amountField.cryptoAmount to amountField.value + } + val requester = remember { FocusRequester() } + + AmountTextField( + value = primaryValue, + decimals = primaryAmount.decimals, + visualTransformation = AmountVisualTransformation( + decimals = primaryAmount.decimals, + symbol = primaryAmount.currencySymbol, + currencyCode = currencyCode, + decimalFormat = decimalFormat, + ), + onValueChange = onValueChange, + keyboardOptions = amountField.keyboardOptions, + keyboardActions = amountField.keyboardActions, + textStyle = TangemTheme.typography.h2.copy( + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ), + isAutoResize = true, + isValuePasted = amountField.isValuePasted, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, + modifier = Modifier + .focusRequester(requester) + .padding( + top = TangemTheme.dimens.spacing24, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ) + .requiredHeightIn(min = TangemTheme.dimens.size32), + ) + + LaunchedEffect(key1 = Unit) { + delay(timeMillis = 200) + requester.requestFocus() + } + + AmountSecondary(amountField, appCurrencyCode) +} + @Composable private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) { val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount @@ -96,11 +151,12 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri val text = if (amountField.isFiatValue) { secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) } } else { - BigDecimalFormatter.formatFiatAmount( - fiatAmount = secondaryAmount.value, - fiatCurrencySymbol = secondaryAmount.currencySymbol, - fiatCurrencyCode = appCurrencyCode, - ) + secondaryAmount.value.format { + fiat( + fiatCurrencySymbol = secondaryAmount.currencySymbol, + fiatCurrencyCode = appCurrencyCode, + ) + } } Text( text = text, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 8b9904cc42..104f2f60ae 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -24,6 +24,8 @@ internal fun LazyListScope.amountField( amountState: AmountState.Data, isBalanceHidden: Boolean, modifier: Modifier = Modifier, + onValueChange: (String) -> Unit, + onValuePastedTriggerDismiss: () -> Unit, ) { item(key = AMOUNT_FIELD_KEY) { Column( @@ -63,6 +65,8 @@ internal fun LazyListScope.amountField( AmountField( amountField = amountState.amountTextField, appCurrencyCode = amountState.appCurrencyCode, + onValueChange = onValueChange, + onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index e37d15ae31..d546442104 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -159,6 +159,11 @@ sealed class NotificationUM(val config: NotificationConfig) { ), subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text), ) + + data object DestinationMemoRequired : Error( + title = resourceReference(id = R.string.send_validation_destination_tag_required_title), + subtitle = resourceReference(id = R.string.send_validation_destination_tag_required_description), + ) } open class Warning( diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index 347ac937ec..b5e98fa800 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.utils.getFiatString +import com.tangem.common.ui.notifications.NotificationsFactory.addRequireDestinationFlagErrorNotification import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format @@ -112,6 +113,10 @@ object NotificationsFactory { } } + fun MutableList.addRequireDestinationFlagErrorNotification() { + add(NotificationUM.Error.DestinationMemoRequired) + } + fun MutableList.addMinimumAmountErrorNotification( minimumSendAmount: BigDecimal?, sendingAmount: BigDecimal, @@ -326,6 +331,9 @@ object NotificationsFactory { error = validationError, onReduceClick = onReduceClick, ) + is BlockchainSdkError.XRP -> addXRPTransactionValidationError( + error = validationError, + ) null -> minAdaValue?.let { add( NotificationUM.Cardano.MinAdaValueCharged( @@ -406,6 +414,12 @@ object NotificationsFactory { } } + private fun MutableList.addXRPTransactionValidationError(error: BlockchainSdkError.XRP) { + when (error) { + is BlockchainSdkError.XRP.DestinationMemoRequired -> addRequireDestinationFlagErrorNotification() + } + } + fun MutableList.addRentExemptionNotification(rentWarning: CryptoCurrencyWarning.Rent?) { if (rentWarning == null) return add(NotificationUM.Solana.RentInfo(rentWarning)) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt index 025a6c1038..8c12f046df 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsEvent.kt @@ -7,7 +7,6 @@ open class AnalyticsEvent( val category: String, val event: String, var params: Map = mapOf(), - val error: Throwable? = null, ) { val id: String = "[$category] $event" diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index c623358a84..8f2960f7a1 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -3,8 +3,7 @@ package com.tangem.core.analytics.models sealed class Basic( event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent("Basic", event, params, error) { +) : AnalyticsEvent("Basic", event, params) { class CardWasScanned( source: AnalyticsParam.ScreensSources, @@ -73,11 +72,6 @@ sealed class Basic( } } - class ScanError(error: Throwable) : Basic( - event = "Scan", - error = error, - ) - class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic( event = "Request Support", params = mapOf( diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt new file mode 100644 index 0000000000..c2af9763c7 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/ExceptionAnalyticsEvent.kt @@ -0,0 +1,6 @@ +package com.tangem.core.analytics.models + +class ExceptionAnalyticsEvent( + val exception: Throwable, + val params: Map = mapOf(), +) \ No newline at end of file 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 239573bed5..081a583c06 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 @@ -2,6 +2,7 @@ package com.tangem.core.analytics import com.tangem.core.analytics.api.* import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex @@ -16,7 +17,9 @@ interface GlobalAnalyticsEventHandler : AnalyticsEventHandler, AnalyticsHandlerHolder, AnalyticsFilterHolder, - ParamsInterceptorHolder + ParamsInterceptorHolder, + AnalyticsErrorHandler, + AnalyticsExceptionHandler object Analytics : GlobalAnalyticsEventHandler { @@ -72,6 +75,25 @@ object Analytics : GlobalAnalyticsEventHandler { } } + override fun sendErrorEvent(event: AnalyticsEvent) { + analyticsScope.launch { + event.params = applyParamsInterceptors(event) + analyticsMutex.withLock { + analyticsHandlers.filterIsInstance() + .forEach { handler -> handler.sendErrorEvent(event) } + } + } + } + + override fun sendException(event: ExceptionAnalyticsEvent) { + analyticsScope.launch { + analyticsMutex.withLock { + analyticsHandlers.filterIsInstance() + .forEach { it.sendException(event) } + } + } + } + private suspend fun applyParamsInterceptors(event: AnalyticsEvent): MutableMap { val interceptedParams = event.params.toMutableMap() analyticsMutex.withLock { diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/api/ClientApi.kt b/core/analytics/src/main/java/com/tangem/core/analytics/api/ClientApi.kt index f44a0133cd..523ac41b71 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/api/ClientApi.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/api/ClientApi.kt @@ -8,5 +8,9 @@ interface EventLogger { } interface ErrorEventLogger { - fun logErrorEvent(error: Throwable, params: Map = emptyMap()) + fun logErrorEvent(event: String, params: Map = emptyMap()) +} + +interface ExceptionLogger { + fun logException(error: Throwable, params: Map = emptyMap()) } \ No newline at end of file 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 7bedfebff3..6c98379d6d 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,15 +1,25 @@ package com.tangem.core.analytics.api import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent /** -[REDACTED_AUTHOR] + * Sends usual analytics events */ interface AnalyticsEventHandler { fun send(event: AnalyticsEvent) } +interface AnalyticsErrorHandler { + fun sendErrorEvent(event: AnalyticsEvent) +} + +interface AnalyticsExceptionHandler { + fun sendException(event: ExceptionAnalyticsEvent) +} + interface AnalyticsHandler : AnalyticsEventHandler { + fun id(): String fun send(eventId: String, params: Map = emptyMap()) @@ -19,10 +29,6 @@ interface AnalyticsHandler : AnalyticsEventHandler { } } -interface ErrorEventHandler { - fun send(error: Throwable, params: Map = emptyMap()) -} - interface AnalyticsHandlerHolder { fun addHandler(name: String, handler: AnalyticsHandler) fun removeHandler(name: String): AnalyticsHandler? diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt index 18a95d3a02..de53ef6a99 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/di/AnalyticsModule.kt @@ -1,7 +1,9 @@ package com.tangem.core.analytics.di import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.api.ParamsInterceptorHolder import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.domain.analytics.repository.AnalyticsRepository @@ -21,6 +23,18 @@ internal object AnalyticsModule { return Analytics // todo replace after refactoring calling Analytics in whole project } + @Singleton + @Provides + fun provideAnalyticsErrorHandler(): AnalyticsErrorHandler { + return Analytics + } + + @Singleton + @Provides + fun provideAnalyticsExceptionHandler(): AnalyticsExceptionHandler { + return Analytics + } + @Singleton @Provides fun provideParamsInterceptorHolder(): ParamsInterceptorHolder { diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 67b61d21e3..046b05e60e 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -3,22 +3,10 @@ "name": "NEW_CARD_SCANNING_ENABLED", "version": "undefined" }, - { - "name": "WC_SOLANA_TX_SIGN_ENABLED", - "version": "5.18.0" - }, - { - "name": "IS_ETHEREUM_EIP_1559_ENABLED", - "version": "5.17.0" - }, { "name": "ONRAMP_ENABLED", "version": "undefined" }, - { - "name": "MAIN_ACTION_BUTTONS_ENABLED", - "version": "5.19.0" - }, { "name": "ONBOARDING_CODE_REFACTORING_ENABLED", "version": "5.22.0" @@ -27,26 +15,14 @@ "name": "NAVIGATION_REFACTORING", "version": "undefined" }, - { - "name": "SWAP_STORIES_ENABLED", - "version": "5.21.0" - }, { "name": "VISA_ONBOARDING_ENABLED", "version": "undefined" }, - { - "name": "ONRAMP_HOT_TOKENS_ENABLED", - "version": "5.21.0" - }, { "name": "STAKING_TON_ENABLED", "version": "undefined" }, - { - "name": "BALANCES_CACHING_ENABLED", - "version": "5.21.0" - }, { "name": "NFT_ENABLED", "version": "undefined" @@ -65,6 +41,26 @@ }, { "name": "NOTE_REFACTORING_ENABLED", + "version": "5.23.0" + }, + { + "name": "NEW_ATTESTATION_ENABLED", + "version": "undefined" + }, + { + "name": "TWIN_REFACTORING_ENABLED", + "version": "undefined" + }, + { + "name": "SEND_V2_ENABLED", + "version": "undefined" + }, + { + "name": "WALLET_CONNECT_REDESIGN_ENABLED", + "version": "undefined" + }, + { + "name": "NETWORKS_LOADING_REFACTORING_ENABLED", "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt index dd567e31f4..e15e7ce4e3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemTech.kt @@ -19,13 +19,13 @@ internal class TangemTech( private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://api.tangem-tech.com/v1/", + baseUrl = "https://api.tangem.org/v1/", headers = createHeaders(), ) private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.DEV, - baseUrl = "https://devapi.tangem-tech.com/v1/", + baseUrl = "[REDACTED_ENV_URL]", headers = createHeaders(), ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt index 885b52ca28..6f9fbc020a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapter.kt @@ -1,16 +1,18 @@ package com.tangem.datasource.api.common.response +import com.tangem.core.analytics.api.AnalyticsErrorHandler import retrofit2.Call import retrofit2.CallAdapter import java.lang.reflect.Type internal class ApiResponseCallAdapter( private val resultType: Type, + private val analyticsErrorHandler: AnalyticsErrorHandler, ) : CallAdapter>> { override fun responseType(): Type = resultType override fun adapt(call: Call): Call> { - return ApiResponseCallDelegate(call) + return ApiResponseCallDelegate(call, analyticsErrorHandler) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt index 8f8c65e92d..b3bd217b7b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallAdapterFactory.kt @@ -1,12 +1,15 @@ package com.tangem.datasource.api.common.response +import com.tangem.core.analytics.api.AnalyticsErrorHandler import retrofit2.Call import retrofit2.CallAdapter import retrofit2.Retrofit import java.lang.reflect.ParameterizedType import java.lang.reflect.Type -class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory() { +class ApiResponseCallAdapterFactory private constructor( + private val analyticsErrorHandler: AnalyticsErrorHandler, +) : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? { if (getRawType(returnType) != Call::class.java) { @@ -19,11 +22,11 @@ class ApiResponseCallAdapterFactory private constructor() : CallAdapter.Factory( } val resultType = getParameterUpperBound(0, callType as ParameterizedType) - return ApiResponseCallAdapter(resultType) + return ApiResponseCallAdapter(resultType, analyticsErrorHandler) } companion object { - fun create() = ApiResponseCallAdapterFactory() + fun create(analyticsErrorHandler: AnalyticsErrorHandler) = ApiResponseCallAdapterFactory(analyticsErrorHandler) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt index 919be8e848..50ee235c7b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -1,5 +1,6 @@ package com.tangem.datasource.api.common.response +import com.tangem.core.analytics.api.AnalyticsErrorHandler import okhttp3.Request import okio.Timeout import retrofit2.Call @@ -9,6 +10,7 @@ import timber.log.Timber internal class ApiResponseCallDelegate( private val wrappedCall: Call, + private val analyticsErrorHandler: AnalyticsErrorHandler, ) : Call> { override fun enqueue(callback: Callback>) { @@ -16,7 +18,7 @@ internal class ApiResponseCallDelegate( } override fun execute(): Response> = throw NotImplementedError() - override fun clone(): Call> = ApiResponseCallDelegate(wrappedCall.clone()) + override fun clone(): Call> = ApiResponseCallDelegate(wrappedCall.clone(), analyticsErrorHandler) override fun request(): Request = wrappedCall.request() override fun timeout(): Timeout = wrappedCall.timeout() override fun isExecuted(): Boolean = wrappedCall.isExecuted @@ -30,7 +32,7 @@ internal class ApiResponseCallDelegate( ) : Callback { override fun onResponse(call: Call, response: Response) { - val safeResponse = response.toSafeApiResponse() + val safeResponse = response.toSafeApiResponse(analyticsErrorHandler) responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt index 22162b76b4..10ec8add5d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -19,51 +19,51 @@ sealed class ApiResponseError : Exception() { ) : ApiResponseError() { // region Error Codes - enum class Code(val code: Int) { + enum class Code(val numericCode: Int) { // 4xx Server Errors - BAD_REQUEST(code = 400), - UNAUTHORIZED(code = 401), - PAYMENT_REQUIRED(code = 402), - FORBIDDEN(code = 403), - NOT_FOUND(code = 404), - METHOD_NOT_ALLOWED(code = 405), - NOT_ACCEPTABLE(code = 406), - PROXY_AUTHENTICATION_REQUIRED(code = 407), - REQUEST_TIMEOUT(code = 408), - CONFLICT(code = 409), - GONE(code = 410), - LENGTH_REQUIRED(code = 411), - PRECONDITION_FAILED(code = 412), - PAYLOAD_TOO_LARGE(code = 413), - URI_TOO_LONG(code = 414), - UNSUPPORTED_MEDIA_TYPE(code = 415), - RANGE_NOT_SATISFIABLE(code = 416), - EXPECTATION_FAILED(code = 417), - IM_A_TEAPOT(code = 418), // Not an error, but an April Fools' joke from RFC 2324 - UNPROCESSABLE_ENTITY(code = 422), - LOCKED(code = 423), - FAILED_DEPENDENCY(code = 424), - TOO_EARLY(code = 425), - UPGRADE_REQUIRED(code = 426), - PRECONDITION_REQUIRED(code = 428), - TOO_MANY_REQUESTS(code = 429), - REQUEST_HEADER_FIELDS_TOO_LARGE(code = 431), - UNAVAILABLE_FOR_LEGAL_REASONS(code = 451), + BAD_REQUEST(numericCode = 400), + UNAUTHORIZED(numericCode = 401), + PAYMENT_REQUIRED(numericCode = 402), + FORBIDDEN(numericCode = 403), + NOT_FOUND(numericCode = 404), + METHOD_NOT_ALLOWED(numericCode = 405), + NOT_ACCEPTABLE(numericCode = 406), + PROXY_AUTHENTICATION_REQUIRED(numericCode = 407), + REQUEST_TIMEOUT(numericCode = 408), + CONFLICT(numericCode = 409), + GONE(numericCode = 410), + LENGTH_REQUIRED(numericCode = 411), + PRECONDITION_FAILED(numericCode = 412), + PAYLOAD_TOO_LARGE(numericCode = 413), + URI_TOO_LONG(numericCode = 414), + UNSUPPORTED_MEDIA_TYPE(numericCode = 415), + RANGE_NOT_SATISFIABLE(numericCode = 416), + EXPECTATION_FAILED(numericCode = 417), + IM_A_TEAPOT(numericCode = 418), // Not an error, but an April Fools' joke from RFC 2324 + UNPROCESSABLE_ENTITY(numericCode = 422), + LOCKED(numericCode = 423), + FAILED_DEPENDENCY(numericCode = 424), + TOO_EARLY(numericCode = 425), + UPGRADE_REQUIRED(numericCode = 426), + PRECONDITION_REQUIRED(numericCode = 428), + TOO_MANY_REQUESTS(numericCode = 429), + REQUEST_HEADER_FIELDS_TOO_LARGE(numericCode = 431), + UNAVAILABLE_FOR_LEGAL_REASONS(numericCode = 451), // 5xx Server Errors - INTERNAL_SERVER_ERROR(code = 500), - NOT_IMPLEMENTED(code = 501), - BAD_GATEWAY(code = 502), - SERVICE_UNAVAILABLE(code = 503), - GATEWAY_TIMEOUT(code = 504), - HTTP_VERSION_NOT_SUPPORTED(code = 505), - VARIANT_ALSO_NEGOTIATES(code = 506), - INSUFFICIENT_STORAGE(code = 507), - LOOP_DETECTED(code = 508), - NOT_EXTENDED(code = 510), - NETWORK_AUTHENTICATION_REQUIRED(code = 511), + INTERNAL_SERVER_ERROR(numericCode = 500), + NOT_IMPLEMENTED(numericCode = 501), + BAD_GATEWAY(numericCode = 502), + SERVICE_UNAVAILABLE(numericCode = 503), + GATEWAY_TIMEOUT(numericCode = 504), + HTTP_VERSION_NOT_SUPPORTED(numericCode = 505), + VARIANT_ALSO_NEGOTIATES(numericCode = 506), + INSUFFICIENT_STORAGE(numericCode = 507), + LOOP_DETECTED(numericCode = 508), + NOT_EXTENDED(numericCode = 510), + NETWORK_AUTHENTICATION_REQUIRED(numericCode = 511), ; - override fun toString(): String = "$code - $name" + override fun toString(): String = "$numericCode - $name" companion object { val values = values() diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index f696275bc3..1699dbc089 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -1,5 +1,7 @@ package com.tangem.datasource.api.common.response +import com.tangem.core.analytics.api.AnalyticsErrorHandler +import com.tangem.datasource.api.common.response.analytics.ApiErrorEvent import kotlinx.coroutines.TimeoutCancellationException import retrofit2.Response import timber.log.Timber @@ -9,18 +11,20 @@ import java.net.UnknownHostException import java.util.concurrent.TimeoutException import javax.net.ssl.SSLHandshakeException -internal fun Response.toSafeApiResponse(): ApiResponse { +internal fun Response.toSafeApiResponse(analyticsErrorHandler: AnalyticsErrorHandler): ApiResponse { val body = body() return if (isSuccessful && body != null) { apiSuccess(body) } else { val code = ApiResponseError.HttpException.Code.values - .firstOrNull { it.code == code() } + .firstOrNull { it.numericCode == code() } val e = try { if (code == null) { ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}")) } else { + sendHttpError(code, analyticsErrorHandler) + ApiResponseError.HttpException(code, message(), errorBody()?.string()) } } catch (e: Exception) { @@ -32,6 +36,21 @@ internal fun Response.toSafeApiResponse(): ApiResponse { } } +private fun Response.sendHttpError( + code: ApiResponseError.HttpException.Code, + analyticsErrorHandler: AnalyticsErrorHandler, +) { + val fullRequestUrl = raw().request.url.toUrl() + val shortUrl = fullRequestUrl.authority + fullRequestUrl.path + analyticsErrorHandler.sendErrorEvent( + ApiErrorEvent( + endpoint = shortUrl, + code = code.numericCode, + message = errorBody()?.string().orEmpty(), + ), + ) +} + internal fun Throwable.toApiError(): ApiResponseError = when (this) { is ConnectException, is UnknownHostException, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/analytics/ApiErrorEvent.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/analytics/ApiErrorEvent.kt new file mode 100644 index 0000000000..c01da30063 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/analytics/ApiErrorEvent.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.api.common.response.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal data class ApiErrorEvent( + val endpoint: String, + val code: Int, + val message: String, +) : AnalyticsEvent( + category = "Tangem API Service", + event = "Exception", + params = mapOf( + "Endpoint" to endpoint, + "Code" to code.toString(), + "Message" to message, + ), +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index 3dbf406f16..b2af512cda 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -20,7 +20,7 @@ data class TokenMarketListResponse( @Json(name = "name") val name: String, @Json(name = "symbol") val symbol: String, @Json(name = "current_price") val currentPrice: BigDecimal, - @Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage, + @Json(name = "price_change_percentage") val priceChangePercentage: PriceChangePercentage?, @Json(name = "market_rating") val marketRating: Int?, @Json(name = "market_cap") val marketCap: BigDecimal?, @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, @@ -28,9 +28,9 @@ data class TokenMarketListResponse( @JsonClass(generateAdapter = true) data class PriceChangePercentage( - @Json(name = "24h") val h24: BigDecimal, - @Json(name = "1w") val week1: BigDecimal, - @Json(name = "30d") val day30: BigDecimal, + @Json(name = "24h") val h24: BigDecimal?, + @Json(name = "1w") val week1: BigDecimal?, + @Json(name = "30d") val day30: BigDecimal?, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt index a83ebdd931..121730cdcf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.api.stakekit.models.response.model import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.datasource.api.stakekit.models.request.Address +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.PendingAction.PendingActionArgs.Amount import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import org.joda.time.DateTime import java.math.BigDecimal @@ -31,6 +32,8 @@ data class BalanceDTO( val pricePerShare: BigDecimal, @Json(name = "pendingActions") val pendingActions: List, + @Json(name = "pendingActionConstraints") + val pendingActionConstraints: List?, @Json(name = "token") val tokenDTO: TokenDTO, @Json(name = "validatorAddress") @@ -99,7 +102,7 @@ data class BalanceDTO( @JsonClass(generateAdapter = true) data class Amount( @Json(name = "required") - val required: Boolean, + val required: Boolean = true, @Json(name = "minimum") val minimum: BigDecimal?, @Json(name = "maximum") @@ -136,6 +139,14 @@ data class BalanceDTO( } } + @JsonClass(generateAdapter = true) + data class PendingActionConstraints( + @Json(name = "type") + val type: StakingActionTypeDTO, + @Json(name = "amount") + val amountArg: Amount, + ) + @JsonClass(generateAdapter = true) data class Required( @Json(name = "required") 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 29dbfa7630..dd14d5f63a 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 @@ -2,6 +2,7 @@ package com.tangem.datasource.di import android.content.Context import com.squareup.moshi.Moshi +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiConfigs @@ -61,6 +62,7 @@ internal object NetworkModule { fun provideExpressApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, ): TangemExpressApi { @@ -69,6 +71,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), @@ -83,6 +86,7 @@ internal object NetworkModule { @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, apiConfigsManager: ApiConfigsManager, + analyticsErrorHandler: AnalyticsErrorHandler, appLogsStore: AppLogsStore, ): StakeKitApi { return createApi( @@ -90,6 +94,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, timeouts = Timeouts( callTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, connectTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS, @@ -109,6 +114,7 @@ internal object NetworkModule { fun provideOnrampApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, ): OnrampApi { @@ -117,6 +123,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), @@ -130,6 +137,7 @@ internal object NetworkModule { fun provideTangemTechApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, ): TangemTechApi { return createApi( @@ -137,6 +145,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { applyTimeoutAnnotations() }, ) } @@ -147,6 +156,7 @@ internal object NetworkModule { fun provideTangemTechApiV2( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, appVersionProvider: AppVersionProvider, ): TangemTechApiV2 { return provideTangemTechApiInternal( @@ -154,6 +164,7 @@ internal object NetworkModule { context = context, appVersionProvider = appVersionProvider, baseUrl = PROD_V2_TANGEM_TECH_BASE_URL, + analyticsErrorHandler = analyticsErrorHandler, ) } @@ -162,6 +173,7 @@ internal object NetworkModule { fun provideTangemTechMarketsApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, ): TangemTechMarketsApi { return createApi( @@ -169,6 +181,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { this.callTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) .connectTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS) @@ -183,6 +196,7 @@ internal object NetworkModule { fun provideTangemVisaAuthApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, ): TangemVisaAuthApi { @@ -191,6 +205,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), @@ -204,6 +219,7 @@ internal object NetworkModule { fun provideTangemVisaApi( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, + analyticsErrorHandler: AnalyticsErrorHandler, apiConfigsManager: ApiConfigsManager, appLogsStore: AppLogsStore, ): TangemVisaApi { @@ -212,6 +228,7 @@ internal object NetworkModule { moshi = moshi, context = context, apiConfigsManager = apiConfigsManager, + analyticsErrorHandler = analyticsErrorHandler, clientBuilder = { addInterceptor( NetworkLogsSaveInterceptor(appLogsStore), @@ -226,6 +243,7 @@ internal object NetworkModule { context: Context, appVersionProvider: AppVersionProvider, baseUrl: String, + analyticsErrorHandler: AnalyticsErrorHandler, timeouts: Timeouts = Timeouts(), requestHeaders: List = listOf(AppVersionPlatformHeaders(appVersionProvider)), ): T { @@ -257,7 +275,7 @@ internal object NetworkModule { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) .baseUrl(baseUrl) .client(client) .build() @@ -269,6 +287,7 @@ internal object NetworkModule { moshi: Moshi, context: Context, apiConfigsManager: ApiConfigsManager, + analyticsErrorHandler: AnalyticsErrorHandler, timeouts: Timeouts = Timeouts(), clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this }, ): T { @@ -276,7 +295,7 @@ internal object NetworkModule { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .addCallAdapterFactory(ApiResponseCallAdapterFactory.create()) + .addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler)) .baseUrl(environmentConfig.baseUrl) .client( OkHttpClient.Builder() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index ac6d80ef03..60e30ca5e7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -4,18 +4,15 @@ import android.content.Context import com.tangem.datasource.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.joda.time.DateTime import org.joda.time.format.DateTimeFormatterBuilder import timber.log.Timber -import java.io.BufferedWriter -import java.io.File -import java.io.FileWriter +import java.io.* +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream import javax.inject.Inject import javax.inject.Singleton @@ -30,7 +27,7 @@ import javax.inject.Singleton @Singleton class AppLogsStore @Inject constructor( @ApplicationContext private val applicationContext: Context, - dispatchers: CoroutineDispatcherProvider, + private val dispatchers: CoroutineDispatcherProvider, ) { private val scope = CoroutineScope( @@ -38,8 +35,10 @@ class AppLogsStore @Inject constructor( CoroutineExceptionHandler { _, error -> Timber.e("AppLogsStore.scope is failed $error") }, ) private val mutex = Mutex() + private val zipMutex = Mutex() - private val file = File(applicationContext.filesDir, NEW_LOG_FILE_NAME) + private val file = File(applicationContext.filesDir, PERMITTED_FILE_NAME) + private val fileZip = File(applicationContext.filesDir, PERMITTED_FILE_NAME_ZIP) private val formatter = DateTimeFormatterBuilder() .appendDayOfMonth(2) @@ -58,6 +57,16 @@ class AppLogsStore @Inject constructor( /** Get log file */ fun getFile(): File? = if (file.exists()) file else null + suspend fun getZipFile(): File? { + return zipMutex.withLock { + if (file.exists()) { + zip(listOf(file), fileZip) + } else { + null + } + } + } + /** Save log [message] */ fun saveLogMessage(tag: String, message: String) { // Temporally logs are not saved in prod environment @@ -133,8 +142,41 @@ class AppLogsStore @Inject constructor( } } + @Suppress("NestedBlockDepth") + private suspend fun zip(filesToCompress: List, outputZipFile: File): File? { + return withContext(dispatchers.io) { + if (outputZipFile.exists() && !outputZipFile.delete()) { + return@withContext null + } + + val buffer = ByteArray(BUFFER_SIZE) + + FileOutputStream(outputZipFile).use { fos -> + ZipOutputStream(fos).use { zos -> + filesToCompress.forEach { file -> + FileInputStream(file).use { inStream -> + val ze = ZipEntry(file.name) + zos.putNextEntry(ze) + var len: Int + while (inStream.read(buffer).also { len = it } > 0) { + zos.write(buffer, 0, len) + } + } + } + zos.finish() // Ensures the zip output is finalized + } + } + outputZipFile + } + } + private companion object { + const val BUFFER_SIZE = 1024 + const val LOG_FILE_NAME = "logs.txt" const val NEW_LOG_FILE_NAME = "app_logs.txt" + // the only name that we allow to send as email to company addresses + const val PERMITTED_FILE_NAME = "log.txt" + const val PERMITTED_FILE_NAME_ZIP = "log.zip" } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt index 43d82aaa8d..a418b31a5d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTPersistenceStore.kt @@ -12,7 +12,7 @@ internal class DefaultNFTPersistenceStore( private val pricesPersistenceStore: DataStore>, ) : NFTPersistenceStore { - override fun getCollections(): Flow> = collectionsPersistenceStore.data + override fun getCollections(): Flow?> = collectionsPersistenceStore.data override suspend fun getCollectionsSync(): List? = collectionsPersistenceStore .data diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt index ecd7275a6a..3a6b27d9f3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStore.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.nft.models.NFTCollection import kotlinx.coroutines.flow.Flow interface NFTPersistenceStore { - fun getCollections(): Flow> + fun getCollections(): Flow?> suspend fun getCollectionsSync(): List? diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt index 49d0ce8415..cb2e6399bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/NFTPersistenceStoreFactory.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.nft import android.content.Context +import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi @@ -11,10 +12,12 @@ import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.listTypes import com.tangem.datasource.utils.mapWithCustomKeyTypes import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import java.lang.reflect.ParameterizedType import javax.inject.Inject import javax.inject.Singleton @@ -25,41 +28,51 @@ class NFTPersistenceStoreFactory @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) { - fun provide(network: Network): NFTPersistenceStore { - val networkStringIdentifier = listOfNotNull( - network.id.value, - network.derivationPath.value, - ).joinToString("_") { - // remove all non-alphanumeric characters - it - .toCharArray() - .filter(Char::isLetterOrDigit) - .joinToString("") - .lowercase() - } + fun provide(userWalletId: UserWalletId, network: Network): NFTPersistenceStore { + // simplify network identifier so that is correct for a file name + // e.g. eth_m4460000 or theopennetwork_m446070 + val networkStringId = + network.id.formatted() + network.derivationPath.formatted()?.let { "_$it" }.orEmpty() + // simplify user wallet id so that is correct for a file name + // e.g. 9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a + val userWalletStringId = userWalletId.formatted() return DefaultNFTPersistenceStore( - collectionsPersistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = listTypes(), - defaultValue = emptyList(), - ), - produceFile = { - context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_collections") - }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + collectionsPersistenceStore = createPersistenceStore( + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_collections + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_collections + fileName = "nft_${userWalletStringId}_${networkStringId}_collections", + types = listTypes(), + defaultValue = emptyList(), ), - pricesPersistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = mapWithCustomKeyTypes(), - defaultValue = emptyMap(), - ), - produceFile = { - context.dataStoreFile(fileName = "nft_${networkStringIdentifier}_prices") - }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + pricesPersistenceStore = createPersistenceStore( + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_eth_m4460000_prices + // result file name example: nft_9a1a178f951a7115555568c09ebad8a882f3d96de25429f0017fe570931e208a_theopennetwork_m446070_prices + fileName = "nft_${userWalletStringId}_${networkStringId}_prices", + types = mapWithCustomKeyTypes(), + defaultValue = emptyMap(), ), ) } + + private fun createPersistenceStore(fileName: String, types: ParameterizedType, defaultValue: T): DataStore = + DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = types, + defaultValue = defaultValue, + ), + produceFile = { context.dataStoreFile(fileName = fileName) }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + + private fun Network.ID.formatted(): String = value + .filter(Char::isLetterOrDigit) + .lowercase() + + private fun Network.DerivationPath.formatted(): String? = value + ?.filter(Char::isLetterOrDigit) + ?.lowercase() + + private fun UserWalletId.formatted(): String = stringValue + .lowercase() } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt index ce67e88dd6..add09e2c0a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt @@ -7,14 +7,11 @@ import com.tangem.domain.tokens.model.Network import com.tangem.utils.converter.Converter import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset -class NFTSdkAssetConverter( - private val nftSdkAssetIdentifierConverter: NFTSdkAssetIdentifierConverter, - private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter, -) : Converter, NFTAsset> { +object NFTSdkAssetConverter : Converter, NFTAsset> { override fun convert(value: Pair): NFTAsset { val (network, asset) = value - val assetId = nftSdkAssetIdentifierConverter.convert(asset.identifier) - val collectionId = nftSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier) + val assetId = NFTSdkAssetIdentifierConverter.convert(asset.identifier) + val collectionId = NFTSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier) return NFTAsset.Value( id = assetId, collectionId = collectionId, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt index 7b1be28a4e..ed9a5ed3ec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkCollectionConverter.kt @@ -6,13 +6,10 @@ import com.tangem.domain.tokens.model.Network import com.tangem.utils.converter.Converter import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection -class NFTSdkCollectionConverter( - private val nftSdkCollectionIdentifierConverter: NFTSdkCollectionIdentifierConverter, - private val nftSdkAssetConverter: NFTSdkAssetConverter, -) : Converter, NFTCollection> { +object NFTSdkCollectionConverter : Converter, NFTCollection> { override fun convert(value: Pair): NFTCollection { val (network, collection) = value - val collectionId = nftSdkCollectionIdentifierConverter.convert(collection.identifier) + val collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier) return NFTCollection( id = collectionId, network = network, @@ -22,7 +19,7 @@ class NFTSdkCollectionConverter( count = collection.count, assets = collection.assets .map { asset -> - nftSdkAssetConverter.convert(network to asset) + NFTSdkAssetConverter.convert(network to asset) } .filter { it.id !is NFTAsset.Identifier.Unknown diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt new file mode 100644 index 0000000000..2284023080 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConstraintsConverter.kt @@ -0,0 +1,20 @@ +package com.tangem.datasource.local.token.converter + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.PendingActionConstraints +import com.tangem.utils.converter.Converter + +internal object PendingActionConstraintsConverter : + Converter { + override fun convert(value: BalanceDTO.PendingActionConstraints): PendingActionConstraints { + return PendingActionConstraints( + type = StakingActionTypeConverter.convert(value.type), + amountArg = PendingAction.PendingActionArgs.Amount( + required = value.amountArg.required, + minimum = value.amountArg.minimum, + maximum = value.amountArg.maximum, + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index 6fc465801a..051e63780e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -38,6 +38,8 @@ internal class YieldBalanceConverter( pendingActions = PendingActionConverter .convertList(item.pendingActions) .sortedBy { it.passthrough }, + pendingActionsConstraints = PendingActionConstraintsConverter + .convertList(item.pendingActionConstraints.orEmpty()), isPending = false, ) } diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index a58ab9efa6..9097d47c30 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -152,7 +152,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { id = ApiConfig.ID.TangemTech, expected = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://api.tangem-tech.com/v1/", + baseUrl = "https://api.tangem.org/v1/", headers = mapOf( "card_id" to ProviderSuspend { APP_CARD_ID }, "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt index 99230c1853..131ca91e00 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -14,6 +14,11 @@ fun SpacerH(height: Dp, modifier: Modifier = Modifier) { Spacer(modifier = modifier.height(height)) } +@Composable +fun SpacerH2(modifier: Modifier = Modifier) { + SpacerH(2.dp, modifier) +} + @Composable fun SpacerH4(modifier: Modifier = Modifier) { SpacerH(4.dp, modifier) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt similarity index 88% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt index c96d848bac..19f8e5fcbd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/UnableToLoadData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt @@ -1,4 +1,4 @@ -package com.tangem.features.markets.tokenlist.impl.ui.components +package com.tangem.core.ui.components import android.content.res.Configuration import androidx.compose.foundation.layout.Arrangement @@ -8,16 +8,16 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R @Composable -internal fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { +fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index a9a7677136..4727ba9759 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -109,7 +109,7 @@ private fun BoxScope.ContentIconContainer( ) if (icon.topBadgeIconResId != null && shouldShowTopBadge) { - TopBadge( + CurrencyIconTopBadge( modifier = Modifier .offset(x = networkBadgeOffset, y = -networkBadgeOffset) .align(Alignment.TopEnd), @@ -120,7 +120,7 @@ private fun BoxScope.ContentIconContainer( } if (icon.showCustomBadge) { - BottomBadge( + CurrencyIconBottomBadge( modifier = Modifier.align(Alignment.BottomEnd), ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconIconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconIconBadge.kt new file mode 100644 index 0000000000..b074076629 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconIconBadge.kt @@ -0,0 +1,32 @@ +package com.tangem.core.ui.components.currency.icon + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun CurrencyIconBottomBadge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size12) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + ) { + Box( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing2) + .matchParentSize() + .background( + color = TangemTheme.colors.icon.informative, + shape = CircleShape, + ), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt similarity index 66% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt index 09e597f7a6..03057d495d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource import com.tangem.core.ui.res.TangemTheme @Composable -internal fun TopBadge( +fun CurrencyIconTopBadge( @DrawableRes iconResId: Int, alpha: Float, colorFilter: ColorFilter?, @@ -38,26 +38,4 @@ internal fun TopBadge( contentDescription = null, ) } -} - -@Composable -internal fun BottomBadge(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size12) - .background( - color = TangemTheme.colors.background.primary, - shape = CircleShape, - ), - ) { - Box( - modifier = Modifier - .padding(all = TangemTheme.dimens.spacing2) - .matchParentSize() - .background( - color = TangemTheme.colors.icon.informative, - shape = CircleShape, - ), - ) - } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt new file mode 100644 index 0000000000..b29403e17d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownItem.kt @@ -0,0 +1,47 @@ +package com.tangem.core.ui.components.dropdownmenu + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Suppress("ComposableEventParameterNaming") +@Composable +fun TangemDropdownItem(item: TangemDropdownMenuItem, dismissParent: () -> Unit, modifier: Modifier = Modifier) { + Text( + modifier = modifier + .clickable { + dismissParent() + item.onClick() + } + .padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16), + text = item.title.resolveReference(), + style = TangemTheme.typography.button.copy(color = item.textColorProvider()), + ) +} + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_TokenDetailsAppBarDropdownItem() { + TangemThemePreview { + TangemDropdownItem( + modifier = Modifier.background(TangemTheme.colors.background.primary), + dismissParent = {}, + item = TangemDropdownMenuItem( + title = TextReference.Res(id = R.string.token_details_hide_token), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt similarity index 90% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt index fa6ff8aca3..6974bc2d7a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt @@ -1,11 +1,17 @@ @file:Suppress("TopLevelPropertyNaming") -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components + +package com.tangem.core.ui.components.dropdownmenu import androidx.compose.animation.core.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.TransformOrigin @@ -15,16 +21,19 @@ import androidx.compose.ui.unit.* import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.PopupProperties +import com.tangem.core.ui.res.TangemTheme +import kotlin.math.max +import kotlin.math.min /** * Just copy paste [DropdownMenu] from material3 with deleting vertical paddings. */ @Composable -internal fun TangemDropdownMenu( +fun TangemDropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, - offset: DpOffset = DpOffset(0.dp, 0.dp), + offset: DpOffset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)), properties: PopupProperties = PopupProperties(focusable = true), content: @Composable ColumnScope.() -> Unit, ) { @@ -142,10 +151,7 @@ private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect) menuBounds.width == 0 -> 0f else -> { val intersectionCenter = - ( - kotlin.math.max(parentBounds.left, menuBounds.left) + - kotlin.math.min(parentBounds.right, menuBounds.right) - ) / 2 + (max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2 (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width } } @@ -155,10 +161,7 @@ private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect) menuBounds.height == 0 -> 0f else -> { val intersectionCenter = - ( - kotlin.math.max(parentBounds.top, menuBounds.top) + - kotlin.math.min(parentBounds.bottom, menuBounds.bottom) - ) / 2 + (max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2 (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenuItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenuItem.kt new file mode 100644 index 0000000000..7d239636dd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenuItem.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui.components.dropdownmenu + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.extensions.TextReference + +data class TangemDropdownMenuItem( + val title: TextReference, + val textColorProvider: @Composable () -> Color, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 9863c30ba0..d69616e5d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager @@ -36,7 +37,11 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @Composable -fun SearchBar(state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFieldColors = TangemSearchBarColors) { +fun SearchBar( + state: SearchBarUM, + modifier: Modifier = Modifier, + colors: TextFieldColors = TangemSearchBarDefaults.defaultTextFieldColors, +) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current val interactionSource = remember { MutableInteractionSource() } @@ -161,18 +166,30 @@ private fun ClearButton( } } -private val TangemSearchBarColors: TextFieldColors - @Composable - get() = TextFieldDefaults.colors().copy( - focusedContainerColor = TangemTheme.colors.field.primary, - unfocusedContainerColor = TangemTheme.colors.field.primary, - focusedTextColor = TangemTheme.colors.text.primary1, - unfocusedTextColor = TangemTheme.colors.text.primary1, - cursorColor = TangemTheme.colors.icon.primary1, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ) +object TangemSearchBarDefaults { + val defaultTextFieldColors: TextFieldColors + @Composable @Stable get() = TextFieldDefaults.colors().copy( + focusedContainerColor = TangemTheme.colors.field.primary, + unfocusedContainerColor = TangemTheme.colors.field.primary, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.primary1, + cursorColor = TangemTheme.colors.icon.primary1, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) + val secondaryTextFieldColors: TextFieldColors + @Composable @Stable get() = TextFieldDefaults.colors().copy( + focusedContainerColor = TangemTheme.colors.field.focused, + unfocusedContainerColor = TangemTheme.colors.field.focused, + focusedTextColor = TangemTheme.colors.text.primary1, + unfocusedTextColor = TangemTheme.colors.text.primary1, + cursorColor = TangemTheme.colors.icon.primary1, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledIndicatorColor = Color.Transparent, + ) +} // region Previews @Preview(widthDp = 328) diff --git a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index 02615d9faa..9b16d77616 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -32,7 +32,7 @@ fun Modifier.roundedShapeItemDecoration( val applyShape: Modifier.(shape: RoundedCornerShape?) -> Modifier = { shape -> if (backgroundColor != null) { if (shape != null) { - background(color = backgroundColor, shape = shape) + clip(shape).background(color = backgroundColor, shape = shape) } else { background(color = backgroundColor) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt index 28d531895d..b1ca7da16a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemAnimations.kt @@ -22,9 +22,9 @@ object TangemAnimations { forwardWhen: (initial: S, target: S) -> Boolean, ): AnimatedContentTransitionScope.() -> ContentTransform = { val direction = if (forwardWhen(initialState, targetState)) { - AnimatedContentTransitionScope.SlideDirection.End - } else { AnimatedContentTransitionScope.SlideDirection.Start + } else { + AnimatedContentTransitionScope.SlideDirection.End } slideIntoContainer(towards = direction, animationSpec = tween()) diff --git a/app/src/main/res/drawable/ic_accepted.xml b/core/ui/src/main/res/drawable/ic_accepted_20.xml similarity index 100% rename from app/src/main/res/drawable/ic_accepted.xml rename to core/ui/src/main/res/drawable/ic_accepted_20.xml diff --git a/core/ui/src/main/res/drawable/ic_access_code.xml b/core/ui/src/main/res/drawable/ic_access_code_119.xml similarity index 100% rename from core/ui/src/main/res/drawable/ic_access_code.xml rename to core/ui/src/main/res/drawable/ic_access_code_119.xml diff --git a/core/ui/src/main/res/drawable/ic_nft_placeholder_76.xml b/core/ui/src/main/res/drawable/ic_nft_placeholder_76.xml new file mode 100644 index 0000000000..6e6a840d1f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_nft_placeholder_76.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_unticked.xml b/core/ui/src/main/res/drawable/ic_unticked_20.xml similarity index 100% rename from app/src/main/res/drawable/ic_unticked.xml rename to core/ui/src/main/res/drawable/ic_unticked_20.xml diff --git a/core/ui/src/main/res/drawable/img_wallet_connect_76.xml b/core/ui/src/main/res/drawable/img_wallet_connect_76.xml new file mode 100644 index 0000000000..a24b3060f6 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_wallet_connect_76.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt index 9c271d0920..51b347095e 100644 --- a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt +++ b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt @@ -1,8 +1,15 @@ package com.tangem.utils.transformer +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + /** * Transforms state to updated state. */ interface Transformer { fun transform(prevState: S): S +} + +fun MutableStateFlow.update(transformer: Transformer) { + update { transformer.transform(this.value) } } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 17f8dca358..028f1697e1 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -94,6 +94,8 @@ internal class DefaultFeedbackRepository( override fun getLogFile(): File? = appLogsStore.getFile() + override suspend fun getZipLogFile(): File? = appLogsStore.getZipFile() + override fun sendEmail(feedbackEmail: FeedbackEmail) { emailSender.send( EmailSender.Email( diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 4b2f305bcf..91c229e3ef 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -335,7 +335,7 @@ internal class DefaultMarketsTokenRepository( ): T { return when (error) { is ApiResponseError.HttpException -> { - createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.code, error.message.orEmpty()) + createEvent(MarketsDataAnalyticsEvent.Type.Http, error.code.numericCode, error.message.orEmpty()) } is ApiResponseError.TimeoutException -> { createEvent(MarketsDataAnalyticsEvent.Type.Timeout, null, error.message.orEmpty()) diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 38d39f1843..2091fcf399 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -28,9 +28,9 @@ internal object TokenMarketListConverter : Converter() - private val nftRuntimeStores = mutableMapOf() - private val nftPersistenceStores = mutableMapOf() - - private val nftSdkAssetConverter = NFTSdkAssetConverter( - nftSdkAssetIdentifierConverter = NFTSdkAssetIdentifierConverter, - nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter, - ) - private val collectionConverter = NFTSdkCollectionConverter( - nftSdkCollectionIdentifierConverter = NFTSdkCollectionIdentifierConverter, - nftSdkAssetConverter = nftSdkAssetConverter, - ) + private val nftRuntimeStores = mutableMapOf() + private val nftPersistenceStores = mutableMapOf() override fun observeCollections(userWalletId: UserWalletId, networks: List): Flow> = - flow { - emitAll( - combine( - networks.map { getNFTRuntimeStore(it).getCollections() }, - ) { it.asList() }, - ) - }.onStart { - refreshCollections(userWalletId, networks) - } + flow { emitAll(observeCollectionsInternal(userWalletId, networks)) } + + private suspend fun observeCollectionsInternal( + userWalletId: UserWalletId, + networks: List, + ): Flow> = combine( + networks.map { observeCollectionsInternal(userWalletId, it) }, + ) { it.asList() } + + private suspend fun observeCollectionsInternal( + userWalletId: UserWalletId, + network: Network, + ): Flow = if (network.canHandleNFTs()) { + getNFTRuntimeStore(userWalletId, network).getCollections() + } else { + flowOf(NFTCollections.empty(network)) + } override suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) = coroutineScope { - networks.map { network -> - launch(dispatchers.io) { - Either.catch { - expireCollections(network) - walletManagersFacade.getNFTCollections(userWalletId, network) - }.onLeft { - saveFailedStateInRuntime( - network = network, - error = it, - ) - }.onRight { - val mergedCollections = it.mergeWithStoredAssets(network) + networks.mapNotNull { network -> + if (network.canHandleNFTs()) { + launch(dispatchers.io) { + Either.catch { + expireCollections(userWalletId, network) + walletManagersFacade.getNFTCollections(userWalletId, network) + }.onLeft { + saveFailedStateInRuntime( + userWalletId = userWalletId, + network = network, + error = it, + ) + }.onRight { + val mergedCollections = it.mergeWithStoredAssets(userWalletId, network) - saveCollectionsInRuntime( - network = network, - collections = mergedCollections, - ) - saveCollectionsInPersistence( - network = network, - collections = mergedCollections, - ) - } - }.saveIn(getJobHolder(network)) + saveCollectionsInRuntime( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + saveCollectionsInPersistence( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + } + }.saveIn(getJobHolder(network)) + } else { + null + } }.joinAll() } - private suspend fun expireCollections(network: Network) { - val runtimeStore = getNFTRuntimeStore(network) - + private suspend fun expireCollections(userWalletId: UserWalletId, network: Network) { + val runtimeStore = getNFTRuntimeStore(userWalletId, network) val expiredCollections = runtimeStore .getCollectionsSync() - .let { collections -> - collections.copy( - content = when (val content = collections.content) { - is NFTCollections.Content.Collections -> content.copy( - source = StatusSource.CACHE, - ) - is NFTCollections.Content.Error -> content - }, - ) - } + .changeStatusSource(StatusSource.CACHE) runtimeStore.saveCollections(expiredCollections) } - private suspend fun saveCollectionsInRuntime(network: Network, collections: List) { - getNFTRuntimeStore(network).saveCollections( + private suspend fun saveCollectionsInRuntime( + userWalletId: UserWalletId, + network: Network, + collections: List, + ) { + getNFTRuntimeStore(userWalletId, network).saveCollections( NFTCollections( network = network, content = NFTCollections.Content.Collections( collections = collections .map { collection -> - collectionConverter.convert(network to collection) + NFTSdkCollectionConverter.convert(network to collection) } .filter { it.id !is NFTCollection.Identifier.Unknown @@ -120,19 +122,30 @@ internal class DefaultNFTRepository @Inject constructor( ) } - private suspend fun saveFailedStateInRuntime(network: Network, error: Throwable) { - getNFTRuntimeStore(network).saveCollections( - NFTCollections( - network = network, - content = NFTCollections.Content.Error( - error = error, - ), - ), - ) + private suspend fun saveFailedStateInRuntime(userWalletId: UserWalletId, network: Network, error: Throwable) { + getNFTRuntimeStore(userWalletId, network).let { store -> + val storedCollections = store.getCollectionsSync() + val content = storedCollections.content + val updatedCollections = if (content is NFTCollections.Content.Collections && content.collections != null) { + // if there is any cached collections in store, then mark them as not actual and emit anyway + storedCollections.changeStatusSource(StatusSource.ONLY_CACHE) + } else { + // otherwise, just emit an error + NFTCollections( + network = network, + content = NFTCollections.Content.Error(error), + ) + } + store.saveCollections(updatedCollections) + } } - private suspend fun saveCollectionsInPersistence(network: Network, collections: List) { - getNFTPersistenceStore(network).saveCollections(collections) + private suspend fun saveCollectionsInPersistence( + userWalletId: UserWalletId, + network: Network, + collections: List, + ) { + getNFTPersistenceStore(userWalletId, network).saveCollections(collections) } private fun getJobHolder(network: Network): JobHolder = jobs[network] ?: run { @@ -141,65 +154,85 @@ internal class DefaultNFTRepository @Inject constructor( } } - private fun getNFTPersistenceStore(network: Network): NFTPersistenceStore = nftPersistenceStores[network] ?: run { - nftPersistenceStoreFactory.provide(network).also { - nftPersistenceStores[network] = it + private fun getNFTPersistenceStore(userWalletId: UserWalletId, network: Network): NFTPersistenceStore { + val storeId = (userWalletId to network).formatted() + return nftPersistenceStores[storeId] ?: run { + nftPersistenceStoreFactory.provide(userWalletId, network).also { + nftPersistenceStores[storeId] = it + } } } - private suspend fun getNFTRuntimeStore(network: Network): NFTRuntimeStore = nftRuntimeStores[network] ?: run { - nftRuntimeStoreFactory.provide(network).also { - nftRuntimeStores[network] = it - val storedCollections = getStoredCollections(network) - val storedPrices = getStoredPrices(network) - it.initialize( - collections = storedCollections, - prices = storedPrices, - ) + private suspend fun getNFTRuntimeStore(userWalletId: UserWalletId, network: Network): NFTRuntimeStore { + val storeId = (userWalletId to network).formatted() + return nftRuntimeStores[storeId] ?: run { + nftRuntimeStoreFactory.provide(network).also { + nftRuntimeStores[storeId] = it + val storedCollections = getStoredCollections(userWalletId, network) + val storedPrices = getStoredPrices(userWalletId, network) + it.initialize( + collections = storedCollections, + prices = storedPrices, + ) + } } } - private suspend fun getStoredCollections(network: Network) = getNFTPersistenceStore(network) - .getCollectionsSync() - .let { - NFTCollections( - network = network, - content = NFTCollections.Content.Collections( - collections = it - ?.map { collection -> - collectionConverter.convert(network to collection) - } - ?.filter { - it.id !is NFTCollection.Identifier.Unknown - }, - source = StatusSource.CACHE, - ), - ) - } - - private suspend fun getStoredPrices(network: Network) = getNFTPersistenceStore(network) - .getSalePricesSync() - .orEmpty() - .let { prices -> - prices - .mapKeys { - val (assetId, _) = it - NFTSdkAssetIdentifierConverter.convert(assetId) - } - .mapValues { - val (assetId, price) = it - NFTSalePrice.Value( - assetId = assetId, - value = price.value, - symbol = price.symbol, + private suspend fun getStoredCollections(userWalletId: UserWalletId, network: Network) = + getNFTPersistenceStore(userWalletId, network) + .getCollectionsSync() + .let { + NFTCollections( + network = network, + content = NFTCollections.Content.Collections( + collections = it + ?.map { collection -> + NFTSdkCollectionConverter.convert(network to collection) + } + ?.filter { + it.id !is NFTCollection.Identifier.Unknown + }, source = StatusSource.CACHE, - ) - } - } + ), + ) + } - private suspend fun List.mergeWithStoredAssets(network: Network): List { + private suspend fun getStoredPrices(userWalletId: UserWalletId, network: Network) = + getNFTPersistenceStore(userWalletId, network) + .getSalePricesSync() + .orEmpty() + .let { prices -> + prices + .mapKeys { + val (assetId, _) = it + NFTSdkAssetIdentifierConverter.convert(assetId) + } + .mapValues { + val (assetId, price) = it + NFTSalePrice.Value( + assetId = assetId, + value = price.value, + symbol = price.symbol, + source = StatusSource.CACHE, + ) + } + } + + private fun NFTCollections.changeStatusSource(source: StatusSource) = copy( + content = when (val content = content) { + is NFTCollections.Content.Collections -> content.copy( + source = source, + ) + is NFTCollections.Content.Error -> content + }, + ) + + private suspend fun List.mergeWithStoredAssets( + userWalletId: UserWalletId, + network: Network, + ): List { val storedCollections = - getNFTPersistenceStore(network) + getNFTPersistenceStore(userWalletId, network) .getCollectionsSync() .orEmpty() .associateBy(SdkNFTCollection::identifier) @@ -224,4 +257,11 @@ internal class DefaultNFTRepository @Inject constructor( } } } + + private fun Pair.formatted(): String { + val (walletId, network) = this + return walletId.stringValue + "_" + network.id.value + "_" + network.derivationPath.value + } + + private fun Network.canHandleNFTs(): Boolean = Blockchain.fromNetworkId(backendId)?.canHandleNFTs() == true } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 86807b4905..dd1fb830b0 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -131,7 +131,7 @@ internal class DefaultHotCryptoRepository( analyticsEventHandler.send( event = MainScreenAnalyticsEvent.HotTokenError( - errorCode = (it as? ApiResponseError.HttpException)?.code?.code?.toString().orEmpty(), + errorCode = (it as? ApiResponseError.HttpException)?.code?.numericCode?.toString().orEmpty(), ), ) } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 91b4be5185..3ecf2df253 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -8,6 +8,8 @@ import com.tangem.data.onramp.DefaultOnrampErrorResolver import com.tangem.data.onramp.DefaultOnrampRepository import com.tangem.data.onramp.DefaultOnrampTransactionRepository import com.tangem.data.onramp.converters.error.OnrampErrorConverter +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository +import com.tangem.data.onramp.legacy.MercuryoTopUpRepository import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.api.onramp.OnrampApi @@ -15,6 +17,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore @@ -112,4 +115,16 @@ internal object OnrampDataModule { analyticsEventHandler = analyticsEventHandler, ) } + + @Provides + @Singleton + fun provideMercuryoRepository( + environmentConfigStorage: EnvironmentConfigStorage, + dispatchersProvider: CoroutineDispatcherProvider, + ): LegacyTopUpRepository { + return MercuryoTopUpRepository( + environmentConfigStorage = environmentConfigStorage, + dispatchersProvider = dispatchersProvider, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt rename to data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt index 3c20cb9f8e..aed9a68d2a 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt @@ -1,12 +1,13 @@ -package com.tangem.tap.network.exchangeServices.mercuryo +package com.tangem.data.onramp.legacy import com.tangem.blockchain.common.Blockchain /** + * Legacy onboarding note/twins to get top up URL from mercuryo * Map [Blockchain] to ids from [link](https://api.mercuryo.io/v1.6/lib/currencies) [crypto_currencies] */ @Suppress("CyclomaticComplexMethod") -internal val Blockchain.mercuryoNetwork: String? +public val Blockchain.mercuryoNetwork: String? get() { return when (this) { Blockchain.Algorand -> "ALGORAND" diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt new file mode 100644 index 0000000000..91bd1a5854 --- /dev/null +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt @@ -0,0 +1,47 @@ +package com.tangem.data.onramp.legacy + +import android.net.Uri +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.calculateSha512 +import com.tangem.common.extensions.toHexString +import com.tangem.datasource.local.config.environment.EnvironmentConfig +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class MercuryoTopUpRepository @Inject constructor( + private val environmentConfigStorage: EnvironmentConfigStorage, + private val dispatchersProvider: CoroutineDispatcherProvider, +) : LegacyTopUpRepository { + + override suspend fun getTopUpUrl(cryptoCurrency: CryptoCurrency, walletAddress: String): String = + withContext(dispatchersProvider.default) { + val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val environmentConfig = environmentConfigStorage.getConfigSync() + + val builder = Uri.Builder() + .scheme(LegacyTopUpRepository.SCHEME) + .authority("exchange.mercuryo.io") + .appendQueryParameter("widget_id", environmentConfig.mercuryoWidgetId) + .appendQueryParameter("type", "buy") + .appendQueryParameter("currency", cryptoCurrency.symbol) + .appendQueryParameter("address", walletAddress) + .appendQueryParameter("signature", environmentConfig.signature(walletAddress)) + .appendQueryParameter("fix_currency", "true") + .appendQueryParameter("redirect_url", LegacyTopUpRepository.SUCCESS_URL) + + blockchain.mercuryoNetwork?.let { + builder.appendQueryParameter("network", it) + } + + builder.build().toString() + } + + private fun EnvironmentConfig.signature(address: String) = (address + mercuryoSecret) + .calculateSha512() + .toHexString() + .lowercase() +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index d08938d3fe..9275635df5 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -738,7 +738,7 @@ internal class DefaultStakingRepository( private companion object { const val YIELDS_STORE_KEY = "yields" - const val TON_INTEGRATION_ID = "ton-ton-tonwhales-pools-staking" + const val TON_INTEGRATION_ID = "ton-ton-chorus-one-pools-staking" const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 035040383b..f8999494d2 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.tokens.models) /** Project - Utils */ implementation(projects.core.utils) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 6a78abbf06..b513244dfc 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -43,11 +43,9 @@ internal class DefaultVisaRepository @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val visaApiRequestMaker: VisaApiRequestMaker, private val visaApi: TangemVisaApi, + private val visaCurrencyFactory: VisaCurrencyFactory, ) : VisaRepository { - private val currencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { - VisaCurrencyFactory() - } private val txDetailsFactory by lazy(mode = LazyThreadSafetyMode.NONE) { VisaTxDetailsFactory() } @@ -79,20 +77,22 @@ internal class DefaultVisaRepository @Inject constructor( private suspend fun fetchVisaCurrency(userWalletId: UserWalletId, address: String) { val contractInfoProvider = visaLibLoader.getOrCreateProvider() + val paymentAccountAddress = getPaymentAccountAddress(userWalletId) + val userWallet = findVisaUserWallet(userWalletId) parZip( dispatchers.io, { contractInfoProvider.getContractInfo( walletAddress = address, - paymentAccountAddress = getPaymentAccountAddress(userWalletId), + paymentAccountAddress = paymentAccountAddress, ) }, { getFiatRate() }, { contractInfo, fiatRate -> fetchedCurrencies.update { value -> value.apply { - put(address, currencyFactory.create(contractInfo, fiatRate)) + put(address, visaCurrencyFactory.create(userWallet, contractInfo, fiatRate)) } } }, @@ -157,7 +157,7 @@ internal class DefaultVisaRepository @Inject constructor( ) } - // TODO select correct account when multiple accounts are available (will be implemented when backend is ready) + // There will be only one payment account (backend has filtered for us), so we can take the first one customerInfo.paymentAccounts.firstOrNull()?.paymentAccountAddress }.getOrNull() @@ -187,14 +187,14 @@ internal class DefaultVisaRepository @Inject constructor( } } - private suspend fun getFiatRate(): BigDecimal? { + private suspend fun getFiatRate(): BigDecimal { val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase() val quotes = tangemTechApi.getQuotes( currencyId = fiatCurrencyId, coinIds = VisaConstants.TOKEN_ID, ).getOrThrow() - return quotes.quotes[VisaConstants.TOKEN_ID]?.price + return quotes.quotes[VisaConstants.TOKEN_ID]?.price ?: error("No price found") } private fun makeWalletAddresses(userWallet: UserWallet): Set
{ diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index 989a2d504c..7765fc8608 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -1,16 +1,28 @@ package com.tangem.data.visa.utils +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.getNetwork +import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.visa.model.VisaCurrency +import com.tangem.domain.wallets.models.UserWallet import com.tangem.lib.visa.model.VisaContractInfo import org.joda.time.DateTime import org.joda.time.DateTimeZone import org.joda.time.Instant import java.math.BigDecimal import java.math.BigInteger +import javax.inject.Inject -internal class VisaCurrencyFactory { +internal class VisaCurrencyFactory @Inject constructor( + private val cryptoCurrencyFactory: CryptoCurrencyFactory, + private val excludedBlockchains: ExcludedBlockchains, +) { - fun create(contractInfo: VisaContractInfo, fiatRate: BigDecimal?): VisaCurrency { + fun create(userWallet: UserWallet, contractInfo: VisaContractInfo, fiatRate: BigDecimal): VisaCurrency { val now = Instant.now() val currentLimit = if (contractInfo.limitsChangeDate > now) { contractInfo.oldLimits @@ -19,11 +31,30 @@ internal class VisaCurrencyFactory { } val remainingOtpLimit = getRemainingOtp(currentLimit, now) + val currencyNetwork = getNetwork( + blockchain = Blockchain.Polygon, + extraDerivationPath = null, + derivationStyleProvider = userWallet.scanResponse.derivationStyleProvider, + excludedBlockchains = excludedBlockchains, + canHandleTokens = true, + ) ?: error("Unable to create network for Visa currency") + + val cryptoCurrency = cryptoCurrencyFactory.createToken( + network = currencyNetwork, + rawId = CryptoCurrency.RawID(VisaConstants.TOKEN_ID), + name = contractInfo.token.name, + symbol = contractInfo.token.symbol, + decimals = contractInfo.token.decimals, + contractAddress = contractInfo.token.address, + ) + return VisaCurrency( + cryptoCurrency = cryptoCurrency, symbol = contractInfo.token.symbol, networkName = VisaConstants.NETWORK_NAME, decimals = contractInfo.token.decimals, fiatRate = fiatRate, + priceChange = BigDecimal.ZERO, // [Second Visa Iteration] pass to create() params if needed fiatCurrency = VisaConstants.fiatCurrency, balances = with(contractInfo) { VisaCurrency.Balances( @@ -40,6 +71,12 @@ internal class VisaCurrencyFactory { singleTransaction = currentLimit.singleTransactionLimit, expirationDate = getLimitsExpirationDate(currentLimit, now), ), + paymentAccountAddress = NetworkAddress.Single( + NetworkAddress.Address( + value = contractInfo.paymentAccountAddress, + type = NetworkAddress.Address.Type.Primary, + ), + ), ) } diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index 5dc33697df..fab7b8d0e6 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { /* Project - Domain */ implementation(projects.domain.walletConnect) + implementation(projects.domain.walletConnect.models) implementation(projects.domain.wallets.models) /* Project - Data */ @@ -25,6 +26,15 @@ dependencies { implementation(deps.hilt.core) kapt(deps.hilt.kapt) + /* Reown - WalletConnect */ + implementation(deps.reownCore) { + exclude(group = "app.cash.sqldelight", module = "android-driver") + } + implementation(deps.reownWeb3) { + exclude(group = "app.cash.sqldelight", module = "android-driver") + } + /* Other */ implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt new file mode 100644 index 0000000000..388c5c30a1 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWcSessionsManager.kt @@ -0,0 +1,38 @@ +package com.tangem.data.walletconnect + +import com.reown.walletkit.client.Wallet +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import timber.log.Timber + +internal class DefaultWcSessionsManager : WcSessionsManager, WcSdkObserver { + + private val _sessions = MutableSharedFlow>>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + override val sessions get() = _sessions.distinctUntilChanged() + + override suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) { + TODO("Not yet implemented") + } + + override suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) { + TODO("Not yet implemented") + } + + override suspend fun findSessionByTopic(topic: String): WcSession? { + TODO("Not yet implemented") + } + + override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { + // Triggered when the session is deleted by the peer + Timber.i("onSessionDelete: $sessionDelete") + // todo (wc) find session, delete and update sessions Flow + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt new file mode 100644 index 0000000000..c7e39cd519 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -0,0 +1,115 @@ +package com.tangem.data.walletconnect.initialize + +import android.app.Application +import com.reown.android.Core +import com.reown.android.CoreClient +import com.reown.android.relay.ConnectionType +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit +import com.tangem.data.walletconnect.DefaultWcSessionsManager +import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase +import com.tangem.data.walletconnect.request.DefaultWcRequestService +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase +import timber.log.Timber + +internal class DefaultWcInitializeUseCase( + private val application: Application, + private val sessionsManager: DefaultWcSessionsManager, + private val networkService: DefaultWcRequestService, + private val wcPairFlow: DefaultWcPairUseCase, +) : WcInitializeUseCase { + + private val wcSdkObservers = mutableSetOf( + sessionsManager, + networkService, + wcPairFlow, + ) + + override fun init(projectId: String) { + val relayUrl = "relay.walletconnect.com" + val serverUrl = "wss://$relayUrl?projectId=$projectId" + val connectionType = ConnectionType.AUTOMATIC + + val appMetaData = Core.Model.AppMetaData( + name = "Tangem", + description = "Tangem Wallet", + url = "tangem.com", + icons = listOf( + "https://user-images.githubusercontent.com/24321494/124071202-72a00900-da58-11eb-935a-dcdab21de52b.png", + ), + redirect = "kotlin-wallet-wc:/request", // Custom Redirect URI + ) + + CoreClient.initialize( + relayServerUrl = serverUrl, + connectionType = connectionType, + application = application, + metaData = appMetaData, + ) { error -> + Timber.e("Error while initializing client: $error") + } + + WalletKit.initialize( + Wallet.Params.Init(core = CoreClient), + onSuccess = { + val walletDelegate = defineWalletDelegate() + WalletKit.setWalletDelegate(walletDelegate) + }, + onError = { error -> + Timber.e("Error while initializing Web3Wallet: $error") + }, + ) + } + + private fun defineWalletDelegate() = object : WalletKit.WalletDelegate { + override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)? + get() = super.onSessionAuthenticate + + override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { + wcSdkObservers.forEach { it.onConnectionStateChange(state) } + } + + override fun onError(error: Wallet.Model.Error) { + wcSdkObservers.forEach { it.onError(error) } + } + + override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) { + wcSdkObservers.forEach { it.onProposalExpired(proposal) } + } + + override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) { + wcSdkObservers.forEach { it.onRequestExpired(request) } + } + + override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { + wcSdkObservers.forEach { it.onSessionDelete(sessionDelete) } + } + + override fun onSessionExtend(session: Wallet.Model.Session) { + wcSdkObservers.forEach { it.onSessionExtend(session) } + } + + override fun onSessionProposal( + sessionProposal: Wallet.Model.SessionProposal, + verifyContext: Wallet.Model.VerifyContext, + ) { + wcSdkObservers.forEach { it.onSessionProposal(sessionProposal, verifyContext) } + } + + override fun onSessionRequest( + sessionRequest: Wallet.Model.SessionRequest, + verifyContext: Wallet.Model.VerifyContext, + ) { + wcSdkObservers.forEach { it.onSessionRequest(sessionRequest, verifyContext) } + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { + wcSdkObservers.forEach { it.onSessionSettleResponse(settleSessionResponse) } + } + + override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) { + wcSdkObservers.forEach { it.onSessionUpdateResponse(sessionUpdateResponse) } + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt new file mode 100644 index 0000000000..89099dc766 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -0,0 +1,216 @@ +package com.tangem.data.walletconnect.pair + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.usecase.pair.WcPairState +import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.suspendCancellableCoroutine +import timber.log.Timber +import kotlin.coroutines.resume + +val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") + +@Suppress("UnusedPrivateMember") // todo(wc) remove after add mapping +internal class DefaultWcPairUseCase( + private val sessionsManager: WcSessionsManager, +) : WcPairUseCase, WcSdkObserver { + + private val onWalletSelect = Channel() + private val onAccountSelect = Channel() + private val onCallTerminalAction = Channel() + private val onSessionProposal = + Channel>(Channel.BUFFERED) + private val onSessionSettleResponse = Channel(Channel.BUFFERED) + + override fun pairFlow(uri: String, source: WcPairUseCase.Source, selectedWallet: UserWallet): Flow = + flow { + emit(WcPairState.Loading) + + // call sdk.pair and wait result, finish flow on error + walletKitPair(uri).onLeft { throwable -> + emit(WcPairState.Error(throwable)) + return@flow + } + // wait for sdk onSessionProposal callback + val (sdkSessionProposal, verifyContext) = onSessionProposal.receiveAsFlow().map { (fist, second) -> + val ourCopy = WcSdkSessionProposal( + name = fist.name, + description = fist.description, + url = fist.url, + proposerPublicKey = fist.proposerPublicKey, + ) + ourCopy to second + }.first { (sessionProposal, verifyContext) -> + true // todo(wc) check verifyContext? compare uri? + } + + // check unsupported dApps, just local constant for now, finish if unsupported + if (sdkSessionProposal.name in unsupportedDApps) { + Timber.i("Unsupported DApp") + val error = WcPairState + .Error(RuntimeException("todo(wc) use domain exception")) // todo(wc) WalletConnectError.UnsupportedDApp + emit(error) + return@flow + } + + // flow that collect terminal and all middle actions + val actionsFlow = channelFlow { + val userWallet = onWalletSelect.receiveAsFlow() + .stateIn(scope = this, started = SharingStarted.Lazily, initialValue = selectedWallet) + val accountSelect = onAccountSelect.receiveAsFlow() + .stateIn(scope = this, started = SharingStarted.Lazily, initialValue = Any()) + // combine all middle variables and emit new ProposalState to main FlowCollector + combine(accountSelect, userWallet) { account, selectedWallet -> + buildProposalState(sdkSessionProposal, verifyContext, account, selectedWallet) + }.onEach { newProposalState -> this@flow.emit(newProposalState) } + .launchIn(this) + + // collect terminal action and emit to this inner channelFlow, unlike combine above + onCallTerminalAction.receiveAsFlow() + .onEach { this.channel.send(it) } + .launchIn(this) + } + + // wait first terminal action and continue WC pair flow + val sessionForApprove: WcSessionProposal = when (val terminalAction = actionsFlow.first()) { + is TerminalAction.Approve -> terminalAction.sessionForApprove + TerminalAction.Reject -> { + // non suspending WalletKit.rejectSession call + rejectSession(sdkSessionProposal) + return@flow + } + } + + // start flow of approving in wc sdk + emit(WcPairState.Approving.Loading(sessionForApprove)) + fun mapResult(either: Either) = + WcPairState.Approving.Result(sessionForApprove, either) + // call sdk approve and wait result + walletKitApproveSession(sessionForApprove).onLeft { emit(mapResult(it.left())) }.onRight { + // wait for sdk callback + val result = when (val settledSession = onSessionSettleResponse.receiveAsFlow().first()) { + is Wallet.Model.SettledSessionResponse.Error -> mapResult( + // WalletConnectError.ExternalApprovalError(settledSession.errorMessage) todo(wc) + RuntimeException("todo(wc) use domain exception").left(), + ) + + is Wallet.Model.SettledSessionResponse.Result -> { + val newSession = settledSession.session.toDomain(sessionForApprove.wallet) + sessionsManager.saveSessions(sessionForApprove.wallet.walletId, newSession) + mapResult(newSession.right()) + } + } + emit(result) + } + } + + override fun onWalletSelect(selectedWallet: UserWallet) { + onWalletSelect.trySend(selectedWallet) + } + + override fun onAccountSelect(account: Any) { + onAccountSelect.trySend(account) + } + + override fun approve(sessionForApprove: WcSessionProposal) { + onCallTerminalAction.trySend(TerminalAction.Approve(sessionForApprove)) + } + + override fun reject() { + onCallTerminalAction.trySend(TerminalAction.Reject) + } + + override fun onSessionProposal( + sessionProposal: Wallet.Model.SessionProposal, + verifyContext: Wallet.Model.VerifyContext, + ) { + // Triggered when wallet receives the session proposal sent by a Dapp + Timber.i("sessionProposal: $sessionProposal") + onSessionProposal.trySend(sessionProposal to verifyContext) + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { + // Triggered when wallet receives the session settlement response from Dapp + Timber.i("onSessionSettleResponse: $settleSessionResponse") + onSessionSettleResponse.trySend(settleSessionResponse) + } + + private suspend fun walletKitPair(uri: String): Either = + suspendCancellableCoroutine { continuation -> + WalletKit.pair( + params = Wallet.Params.Pair(uri), + onSuccess = { + Timber.i("Paired successfully: $it") + continuation.resume(Unit.right()) + }, + onError = { + Timber.e("Error while pairing: $it") + continuation.resume(it.throwable.left()) + }, + ) + } + + private suspend fun walletKitApproveSession(sessionForApprove: WcSessionProposal): Either = + suspendCancellableCoroutine { continuation -> + WalletKit.approveSession( + params = TODO("wc sdk model"), + onSuccess = { + Timber.i("Approved successfully: $it") + continuation.resume(Unit.right()) + }, + onError = { + Timber.e("Error while approving: $it") + continuation.resume(it.throwable.left()) + }, + ) + } + + private fun rejectSession(sessionProposal: WcSdkSessionProposal) { + WalletKit.rejectSession( + params = Wallet.Params.SessionReject( + proposerPublicKey = sessionProposal.proposerPublicKey, + reason = "", + ), + onSuccess = { + Timber.i("Rejected successfully: $it") + }, + onError = { + Timber.e("Error while rejecting: $it") + }, + ) + } + + private fun buildProposalState( + sessionProposal: WcSdkSessionProposal, + verifyContext: Wallet.Model.VerifyContext, + selectedAccount: Any, + userWallet: UserWallet, + ): WcPairState.Proposal { + // todo(wc) a lot of mapping from Wallet.Model.SessionProposal to our Blockchain, Network, ect + // todo(wc) check security status by Wallet.Model.VerifyContext or Blockaid, don't know for now + val mock: WcSessionProposal = TODO() + return WcPairState.Proposal(mock, Any()) + } + + private fun Wallet.Model.Session.toDomain(userWallet: UserWallet): WcSession = WcSession( + userWalletId = userWallet.walletId, + sdkModel = this.toOurModel(), + ) + + private sealed interface TerminalAction { + data class Approve(val sessionForApprove: WcSessionProposal) : TerminalAction + data object Reject : TerminalAction + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt new file mode 100644 index 0000000000..c36cfbdb75 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -0,0 +1,48 @@ +package com.tangem.data.walletconnect.request + +import com.reown.walletkit.client.Wallet +import com.tangem.data.walletconnect.utils.WcSdkObserver +import com.tangem.data.walletconnect.utils.toOurModel +import com.tangem.domain.walletconnect.model.WcMethod +import com.tangem.domain.walletconnect.model.WcRequest +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.request.WcRequestHandler +import com.tangem.domain.walletconnect.request.WcRequestService +import com.tangem.domain.walletconnect.respond.WcRespondService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch + +internal class DefaultWcRequestService( + private val sessionsManager: WcSessionsManager, + private val respondService: WcRespondService, + private val requestAdapters: List>, + private val scope: CoroutineScope, // todo(wc) is ok inject scope? featureScope like Decompose Model scope +) : WcRequestService, WcSdkObserver { + + override val requests: MutableSharedFlow> = MutableSharedFlow() + + override fun onSessionRequest( + sessionRequest: Wallet.Model.SessionRequest, + verifyContext: Wallet.Model.VerifyContext, + ) { + // Triggered when a Dapp sends SessionRequest to sign a transaction or a message + val sr = sessionRequest.toOurModel() + val method = sr.request.method + val params = sr.request.params + scope.launch { + val session = sessionsManager.findSessionByTopic(sr.topic) + val handler: WcRequestHandler? = requestAdapters.firstOrNull { it.canHandle(method) } + + val deserialized: WcMethod? = handler?.deserialize(method, params) + if (handler == null || deserialized == null || session == null) { + respondService.rejectRequest(sr, "UnsupportedMethod") // todo(wc) use our domain error + return@launch + } + + val wcRequest = WcRequest(sr, session, deserialized) + handler.handle(wcRequest) + requests.emit(wcRequest) + } + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt new file mode 100644 index 0000000000..7381e9b548 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -0,0 +1,53 @@ +package com.tangem.data.walletconnect.respond + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.respond.WcRespondService +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +internal class DefaultWcRespondService : WcRespondService { + + override suspend fun respond(request: WcSdkSessionRequest, response: String): Either = + suspendCancellableCoroutine { continuation -> + WalletKit.respondSessionRequest( + params = Wallet.Params.SessionRequestResponse( + sessionTopic = request.topic, + jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcResult( + id = request.request.id, + result = response, + ), + ), + onSuccess = { + continuation.resume(Unit.right()) + }, + onError = { + continuation.resume(it.throwable.left()) + }, + ) + } + + override suspend fun rejectRequest(request: WcSdkSessionRequest, message: String) = + suspendCancellableCoroutine { continuation -> + WalletKit.respondSessionRequest( + params = Wallet.Params.SessionRequestResponse( + sessionTopic = request.topic, + jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError( + id = request.request.id, + code = 0, + message = message, + ), + ), + onSuccess = { + continuation.resume(Unit.right()) + }, + onError = { + continuation.resume(it.throwable.left()) + }, + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt new file mode 100644 index 0000000000..5da50ba3c4 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/ToOurModel.kt @@ -0,0 +1,22 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.walletkit.client.Wallet +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest.JSONRPCRequest + +internal fun Wallet.Model.Session.toOurModel(): WcSdkSession = WcSdkSession( + topic = topic, +) + +internal fun Wallet.Model.SessionRequest.toOurModel(): WcSdkSessionRequest = WcSdkSessionRequest( + topic = this.topic, + chainId = this.chainId, + request = this.request.toOurModel(), +) + +internal fun Wallet.Model.SessionRequest.JSONRPCRequest.toOurModel(): JSONRPCRequest = JSONRPCRequest( + id = this.id, + method = this.method, + params = this.params, +) \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt new file mode 100644 index 0000000000..5c15ea1b80 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcSdkObserver.kt @@ -0,0 +1,38 @@ +package com.tangem.data.walletconnect.utils + +import com.reown.walletkit.client.Wallet +import com.reown.walletkit.client.WalletKit + +internal interface WcSdkObserver : WalletKit.WalletDelegate { + + override val onSessionAuthenticate: ((Wallet.Model.SessionAuthenticate, Wallet.Model.VerifyContext) -> Unit)? + get() = super.onSessionAuthenticate + + override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) {} + + override fun onError(error: Wallet.Model.Error) {} + + override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) {} + + override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) {} + + override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) {} + + override fun onSessionExtend(session: Wallet.Model.Session) {} + + override fun onSessionProposal( + sessionProposal: Wallet.Model.SessionProposal, + verifyContext: Wallet.Model.VerifyContext, + ) { + } + + override fun onSessionRequest( + sessionRequest: Wallet.Model.SessionRequest, + verifyContext: Wallet.Model.VerifyContext, + ) { + } + + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) {} + + override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) {} +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index e17b2c880c..35bc64c215 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -208,6 +208,10 @@ internal class DefaultWalletsRepository( .getObjectMap(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) .map { it[userWalletId.stringValue] == true } + override fun nftEnabledStatuses(): Flow> = appPreferencesStore + .getObjectMap(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) + .map { it.mapKeys { UserWalletId(it.key) } } + override suspend fun enableNFT(userWalletId: UserWalletId) { appPreferencesStore.editData { it.setObjectMap( diff --git a/domain/core/build.gradle.kts b/domain/core/build.gradle.kts index 46d2670d68..9910d828a3 100644 --- a/domain/core/build.gradle.kts +++ b/domain/core/build.gradle.kts @@ -10,4 +10,9 @@ dependencies { api(deps.arrow.fx) implementation(deps.kotlin.serialization) + + testImplementation(deps.test.coroutine) + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt new file mode 100644 index 0000000000..2a86b81acf --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowCachingSupplier.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.update + +private typealias FlowsStore = MutableStateFlow>> + +/** + * [Flow] supplier with caching mechanism + * + * @param Producer producer of flow [Flow] + * @param Params type of data that required to get [Flow] + * @param Data data type of [Flow] + * @property flowsStore store of flows + */ +abstract class FlowCachingSupplier, Params : Any, Data : Any>( + private val flowsStore: FlowsStore = MutableStateFlow(value = emptyMap()), +) : FlowSupplier { + + /** Factory of [FlowProducer] */ + abstract val factory: FlowProducer.Factory + + /** Key creator */ + abstract val keyCreator: (Params) -> String + + /** + * Supply [Flow] by [params]. + + */ + override operator fun invoke(params: Params): Flow { + val key = keyCreator(params) + val saved = flowsStore.value[key] + + if (saved != null) return saved + + val flowProducer = factory.create(params = params) + + return runCatching(flowProducer::produceWithFallback) + .onSuccess { flow -> + flowsStore.update { + it.toMutableMap().apply { + put(key = key, value = flow) + } + } + } + .getOrThrow() + .catch { cause -> + flowsStore.update { + it.toMutableMap().apply { + remove(key) + } + } + + throw cause + } + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt new file mode 100644 index 0000000000..3fcba3d9d4 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowFetcher.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.core.flow + +import arrow.core.Either + +/** + * Flow fetcher + * + * @param Params data that required to fetch flow + * +[REDACTED_AUTHOR] + */ +interface FlowFetcher { + + suspend operator fun invoke(params: Params): Either +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt new file mode 100644 index 0000000000..92e95f552d --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowProducer.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.retryWhen + +/** + * [Flow] producer + * + * @param Data data type of [Flow] + * +[REDACTED_AUTHOR] + */ +interface FlowProducer { + + /** Fallback value if [Flow] throws exception */ + val fallback: Data + + /** Produce [Flow] */ + fun produce(): Flow + + /** Produce [Flow] with retry mechanism */ + fun produceWithFallback(): Flow { + return produce().retryWhen { _, _ -> + emit(value = fallback) + + delay(timeMillis = 2000) + + true + } + } + + /** Factory for creating [Producer]. It helps to provide [Params] by constructor */ + interface Factory> { + + /** Create [Producer] using [params] */ + fun create(params: Params): Producer + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt new file mode 100644 index 0000000000..8de04d12bd --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/flow/FlowSupplier.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.core.flow + +import kotlinx.coroutines.flow.Flow + +/** + * [Flow] supplier + * + * @param Params type of data that required to get [Flow] + * @param Data data type of [Flow] + * +[REDACTED_AUTHOR] + */ +interface FlowSupplier { + + /** Supply [Flow] by [params] */ + operator fun invoke(params: Params): Flow +} \ No newline at end of file diff --git a/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt b/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt new file mode 100644 index 0000000000..42822e7796 --- /dev/null +++ b/domain/core/src/test/kotlin/com/tangem/domain/core/flow/FlowCachingSupplierTest.kt @@ -0,0 +1,139 @@ +package com.tangem.domain.core.flow + +import com.google.common.truth.Truth +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class FlowCachingSupplierTest { + + private val factory = mockk() + private val errorFactory = mockk() + + @Test + fun `flow was created before`() = runTest { + val flowsStore = MutableStateFlow>>(value = emptyMap()) + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 1 + + every { factory.create(params = params) } returns MockFlowProducer(params = params) + + val actual = supplier(params = params) + + verify { factory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("test_1") + + Truth.assertThat(flowsStore.value.size).isEqualTo(1) + Truth.assertThat(flowsStore.value.containsKey("mock_1")).isTrue() + Truth.assertThat(flowsStore.value["mock_1"]!!.first()).isEqualTo("test_1") + } + + @Test + fun `flow wasn't created before`() = runTest { + val storedFlow = flowOf("test_1") + val storedMap = mapOf("mock_1" to storedFlow) + val flowsStore = MutableStateFlow(value = storedMap) + + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 1 + + val actual = supplier(params = params) + + verify(inverse = true) { factory.create(params = params) } + + Truth.assertThat(actual).isEqualTo(storedFlow) + Truth.assertThat(flowsStore.value).isEqualTo(storedMap) + } + + @Test + fun `flow wasn't created before and store isn't empty`() = runTest { + val flowsStore = MutableStateFlow( + value = mapOf("mock_1" to flowOf("test_1")), + ) + + val supplier = MockFlowCachingSupplier(factory = factory, flowsStore = flowsStore) + + val params = 2 + + every { factory.create(params = params) } returns MockFlowProducer(params = params) + + val actual = supplier(params = params) + + verify { factory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("test_2") + + Truth.assertThat(flowsStore.value.size).isEqualTo(2) + Truth.assertThat(flowsStore.value.keys).isEqualTo(setOf("mock_1", "mock_2")) + Truth.assertThat(flowsStore.value["mock_1"]!!.first()).isEqualTo("test_1") + Truth.assertThat(flowsStore.value["mock_2"]!!.first()).isEqualTo("test_2") + } + + @Test + fun `flow throws exception`() = runTest { + val flowsStore = MutableStateFlow>>(value = emptyMap()) + val supplier = MockErrorFlowCachingSupplier(factory = errorFactory, flowsStore = flowsStore) + + val params = 1 + + every { errorFactory.create(params = params) } returns MockErrorFlowProducer() + + val actual = supplier(params = params) + + verify { errorFactory.create(params = params) } + + Truth.assertThat(actual.first()).isEqualTo("fallback") + Truth.assertThat(flowsStore.value.size).isEqualTo(1) + } + + private class MockFlowCachingSupplier( + override val factory: FlowProducer.Factory, + flowsStore: MutableStateFlow>>, + ) : FlowCachingSupplier(flowsStore = flowsStore) { + + override val keyCreator: (Int) -> String = { "mock_$it" } + } + + private class MockFlowProducer(private val params: Int) : FlowProducer { + + override val fallback: String + get() = "fallback" + + override fun produce(): Flow = flowOf("test_$params") + + class Factory : FlowProducer.Factory { + override fun create(params: Int): MockFlowProducer = MockFlowProducer(params = params) + } + } + + private class MockErrorFlowCachingSupplier( + override val factory: FlowProducer.Factory, + flowsStore: MutableStateFlow>>, + ) : FlowCachingSupplier(flowsStore = flowsStore) { + + override val keyCreator: (Int) -> String = { "mock_$it" } + } + + private class MockErrorFlowProducer : FlowProducer { + + override val fallback: String + get() = "fallback" + + override fun produce(): Flow = flow { + throw IllegalStateException() + } + + class Factory : FlowProducer.Factory { + override fun create(params: Int): MockErrorFlowProducer = MockErrorFlowProducer() + } + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 570f0f9b56..7bb6e53a9b 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -31,7 +31,7 @@ class SendFeedbackEmailUseCase( subject = emailSubjectResolver.resolve(type), message = createMessage(type), // Temporally user data is not sent - file = null, // feedbackRepository.getLogFile(), + file = null, // feedbackRepository.getZipLogFile(), ) feedbackRepository.sendEmail(email) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index ae50655518..802c36dc7d 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -27,5 +27,7 @@ interface FeedbackRepository { fun getLogFile(): File? + suspend fun getZipLogFile(): File? + fun sendEmail(feedbackEmail: FeedbackEmail) } \ No newline at end of file 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 index ce2a817c95..5dc33bd1cd 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -682,6 +682,12 @@ class DefaultWalletManagersFacade( return walletManager.getAsset(collectionIdentifier, assetIdentifier) } + override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean { + val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) + val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true + return initializableAccountWalletManger.accountInitializationState == InitializableAccount.State.INITIALIZED + } + private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set) { if (tokens.isEmpty()) return 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 index d1554a2118..e71d19ab0f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -265,4 +265,10 @@ interface WalletManagersFacade { collectionIdentifier: NFTCollection.Identifier, assetIdentifier: NFTAsset.Identifier, ): NFTAsset? + + /** + * If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized] + * value. Otherwise always return true + */ + suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotesShort.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotesShort.kt index ce2ff4bb71..9cf4b69b48 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotesShort.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotesShort.kt @@ -4,9 +4,9 @@ import java.math.BigDecimal data class TokenQuotesShort( val currentPrice: BigDecimal, - val h24ChangePercent: BigDecimal, - val weekChangePercent: BigDecimal, - val monthChangePercent: BigDecimal, + val h24ChangePercent: BigDecimal?, + val weekChangePercent: BigDecimal?, + val monthChangePercent: BigDecimal?, ) fun TokenQuotesShort.toFull() = TokenQuotes( diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt index b4e4accc54..bd97bf4cfe 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTCollections.kt @@ -17,4 +17,14 @@ data class NFTCollections( val error: Throwable, ) : Content() } + + companion object { + fun empty(network: Network) = NFTCollections( + network = network, + content = Content.Collections( + collections = null, + source = StatusSource.ACTUAL, + ), + ) + } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt new file mode 100644 index 0000000000..e8ecd813c0 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FetchNFTCollectionsUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class FetchNFTCollectionsUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId) { + val currencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) + nftRepository.refreshCollections(userWalletId, currencies.map { it.network }.distinct()) + } +} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt new file mode 100644 index 0000000000..f59d8578b6 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest + +class GetNFTCollectionsUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + + @OptIn(ExperimentalCoroutinesApi::class) + fun launch(userWalletId: UserWalletId): Flow> = currenciesRepository + .getWalletCurrenciesUpdates(userWalletId) + .flatMapLatest { + val networks = it + .map { cryptoCurrency -> cryptoCurrency.network } + .distinct() + nftRepository.observeCollections(userWalletId, networks) + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt new file mode 100644 index 0000000000..ab5a2807a0 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetLegacyTopUpUrlUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.onramp.repositories.LegacyTopUpRepository +import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +class GetLegacyTopUpUrlUseCase( + private val legacyTopUpRepository: LegacyTopUpRepository, +) { + + suspend operator fun invoke(cryptoCurrencyStatus: CryptoCurrencyStatus): Either = + either { + val walletAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: raise(OnrampError.DomainError("Wallet address is null")) + legacyTopUpRepository.getTopUpUrl( + cryptoCurrency = cryptoCurrencyStatus.currency, + walletAddress = walletAddress, + ) + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/LegacyTopUpRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/LegacyTopUpRepository.kt new file mode 100644 index 0000000000..0b69495806 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/LegacyTopUpRepository.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.onramp.repositories + +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Support legacy onboarding note/twins to get top up URL from mercuryo + * + */ +interface LegacyTopUpRepository { + + /** + * Get top up URL from mercuryo + * + * @property cryptoCurrency crypto currency + * @property walletAddress address of wallet + */ + suspend fun getTopUpUrl(cryptoCurrency: CryptoCurrency, walletAddress: String): String + + companion object { + const val SCHEME = "https" + const val SUCCESS_URL = "https://tangem.com/success" + } +} \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 917cd0ad5f..d87b8d595b 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.jodatime) + implementation(projects.domain.legacy) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index 7456b68af6..8856f70ef3 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -63,9 +63,15 @@ data class BalanceItem( val validatorAddress: String?, val date: DateTime?, val pendingActions: List, + val pendingActionsConstraints: List, val isPending: Boolean, ) +data class PendingActionConstraints( + val type: StakingActionType, + val amountArg: PendingAction.PendingActionArgs.Amount?, +) + data class PendingAction( val type: StakingActionType, val passthrough: String, @@ -135,5 +141,21 @@ enum class BalanceType(val order: Int) { enum class RewardBlockType { NoRewards, Rewards, + RewardsRequirementsError, RewardUnavailable, + ; + + /** + * Indicated whether action can be performed on available reward + * For example, pending action CLAIM_REWARDS could be called + */ + val isActionable: Boolean + get() = when (this) { + NoRewards, + RewardUnavailable, + -> false + RewardsRequirementsError, + Rewards, + -> true + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/CheckAccountInitializedUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/CheckAccountInitializedUseCase.kt new file mode 100644 index 0000000000..99d0d2fec7 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/CheckAccountInitializedUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId + +class CheckAccountInitializedUseCase(private val walletManagersFacade: WalletManagersFacade) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { + return Either.catch { walletManagersFacade.isAccountInitialized(userWalletId, network) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 29e7148ee2..45892df5b3 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -88,6 +88,7 @@ class InvalidatePendingTransactionsUseCase( validatorAddress = action.validatorAddress ?: action.validatorAddresses?.get(0) ?: "", date = null, pendingActions = emptyList(), + pendingActionsConstraints = emptyList(), isPending = true, ), ) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt index 7fe8dfdf64..4d139661e9 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenExchangeAnalyticsEvent.kt @@ -9,7 +9,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM class TokenExchangeAnalyticsEvent( event: String, params: Map = mapOf(), -) : AnalyticsEvent("Token", event, params, null) { +) : AnalyticsEvent("Token", event, params) { class CexTxStatusOpened(token: String) : TokenScreenAnalyticsEvent( event = "Swap Status Opened", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt index 69ba4f794b..ef07a4082d 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenOnrampAnalyticsEvent.kt @@ -8,7 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM class TokenOnrampAnalyticsEvent( event: String, params: Map = mapOf(), -) : AnalyticsEvent("Onramp", event, params, null) { +) : AnalyticsEvent("Onramp", event, params) { class OnrampStatusOpened(tokenSymbol: String, provider: String, fiatCurrency: String) : TokenScreenAnalyticsEvent( event = "Onramp Status Opened", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt index 45d43689a8..7de884bf84 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt @@ -6,7 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM sealed class TokenReceiveAnalyticsEvent( event: String, params: Map = mapOf(), -) : AnalyticsEvent("Token / Receive", event, params, null) { +) : AnalyticsEvent("Token / Receive", event, params) { class ReceiveScreenOpened(token: String) : TokenReceiveAnalyticsEvent( event = "Receive Screen Opened", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index 9e8a010f52..844923dee0 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -13,8 +13,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason sealed class TokenScreenAnalyticsEvent( event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent("Token", event, params, error) { +) : AnalyticsEvent("Token", event, params) { /** Legacy event. It has a unique category, but it also is sent on TokenScreen */ class DetailsScreenOpened(token: String) : AnalyticsEvent( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 981a56de13..7e2b95240b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -14,7 +14,6 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* @@ -33,7 +32,6 @@ class GetCryptoCurrencyActionsUseCase( private val stakingRepository: StakingRepository, private val promoRepository: PromoRepository, private val dispatchers: CoroutineDispatcherProvider, - private val swapFeatureToggles: SwapFeatureToggles, private val currencyStatusOperations: BaseCurrencyStatusOperations, ) { @@ -420,7 +418,6 @@ class GetCryptoCurrencyActionsUseCase( val exchangeableState = withTimeoutOrNull(REQUEST_EXCHANGE_DATA_TIMEOUT) { rampManager.availableForSwap(userWallet.walletId, cryptoCurrency) } ?: ExchangeableState.Loading - val swapStoriesEnabled = swapFeatureToggles.isPromoStoriesEnabled val currencyName = cryptoCurrency.name val reason = when (exchangeableState) { @@ -433,7 +430,7 @@ class GetCryptoCurrencyActionsUseCase( ExchangeableState.NotExchangeable -> ScenarioUnavailabilityReason.NotExchangeable(currencyName) } - val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories && swapStoriesEnabled + val isShowBadge = reason == ScenarioUnavailabilityReason.None && shouldShowSwapStories TokenActionsState.ActionState.Swap( unavailabilityReason = reason, showBadge = isShowBadge, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt index 4363b74633..199e9ad236 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyUseCase.kt @@ -36,36 +36,6 @@ class GetCryptoCurrencyUseCase( } } - /** - * Returns specific cryptocurrency for a given user wallet. - * - * @param userWalletId The ID of the user's wallet. - * @param id The ID of the cryptocurrency. - * @return An [Either] representing success (Right) or an error (Left) in fetching the status. - */ - suspend operator fun invoke( - userWalletId: UserWalletId, - id: CryptoCurrency.ID, - ): Either { - return either { getCurrency(userWalletId, id) } - } - - /** - * Returns specific cryptocurrency for a given user wallet. - * - * !!! Important Use only [CryptoCurrency.ID.value] as cryptoCurrencyId - * - * @param userWalletId The ID of the user's wallet. - * @param cryptoCurrencyId String representation of the [CryptoCurrency.ID.value] of the cryptocurrency. - * @return An [Either] representing success (Right) or an error (Left) in fetching the status. - */ - suspend operator fun invoke( - userWalletId: UserWalletId, - cryptoCurrencyId: String, - ): Either { - return either { getCurrency(userWalletId, cryptoCurrencyId) } - } - /** * Returns the primary cryptocurrency for a given user wallet. * @@ -76,18 +46,6 @@ class GetCryptoCurrencyUseCase( return either { getPrimaryCurrency(userWalletId) } } - private suspend fun Raise.getCurrency( - userWalletId: UserWalletId, - id: CryptoCurrency.ID, - ): CryptoCurrency { - return catch( - block = { - currenciesRepository.getMultiCurrencyWalletCurrency(userWalletId, id) - }, - catch = { raise(CurrencyStatusError.DataError(it)) }, - ) - } - private suspend fun Raise.getCurrency( userWalletId: UserWalletId, id: String, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index faf7427071..b48aaa3c4d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -7,5 +7,5 @@ package com.tangem.domain.tokens */ interface TokensFeatureToggles { - val isBalancesCachingEnabled: Boolean + val isNetworksLoadingRefactoringEnabled: Boolean } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt index a14f37e601..cab4aff110 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt @@ -12,6 +12,7 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error @@ -28,6 +29,7 @@ class CachedCurrenciesStatusesOperations( private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, + @Suppress("UnusedPrivateMember") private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) { @@ -258,6 +260,7 @@ class CachedCurrenciesStatusesOperations( true } .distinctUntilChanged() + .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } private fun getNetworksStatuses( 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 index eeeb5b4e59..2679e1a047 100644 --- 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 @@ -1,57 +1,6 @@ package com.tangem.domain.tokens.operations -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.model.NetworkStatus -import com.tangem.domain.tokens.model.Quote -import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations.Companion.RETRY_DELAY -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* - -class CurrenciesStatusesOperations( - currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - stakingRepository: StakingRepository, -) : BaseCurrencyStatusOperations(currenciesRepository, quotesRepository, networksRepository, stakingRepository) { - - override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { - return quotesRepository.getQuotesUpdatesLegacy(setOf(id)) - .map, Either>> { quotes -> - if (quotes.isEmpty()) Error.EmptyQuotes.left() else quotes.right() - } - .retryWhen { cause, _ -> - emit(Error.DataError(cause).left()) - delay(RETRY_DELAY) - true - } - } - - override fun getNetworksStatuses( - userWalletId: UserWalletId, - network: Network, - ): EitherFlow> { - return networksRepository.getNetworkStatusesUpdatesLegacy(userWalletId, setOf(network)) - .map, Either>> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } - } - - override suspend fun fetchComponents( - userWalletId: UserWalletId, - networks: Set, - currenciesIds: Set, - currencies: List, - ): Either = Unit.right() +class CurrenciesStatusesOperations { sealed class Error { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt deleted file mode 100644 index 255d2f86b5..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/LceCurrenciesStatusesOperations.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.tangem.domain.tokens.operations - -import arrow.core.* -import arrow.core.raise.ensureNotNull -import arrow.core.raise.recover -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lce -import com.tangem.domain.core.lce.lceFlow -import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceList -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.* -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* - -class LceCurrenciesStatusesOperations( - private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, -) : BaseCurrenciesStatusesOperations { - - override fun getCurrenciesStatuses( - userWalletId: UserWalletId, - ): LceFlow> { - return transformToCurrenciesStatuses( - userWalletId = userWalletId, - currenciesFlow = getWalletCurrencies(userWalletId), - ) - } - - private fun transformToCurrenciesStatuses( - userWalletId: UserWalletId, - currenciesFlow: EitherFlow>, - ): LceFlow> = lceFlow { - currenciesFlow.collectLatest { maybeCurrencies -> - val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull() - ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens } - - // This is only 'true' when the flow here is empty, such as during initial loading - if (isLoading.get()) { - val loadingCurrencies = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeYieldBalances = null, - ) - send(loadingCurrencies) - } - - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - - fun createCurrenciesStatuses( - maybeQuotes: Either>?, - maybeNetworkStatuses: Either>?, - maybeYieldBalances: Either?, - ): Lce> = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworkStatuses, - maybeYieldBalances = maybeYieldBalances, - ) - - combine( - getQuotes(currenciesIds), - getNetworksStatuses(userWalletId, networks), - getYieldBalances(userWalletId, nonEmptyCurrencies), - ::createCurrenciesStatuses, - ) - .distinctUntilChanged() - .collectLatest { maybeCurrenciesStatuses -> - send(maybeCurrenciesStatuses) - } - } - } - - private fun getWalletCurrencies(userWalletId: UserWalletId): EitherFlow> { - return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) - .map, Either>> { it.right() } - .catch { emit(TokenListError.DataError(it).left()) } - .distinctUntilChanged() - } - - private fun createCurrenciesStatuses( - currencies: NonEmptyList, - maybeQuotes: Either>?, - maybeNetworkStatuses: Either>?, - maybeYieldBalances: Either?, - ): Lce> = lce { - isLoading.set(maybeNetworkStatuses == null || maybeYieldBalances == null) - - var quotesRetrievingFailed = false - - val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull() - val yieldBalances = maybeYieldBalances?.bindEither() - val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { - null - } - - if (quotes == null) { - quotesRetrievingFailed = true - } - - currencies.map { currency -> - val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus) - - val currencyStatus = createCurrencyStatus( - currency = currency, - quote = quote, - networkStatus = networkStatus, - yieldBalance = yieldBalance, - ignoreQuote = quotesRetrievingFailed, - ) - - if (currencyStatus.value is CryptoCurrencyStatus.Loading) { - isLoading.set(true) - } - - currencyStatus - } - } - - private fun findYieldBalanceOrNull( - yieldBalances: YieldBalanceList?, - currency: CryptoCurrency, - networkStatus: NetworkStatus?, - ): YieldBalance? { - if (yieldBalances !is YieldBalanceList.Data) return null - - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - - if (supportedIntegration.isNullOrBlank()) return null - - return yieldBalances.getBalance( - address = extractAddress(networkStatus), - integrationId = supportedIntegration, - ) - } - - private fun createCurrencyStatus( - currency: CryptoCurrency, - quote: Quote?, - networkStatus: NetworkStatus?, - yieldBalance: YieldBalance?, - ignoreQuote: Boolean, - ): CryptoCurrencyStatus { - val currencyStatusOperations = CurrencyStatusOperations( - currency = currency, - quote = quote, - networkStatus = networkStatus, - yieldBalance = yieldBalance, - ignoreQuote = ignoreQuote, - ) - - return currencyStatusOperations.createTokenStatus() - } - - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return quotesRepository.getQuotesUpdatesLegacy(tokensIds.mapNotNull { it.rawCurrencyId }.toSet()) - .map, Either>> { it.right() } - .retryWhen { cause, _ -> - emit(TokenListError.DataError(cause).left()) - // adding delay before retry to avoid spam when flow restarted - delay(RETRY_QUOTES_DELAY) - true - } - .distinctUntilChanged() - } - - private fun getNetworksStatuses( - userWalletId: UserWalletId, - networks: NonEmptySet, - ): EitherFlow> { - return networksRepository.getNetworkStatusesUpdatesLegacy(userWalletId, networks) - .map, Either>> { it.right() } - .catch { emit(TokenListError.DataError(it).left()) } - .distinctUntilChanged() - } - - private fun getYieldBalances( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): EitherFlow { - return stakingRepository.getMultiYieldBalanceUpdatesLegacy(userWalletId, cryptoCurrencies) - .map> { it.right() } - .catch { emit(TokenListError.DataError(it).left()) } - .distinctUntilChanged() - } - - private fun getIds(currencies: List): Pair, NonEmptySet> { - val currencyIdToNetworkId = currencies.associate { currency -> - currency.id to currency.network - } - val currenciesIds = currencyIdToNetworkId.keys.toNonEmptySetOrNull() - val networks = currencyIdToNetworkId.values.toNonEmptySetOrNull() - - requireNotNull(currenciesIds) { "Currencies IDs cannot be empty" } - requireNotNull(networks) { "Networks IDs cannot be empty" } - - return networks to currenciesIds - } - - private fun extractAddress(networkStatus: NetworkStatus?): String? { - return when (val value = networkStatus?.value) { - is NetworkStatus.NoAccount -> value.address.defaultAddress.value - is NetworkStatus.Unreachable -> value.address?.defaultAddress?.value - is NetworkStatus.Verified -> value.address.defaultAddress.value - else -> null - } - } - - private companion object { - const val RETRY_QUOTES_DELAY = 2000L - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index c8879e30a3..d86abc8442 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -10,7 +10,7 @@ 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.* -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations +import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository @@ -164,7 +164,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { quotes: Flow>> = flowOf(MockQuotes.quotes.right()), statuses: Flow>> = flowOf(MockNetworks.verifiedNetworksStatuses.right()), ) = GetPrimaryCurrencyStatusUpdatesUseCase( - currencyStatusOperations = CurrenciesStatusesOperations( + currencyStatusOperations = CachedCurrenciesStatusesOperations( currenciesRepository = MockCurrenciesRepository( sortTokensResult = Unit.right(), removeCurrencyResult = removeCurrencyResult, @@ -176,6 +176,9 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(statuses), stakingRepository = MockStakingRepository(), + tokensFeatureToggles = object : TokensFeatureToggles { + override val isNetworksLoadingRefactoringEnabled: Boolean = false + }, ), dispatchers = dispatchers, ) diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index f798f98dc8..154228ce40 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) + implementation(projects.libs.crypto) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt index 75262c24e2..8fc2ee7870 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateAddressError.kt @@ -1,7 +1,18 @@ package com.tangem.domain.transaction.error -sealed class ValidateAddressError { - data object AddressInWallet : ValidateAddressError() - data object InvalidAddress : ValidateAddressError() - data class DataError(val throwable: Throwable) : ValidateAddressError() +import arrow.core.Either + +typealias AddressValidationResult = Either + +sealed class AddressValidation { + sealed class Success : AddressValidation() { + data object Valid : Success() + data object ValidXAddress : Success() + } + + sealed class Error : AddressValidation() { + data object AddressInWallet : Error() + data object InvalidAddress : Error() + data class DataError(val throwable: Throwable) : Error() + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateMemoError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateMemoError.kt new file mode 100644 index 0000000000..d5d51e0ed2 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/ValidateMemoError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.transaction.error + +sealed class ValidateMemoError { + data object InvalidMemo : ValidateMemoError() + data class DataError(val throwable: Throwable) : ValidateMemoError() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt index 7db3a825f7..1452cbea68 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt @@ -1,14 +1,15 @@ package com.tangem.domain.transaction.usecase -import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.transaction.WalletAddressServiceRepository -import com.tangem.domain.transaction.error.ValidateAddressError +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.transaction.error.AddressValidationResult import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.lib.crypto.BlockchainUtils /** * Use case for validating wallet address. @@ -23,18 +24,23 @@ class ValidateWalletAddressUseCase( network: Network, address: String, currencyAddress: Set?, - ): Either { + ): AddressValidationResult { val isUtxoConsolidationAvailable = walletManagersFacade.checkUtxoConsolidationAvailability(userWalletId, network) val isCurrentAddress = currencyAddress?.any { it.value == address } ?: true val isForbidSelfSend = isCurrentAddress && !isUtxoConsolidationAvailable - val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, address) + + val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.id.value) + val addressToValidate = decodedXAddress?.address ?: address + + val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) return when { - !isValidAddress -> ValidateAddressError.InvalidAddress.left() - isForbidSelfSend -> ValidateAddressError.AddressInWallet.left() - else -> Unit.right() + !isValidAddress -> AddressValidation.Error.InvalidAddress.left() + isForbidSelfSend -> AddressValidation.Error.AddressInWallet.left() + decodedXAddress != null -> AddressValidation.Success.ValidXAddress.right() + else -> AddressValidation.Success.Valid.right() } } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt index d4d5ab22e8..dc7ae1a340 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt @@ -1,8 +1,11 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.transaction.error.ValidateMemoError /** * Use case for validating wallet memo. @@ -11,7 +14,16 @@ class ValidateWalletMemoUseCase( private val walletAddressServiceRepository: WalletAddressServiceRepository, ) { - operator fun invoke(network: Network, memo: String): Either = Either.catch { - walletAddressServiceRepository.validateMemo(network, memo) + operator fun invoke(network: Network, memo: String): Either { + return try { + val isValidMemo = walletAddressServiceRepository.validateMemo(network, memo) + if (isValidMemo) { + Unit.right() + } else { + ValidateMemoError.InvalidMemo.left() + } + } catch (ex: Throwable) { + ValidateMemoError.DataError(ex).left() + } } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt index b21df662c3..d17b1d726e 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/model/VisaCurrency.kt @@ -1,17 +1,22 @@ package com.tangem.domain.visa.model import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.NetworkAddress import org.joda.time.DateTime import java.math.BigDecimal data class VisaCurrency( + val cryptoCurrency: CryptoCurrency, val networkName: String, val symbol: String, val decimals: Int, - val fiatRate: BigDecimal?, + val fiatRate: BigDecimal, + val priceChange: BigDecimal, val fiatCurrency: AppCurrency, val balances: Balances, val limits: Limits, + val paymentAccountAddress: NetworkAddress, ) { data class Balances( diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts index af72640fab..9b0cbaa2e3 100644 --- a/domain/wallet-connect/build.gradle.kts +++ b/domain/wallet-connect/build.gradle.kts @@ -7,4 +7,8 @@ dependencies { /* Project - Domain */ implementation(projects.domain.core) implementation(projects.domain.wallets.models) + implementation(projects.domain.walletConnect.models) + + /* Other */ + implementation(deps.moshi.adapters) } \ No newline at end of file diff --git a/domain/wallet-connect/models/.gitignore b/domain/wallet-connect/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/wallet-connect/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/wallet-connect/models/build.gradle.kts b/domain/wallet-connect/models/build.gradle.kts new file mode 100644 index 0000000000..47fbf86d0b --- /dev/null +++ b/domain/wallet-connect/models/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} +dependencies { + + /* Domain */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt new file mode 100644 index 0000000000..05c3df4d98 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethod.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.walletconnect.model + +interface WcMethod { + data object Unsupported : WcMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt new file mode 100644 index 0000000000..256aa09ce1 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest + +data class WcRequest( + val rawSdkRequest: WcSdkSessionRequest, + val session: WcSession, + val method: M, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt new file mode 100644 index 0000000000..92501d8bba --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession +import com.tangem.domain.wallets.models.UserWalletId + +data class WcSession( + val userWalletId: UserWalletId, + val sdkModel: WcSdkSession, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt new file mode 100644 index 0000000000..45c189cf20 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.walletconnect.model + +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionProposal +import com.tangem.domain.wallets.models.UserWallet + +data class WcSessionProposal( + val dAppInfo: WcSdkSessionProposal, + + // todo(wc) should map for all user wallets? + val wallet: UserWallet, + val missingChains: List, + val availableChains: List, + val notAddedChains: List, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt new file mode 100644 index 0000000000..bd88f8ea98 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSession.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.walletconnect.model.sdkcopy + +/** + * copy of [com.reown.walletkit.client.Wallet.Model.Session] + */ +data class WcSdkSession( + val topic: String, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt new file mode 100644 index 0000000000..93a3734601 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionProposal.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.walletconnect.model.sdkcopy + +/** + * copy of [com.reown.walletkit.client.Wallet.Model.SessionProposal] + */ +data class WcSdkSessionProposal( + val name: String, + val description: String, + val url: String, + val proposerPublicKey: String, +) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt new file mode 100644 index 0000000000..7b2ed2ff37 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.walletconnect.model.sdkcopy + +/** + * copy of [com.reown.walletkit.client.Wallet.Model.SessionRequest] + */ +data class WcSdkSessionRequest( + val topic: String, + val chainId: String?, + val request: JSONRPCRequest, +) { + + data class JSONRPCRequest( + val id: Long, + val method: String, + val params: String, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt new file mode 100644 index 0000000000..4d2d837da0 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/repository/WcSessionsManager.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.walletconnect.repository + +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface WcSessionsManager { + val sessions: Flow>> + suspend fun saveSessions(userWalletId: UserWalletId, session: WcSession) + suspend fun removeSessions(userWalletId: UserWalletId, session: WcSession) + suspend fun findSessionByTopic(topic: String): WcSession? +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt new file mode 100644 index 0000000000..410815ae03 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestHandler.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.walletconnect.request + +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.domain.walletconnect.model.WcMethod +import com.tangem.domain.walletconnect.model.WcRequest + +interface WcRequestHandler { + fun canHandle(methodName: String): Boolean + fun deserialize(methodName: String, params: String): M? + fun handle(wcRequest: WcRequest) + + companion object { + @OptIn(ExperimentalStdlibApi::class) + inline fun fromJson(params: String, moshi: Moshi): T? = + runCatching { moshi.adapter().fromJsonValue(params) }.getOrNull() + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt new file mode 100644 index 0000000000..5918accfd9 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/request/WcRequestService.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.walletconnect.request + +import com.tangem.domain.walletconnect.model.WcRequest +import kotlinx.coroutines.flow.Flow + +interface WcRequestService { + val requests: Flow> +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt new file mode 100644 index 0000000000..c74ce0e017 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/respond/WcRespondService.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect.respond + +import arrow.core.Either +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest + +interface WcRespondService { + suspend fun respond(request: WcSdkSessionRequest, response: String): Either + suspend fun rejectRequest(request: WcSdkSessionRequest, message: String = ""): Either +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt new file mode 100644 index 0000000000..e0bb5239a4 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcSimpleSignUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.walletconnect.usecase + +import arrow.core.Either +import kotlinx.coroutines.flow.Flow + +interface WcSimpleSignUseCase : WcUseCase { + + fun signFlow(initModel: SignModel): Flow> + + fun action(action: MiddleAction) + + fun cancel() + fun sign(toSign: SignModel) + + sealed interface State { + val model: SignModel + + data class PreSign(override val model: SignModel) : State + data class Signing(override val model: SignModel) : State + data class Result( + val result: Either, + override val model: SignModel, + ) : State + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt new file mode 100644 index 0000000000..a2efe639cd --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCase.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.walletconnect.usecase + +interface WcUseCase \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt new file mode 100644 index 0000000000..700cf1865c --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/WcUseCasesFlowProvider.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.walletconnect.usecase + +import kotlinx.coroutines.flow.Flow + +interface WcUseCasesFlowProvider { + val flow: Flow +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt new file mode 100644 index 0000000000..5e86f84006 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/EthPersonalSignUseCase.kt @@ -0,0 +1,72 @@ +package com.tangem.domain.walletconnect.usecase.ethereum + +import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcRequest +import com.tangem.domain.walletconnect.respond.WcRespondService +import com.tangem.domain.walletconnect.usecase.WcUseCase +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* + +class EthPersonalSignUseCase( + private val wcRequest: WcRequest, + private val respondService: WcRespondService, +) : WcUseCase { + + private val onCallTerminalAction = Channel() + + fun signFlow(): Flow = flow { + val model = SignModel(wcRequest.method.raw) + emit(State.PreSign(model)) + + when (val action = onCallTerminalAction.receiveAsFlow().first()) { + TerminalAction.Cancel -> { + val result = respondService.rejectRequest(wcRequest.rawSdkRequest) + emit(State.Result(result, model)) + } + is TerminalAction.Sign -> { + emit(State.Signing(action.toSign)) + val signed = signAndPrepareForSend() + val result = + if (signed != null) { + respondService.respond(wcRequest.rawSdkRequest, signed) + } else { + respondService.rejectRequest(wcRequest.rawSdkRequest) + } + emit(State.Result(result, model)) + } + } + } + + @Suppress("FunctionOnlyReturningConstant") // todo(wc) remove later + private suspend fun signAndPrepareForSend(): String? { + return null // todo(wc) + } + + fun sign(toSign: SignModel) { + onCallTerminalAction.trySend(TerminalAction.Sign(toSign)) + } + + fun cancel() { + onCallTerminalAction.trySend(TerminalAction.Cancel) + } + + sealed interface TerminalAction { + data class Sign(val toSign: SignModel) : TerminalAction + data object Cancel : TerminalAction + } + + sealed interface State { + val model: SignModel + + data class PreSign(override val model: SignModel) : State + data class Signing(override val model: SignModel) : State + data class Result( + val result: Either, + override val model: SignModel, + ) : State + } + + data class SignModel( + val raw: List, + ) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt new file mode 100644 index 0000000000..ab18902b6e --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthChain.kt @@ -0,0 +1,53 @@ +package com.tangem.domain.walletconnect.usecase.ethereum + +import com.squareup.moshi.Moshi +import com.tangem.domain.walletconnect.model.WcRequest +import com.tangem.domain.walletconnect.request.WcRequestHandler +import com.tangem.domain.walletconnect.respond.WcRespondService +import com.tangem.domain.walletconnect.usecase.WcUseCase +import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider +import com.tangem.domain.walletconnect.usecase.ethereum.WcEthMethod.SignMessage +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.receiveAsFlow + +@Suppress("UnusedPrivateClass") // todo(wc) remove later +private class WcEthChain( + val moshi: Moshi, + private val respondService: WcRespondService, +) : WcRequestHandler, WcUseCasesFlowProvider { + + private val _flow: Channel = Channel(Channel.BUFFERED) + override val flow = _flow.receiveAsFlow() + + override fun canHandle(methodName: String): Boolean { + return Name.entries.find { it.raw == methodName } != null + } + + override fun deserialize(methodName: String, params: String): WcEthMethod? { + val name = Name.entries.find { it.raw == methodName } ?: return null + return when (name) { + Name.Sign -> TODO() + Name.PersonalSign -> WcRequestHandler.fromJson(params, moshi) + Name.SignTypeData -> TODO() + Name.SignTypeDataV4 -> TODO() + Name.SignTransaction -> TODO() + Name.SendTransaction -> TODO() + } + } + + override fun handle(wcRequest: WcRequest) { + val useCase = when (wcRequest.method) { + is SignMessage -> EthPersonalSignUseCase(wcRequest as WcRequest, respondService) + } + _flow.trySend(useCase) + } + + enum class Name(val raw: String) { + Sign("eth_sign"), + PersonalSign("personal_sign"), + SignTypeData("eth_signTypedData"), + SignTypeDataV4("eth_signTypedData_v4"), + SignTransaction("eth_signTransaction"), + SendTransaction("eth_sendTransaction"), + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt new file mode 100644 index 0000000000..cf40981044 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMethod.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.walletconnect.usecase.ethereum + +import com.tangem.domain.walletconnect.model.WcMethod + +sealed interface WcEthMethod : WcMethod { + + data class SignMessage( + val raw: List, + ) : WcEthMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt new file mode 100644 index 0000000000..71f465a43f --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.walletconnect.usecase.initialize + +interface WcInitializeUseCase { + fun init(projectId: String) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt new file mode 100644 index 0000000000..1472db789e --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairState.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.walletconnect.usecase.pair + +import arrow.core.Either +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSessionProposal + +sealed interface WcPairState { + data object Loading : WcPairState + data class Error(val throwable: Throwable) : WcPairState + data class Proposal( + val dAppSession: WcSessionProposal, + val securityStatus: Any, + ) : WcPairState + + sealed interface Approving : WcPairState { + val session: WcSessionProposal + + data class Loading(override val session: WcSessionProposal) : Approving + data class Result( + override val session: WcSessionProposal, + val result: Either, + ) : Approving + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt new file mode 100644 index 0000000000..4bf3e9b047 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/pair/WcPairUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.walletconnect.usecase.pair + +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.flow.Flow + +interface WcPairUseCase { + + fun pairFlow(uri: String, source: Source, selectedWallet: UserWallet): Flow + + // todo(wc) should map for all user wallets without this action in domain layer? + fun onWalletSelect(selectedWallet: UserWallet) + // todo(wc) accounts in future? + fun onAccountSelect(account: Any) + + fun approve(sessionForApprove: WcSessionProposal) + fun reject() + + enum class Source { QR, DEEPLINK, ETC } +} \ No newline at end of file diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 616cbdcc21..6ca312b904 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } @@ -19,6 +21,7 @@ dependencies { api(projects.domain.core) implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) + implementation(projects.libs.tangemSdkApi) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) @@ -29,4 +32,9 @@ dependencies { implementation(tangemDeps.blockchain) // android-library implementation(tangemDeps.card.core) // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // end } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt index ffa69e47ae..dfaec8126c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/UserWalletBuilder.kt @@ -3,14 +3,17 @@ package com.tangem.domain.wallets.builder 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.usecase.GetCardImageUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject -class UserWalletBuilder( - private val scanResponse: ScanResponse, +class UserWalletBuilder @AssistedInject constructor( + @Assisted private val scanResponse: ScanResponse, private val generateWalletNameUseCase: GenerateWalletNameUseCase, - private val getCardImageUseCase: GetCardImageUseCase = GetCardImageUseCase(), + private val getCardImageUseCase: GetCardImageUseCase, ) { private var backupCardsIds: Set = emptySet() private var hasBackupError: Boolean = false @@ -56,4 +59,10 @@ class UserWalletBuilder( } } } + + @AssistedFactory + interface Factory { + + fun create(scanResponse: ScanResponse): UserWalletBuilder + } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index b1e526ce5c..6ec22a4eaf 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -32,6 +32,8 @@ interface WalletsRepository { fun nftEnabledStatus(userWalletId: UserWalletId): Flow + fun nftEnabledStatuses(): Flow> + suspend fun enableNFT(userWalletId: UserWalletId) suspend fun disableNFT(userWalletId: UserWalletId) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt index 67080c0682..b44e440301 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetCardImageUseCase.kt @@ -6,16 +6,15 @@ import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinsHelper import com.tangem.domain.wallets.models.Artwork import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.operations.attestation.TangemApi /** * Use case for getting card image url * - * @property verifier REST API + * @property verifier online card verifier * [REDACTED_AUTHOR] */ -class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardVerifier()) { +class GetCardImageUseCase(private val verifier: OnlineCardVerifier) { /** * Get card image url @@ -30,7 +29,11 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV if (artworkId.isNullOrEmpty()) { getFallbackArtworkUrl(cardId) } else { - getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId) + OnlineCardVerifier.getUrlForArtwork( + cardId = cardId, + cardPublicKey = cardPublicKey.toHexString(), + artworkId = artworkId, + ) } } @@ -51,9 +54,4 @@ class GetCardImageUseCase(private val verifier: OnlineCardVerifier = OnlineCardV } } } - - private fun getUrlForArtwork(cardId: String, cardPublicKeyHex: String, artworkId: String): String { - return TangemApi.Companion.BaseUrl.VERIFY.url + TangemApi.ARTWORK + - "?artworkId=$artworkId&CID=$cardId&publicKey=$cardPublicKeyHex" - } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 6badebac2d..570693940c 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -23,7 +23,6 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.SaveWalletError import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase import com.tangem.features.details.impl.R @@ -38,7 +37,7 @@ import kotlin.coroutines.resume internal class UserWalletSaver @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val reduxStateHolder: ReduxStateHolder, private val messageSender: UiMessageSender, @@ -56,17 +55,20 @@ internal class UserWalletSaver @Inject constructor( } }, ifRight = { scanResponse -> - recover(block = { - scanResponse ?: return - val userWallet = createUserWallet(scanResponse) - saveWallet(userWallet) - }, recover = { + recover( + block = { + scanResponse ?: return + val userWallet = createUserWallet(scanResponse) + saveWallet(userWallet) + }, + recover = { val message = it.message if (!message.isNullOrEmpty()) { messageSender.send(SnackbarMessage(message)) } - },) + }, + ) }, ) } @@ -110,7 +112,7 @@ internal class UserWalletSaver @Inject constructor( } private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { - val userWallet = UserWalletBuilder(response, generateWalletNameUseCase).build() + val userWallet = userWalletBuilderFactory.create(scanResponse = response).build() return ensureNotNull(userWallet) { Error.Unknown } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt index 39970e9671..076b704bb8 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt @@ -16,7 +16,7 @@ import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.common.ui.charts.state.rememberMarketChartState import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData +import com.tangem.core.ui.components.UnableToLoadData @Composable internal fun MarketTokenDetailsChart( diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt index 0d04d56bbe..b796c0f38a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.core.ui.res.TangemTheme import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData +import com.tangem.core.ui.components.UnableToLoadData internal fun LazyListScope.tokenMarketDetailsBody( state: MarketsTokenDetailsUM.Body, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index 41fc02dedd..c4adb37979 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -134,7 +134,7 @@ internal class MarketsTokenItemConverter( TrendInterval.D7 -> tokenQuotesShort.weekChangePercent TrendInterval.M1 -> tokenQuotesShort.monthChangePercent } - val scaled = percent.setScale(4, RoundingMode.HALF_UP) + val scaled = percent?.setScale(4, RoundingMode.HALF_UP) return when { scaled == null -> PriceChangeType.NEUTRAL scaled > BigDecimal.ZERO -> PriceChangeType.UP diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt index 703e2eae67..6fcc3b1195 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/ui/components/MarketsListLazyColumn.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.list.InfiniteListHandler diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 40d3b149c9..7c051fa34e 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -26,6 +26,9 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.datasource) + /** Domain modules */ + implementation(projects.domain.nft.models) + /** Common */ implementation(projects.common.ui) implementation(projects.common.routing) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetUM.kt new file mode 100644 index 0000000000..fc75d29651 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.nft.collections.entity + +internal data class NFTCollectionAssetUM( + val id: String, + val name: String, + val imageUrl: String?, + val price: NFTSalePriceUM, + val onItemClick: () -> Unit, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt new file mode 100644 index 0000000000..7b0147b03a --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionAssetsListUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.nft.collections.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class NFTCollectionAssetsListUM { + data object Collapsed : NFTCollectionAssetsListUM() + + @Immutable + sealed class Expanded : NFTCollectionAssetsListUM() { + data class Loading(val itemsCount: Int) : Expanded() + data class Failed(val onRetryClick: () -> Unit) : Expanded() + data class Content(val items: ImmutableList) : Expanded() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt new file mode 100644 index 0000000000..8e3375906b --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.nft.collections.entity + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +internal data class NFTCollectionUM( + val id: String, + val name: String, + @DrawableRes val networkIconId: Int, + val logoUrl: String?, + val description: TextReference, + val assets: NFTCollectionAssetsListUM, + val onExpandClick: () -> Unit, +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt new file mode 100644 index 0000000000..394144b65d --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.nft.collections.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class NFTCollectionsUM { + + data class Empty( + val onReceiveClick: () -> Unit, + ) : NFTCollectionsUM() + + data class Loading( + val onReceiveClick: () -> Unit, + ) : NFTCollectionsUM() + + data class Failed( + val onRetryClick: () -> Unit, + val onReceiveClick: () -> Unit, + ) : NFTCollectionsUM() + + data class Content( + val search: SearchBarUM, + val collections: ImmutableList, + val warnings: ImmutableList, + val onReceiveClick: () -> Unit, + ) : NFTCollectionsUM() +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt new file mode 100644 index 0000000000..f0bdc22aaa --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsWarningUM.kt @@ -0,0 +1,5 @@ +package com.tangem.features.nft.collections.entity + +import com.tangem.core.ui.components.notifications.NotificationConfig + +internal data class NFTCollectionsWarningUM(val config: NotificationConfig) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt new file mode 100644 index 0000000000..8747a49f55 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTSalePriceUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.nft.collections.entity + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed class NFTSalePriceUM { + data object Loading : NFTSalePriceUM() + data object Failed : NFTSalePriceUM() + data class Content(val price: String) : NFTSalePriceUM() +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt new file mode 100644 index 0000000000..32f7159f78 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollection.kt @@ -0,0 +1,178 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +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.SubcomposeAsyncImage +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.currency.icon.CurrencyIconTopBadge +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM +import com.tangem.features.nft.collections.entity.NFTCollectionUM +import com.tangem.features.nft.impl.R + +private const val CHEVRON_ROTATION_EXPANDED = 180f +private const val CHEVRON_ROTATION_COLLAPSED = 0f + +@Composable +internal fun NFTCollection(state: NFTCollectionUM, modifier: Modifier = Modifier) { + val isExpanded = state.assets is NFTCollectionAssetsListUM.Expanded + + Column( + modifier = modifier, + ) { + Row( + modifier = Modifier + .clickable(onClick = state.onExpandClick) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing12, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Logo(state) + + Text(state) + + val rotation by animateFloatAsState( + targetValue = if (isExpanded) { + CHEVRON_ROTATION_EXPANDED + } else { + CHEVRON_ROTATION_COLLAPSED + }, + label = "chevron_rotation", + ) + + Icon( + modifier = Modifier + .rotate(rotation) + .size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + tint = if (isExpanded) { + TangemTheme.colors.icon.primary1 + } else { + TangemTheme.colors.icon.inactive + }, + contentDescription = null, + ) + } + } +} + +@Composable +private fun Logo(state: NFTCollectionUM) { + val networkBadgeOffset = TangemTheme.dimens.spacing6 + + Box( + modifier = Modifier, + ) { + SubcomposeAsyncImage( + modifier = Modifier + .align(Alignment.CenterStart) + .size(TangemTheme.dimens.size36), + model = state.logoUrl, + loading = { + RectangleShimmer(radius = TangemTheme.dimens.radius8) + }, + error = { + Box( + modifier = Modifier + .clip(shape = TangemTheme.shapes.roundedCorners8) + .background(TangemTheme.colors.field.primary), + ) + }, + contentDescription = null, + ) + CurrencyIconTopBadge( + modifier = Modifier + .offset(x = networkBadgeOffset, y = -networkBadgeOffset) + .align(Alignment.TopEnd), + iconResId = state.networkIconId, + alpha = 1f, + colorFilter = null, + ) + } +} + +@Composable +private fun RowScope.Text(state: NFTCollectionUM) { + Column( + modifier = Modifier + .weight(1f) + .padding( + horizontal = TangemTheme.dimens.spacing12, + ), + ) { + Text( + modifier = Modifier, + text = state.name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing2), + text = state.description.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollection(@PreviewParameter(NFTCollectionProvider::class) item: NFTCollectionUM) { + TangemThemePreview { + NFTCollection( + state = item, + ) + } +} + +private class NFTCollectionProvider : CollectionPreviewParameterProvider( + collection = listOf( + NFTCollectionUM( + id = "item1", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Collapsed, + onExpandClick = { }, + ), + NFTCollectionUM( + id = "item2", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Expanded.Loading( + itemsCount = 3, + ), + onExpandClick = { }, + ), + ), +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt new file mode 100644 index 0000000000..9951cd16a7 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAsset.kt @@ -0,0 +1,100 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +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.SubcomposeAsyncImage +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerH2 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM +import com.tangem.features.nft.collections.entity.NFTSalePriceUM + +@Composable +internal fun NFTCollectionAsset(state: NFTCollectionAssetUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .clickable { state.onItemClick() }, + ) { + SubcomposeAsyncImage( + modifier = Modifier + .aspectRatio(1f), + model = state.imageUrl, + loading = { + RectangleShimmer(radius = TangemTheme.dimens.radius16) + }, + error = { + Box( + modifier = Modifier + .clip(shape = TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.field.primary), + ) + }, + contentDescription = null, + ) + SpacerH12() + Text( + text = state.name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + SpacerH2() + NFTSalePrice( + state = state.price, + ) + } +} + +@Preview(widthDp = 180, showBackground = true) +@Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionAsset( + @PreviewParameter(NFTCollectionAssetProvider::class) state: NFTCollectionAssetUM, +) { + TangemThemePreview { + NFTCollectionAsset( + state = state, + ) + } +} + +private class NFTCollectionAssetProvider : CollectionPreviewParameterProvider( + collection = listOf( + NFTCollectionAssetUM( + id = "item1", + name = "Nethers #0853", + imageUrl = "img", + price = NFTSalePriceUM.Loading, + onItemClick = { }, + ), + NFTCollectionAssetUM( + id = "item2", + name = "Nethers #0854", + imageUrl = "img", + price = NFTSalePriceUM.Failed, + onItemClick = { }, + ), + NFTCollectionAssetUM( + id = "item3", + name = "Nethers #0855", + imageUrl = "img", + price = NFTSalePriceUM.Content("0.05 ETH"), + onItemClick = { }, + ), + ), +) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAssetLoading.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAssetLoading.kt new file mode 100644 index 0000000000..66bf216ad7 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionAssetLoading.kt @@ -0,0 +1,47 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +internal fun NFTCollectionAssetLoading(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + ) { + RectangleShimmer( + modifier = Modifier + .aspectRatio(1f), + ) + SpacerH12() + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size86), + style = TangemTheme.typography.subtitle2, + textSizeHeight = true, + ) + SpacerH2() + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size48), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + } +} + +@Preview(widthDp = 180, showBackground = true) +@Preview(widthDp = 180, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionAsset() { + TangemThemePreview { + NFTCollectionAssetLoading() + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionWarning.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionWarning.kt new file mode 100644 index 0000000000..337e675026 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionWarning.kt @@ -0,0 +1,14 @@ +package com.tangem.features.nft.collections.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.features.nft.collections.entity.NFTCollectionsWarningUM + +@Composable +internal fun NFTCollectionWarning(state: NFTCollectionsWarningUM, modifier: Modifier = Modifier) { + Notification( + modifier = modifier, + config = state.config, + ) +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt new file mode 100644 index 0000000000..6a1b002cf4 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsContent.kt @@ -0,0 +1,318 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.rememberLazyListState +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.tooling.preview.Preview +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.* +import com.tangem.features.nft.collections.entity.NFTCollectionAssetUM +import com.tangem.features.nft.collections.entity.NFTCollectionAssetsListUM +import com.tangem.features.nft.collections.entity.NFTCollectionUM +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.features.nft.collections.entity.NFTSalePriceUM +import com.tangem.features.nft.impl.R +import kotlinx.collections.immutable.persistentListOf + +@Suppress("LongMethod") +@Composable +internal fun NFTCollectionsContent(state: NFTCollectionsUM.Content, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + Column( + modifier = Modifier, + ) { + SearchBar( + state = state.search, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + ) + state.warnings.fastForEach { + NFTCollectionWarning( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16), + state = it, + ) + } + LazyColumn( + modifier = Modifier + .padding( + top = TangemTheme.dimens.spacing16, + ) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.primary), + state = listState, + ) { + state.collections.fastForEach { collection -> + item(key = collection.id) { + NFTCollection( + modifier = Modifier.fillMaxWidth(), + state = collection, + ) + } + when (val assets = collection.assets) { + is NFTCollectionAssetsListUM.Collapsed -> Unit + is NFTCollectionAssetsListUM.Expanded.Loading -> { + assetsListLoading( + collectionId = collection.id, + content = assets, + ) + } + is NFTCollectionAssetsListUM.Expanded.Failed -> { + assetsListFailed( + collectionId = collection.id, + content = assets, + ) + } + is NFTCollectionAssetsListUM.Expanded.Content -> { + assetsListContent( + collectionId = collection.id, + content = assets, + ) + } + } + } + } + } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResourceSafe(R.string.nft_collections_receive), + onClick = { }, + ) + } +} + +private fun LazyListScope.assetsListLoading( + collectionId: String, + content: NFTCollectionAssetsListUM.Expanded.Loading, +) { + val itemsCount = content.itemsCount + val rowCount = (itemsCount + 1) / 2 + repeat(rowCount) { rowIndex -> + item( + key = "loading_${collectionId}_$rowIndex", + ) { + val paddingValues = PaddingValues( + start = TangemTheme.dimens.spacing6, + top = TangemTheme.dimens.spacing6, + end = TangemTheme.dimens.spacing6, + bottom = TangemTheme.dimens.spacing20, + ) + Row( + modifier = Modifier + .padding(TangemTheme.dimens.spacing6) + .animateItem(), + ) { + NFTCollectionAssetLoading( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + if (rowIndex == rowCount - 1 && itemsCount % 2 != 0) { + Box( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } else { + NFTCollectionAssetLoading( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } + } + } + } +} + +private fun LazyListScope.assetsListFailed(collectionId: String, content: NFTCollectionAssetsListUM.Expanded.Failed) { + item( + key = "failed_$collectionId", + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size142), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData( + onRetryClick = content.onRetryClick, + ) + } + } +} + +private fun LazyListScope.assetsListContent( + collectionId: String, + content: NFTCollectionAssetsListUM.Expanded.Content, +) { + val items = content.items + val itemsCount = items.size + val rowCount = (itemsCount + 1) / 2 + repeat(rowCount) { rowIndex -> + val item1 = items[rowIndex * 2] + val item2 = items.getOrNull(rowIndex * 2 + 1) + item( + key = "content_${collectionId}_${item1.id}_${item2?.id}", + ) { + val paddingValues = PaddingValues( + start = TangemTheme.dimens.spacing6, + top = TangemTheme.dimens.spacing6, + end = TangemTheme.dimens.spacing6, + bottom = TangemTheme.dimens.spacing20, + ) + Row( + modifier = Modifier + .padding(TangemTheme.dimens.spacing6) + .animateItem(), + ) { + NFTCollectionAsset( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + state = item1, + ) + if (item2 == null) { + Box( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + ) + } else { + NFTCollectionAsset( + modifier = Modifier + .weight(1f) + .padding(paddingValues), + state = item2, + ) + } + } + } + } +} + +@Suppress("LongMethod") +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionsContent() { + TangemThemePreview { + NFTCollectionsContent( + state = NFTCollectionsUM.Content( + search = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = { }, + ), + collections = persistentListOf( + NFTCollectionUM( + id = "item1", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Collapsed, + onExpandClick = { }, + ), + NFTCollectionUM( + id = "item2", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Expanded.Loading( + itemsCount = 1, + ), + onExpandClick = { }, + ), + NFTCollectionUM( + id = "item3", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Expanded.Failed( + onRetryClick = { }, + ), + onExpandClick = { }, + ), + NFTCollectionUM( + id = "item4", + name = "Nethers", + logoUrl = "", + networkIconId = R.drawable.img_eth_22, + description = TextReference.Str("3 items"), + assets = NFTCollectionAssetsListUM.Expanded.Content( + items = persistentListOf( + NFTCollectionAssetUM( + id = "item1", + name = "Nethers #0854", + imageUrl = "img", + price = NFTSalePriceUM.Content("0.05 ETH"), + onItemClick = { }, + ), + NFTCollectionAssetUM( + id = "item2", + name = "Nethers #0855", + imageUrl = "img", + price = NFTSalePriceUM.Loading, + onItemClick = { }, + ), + NFTCollectionAssetUM( + id = "item3", + name = "Nethers #0856", + imageUrl = "img", + price = NFTSalePriceUM.Failed, + onItemClick = { }, + ), + ), + ), + onExpandClick = { }, + ), + ), + warnings = persistentListOf( + NFTCollectionsWarningUM( + config = NotificationConfig( + title = TextReference.Res(R.string.nft_collections_warning_title), + subtitle = TextReference.Res(R.string.nft_collections_warning_subtitle), + iconResId = R.drawable.ic_alert_triangle_20, + ), + ), + ), + onReceiveClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt new file mode 100644 index 0000000000..1376f56e72 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt @@ -0,0 +1,74 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.Image +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .padding(TangemTheme.dimens.spacing16), + ) { + Column( + modifier = Modifier + .padding(TangemTheme.dimens.spacing16) + .align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(R.drawable.ic_nft_placeholder_76), + contentDescription = null, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), + text = stringResource(R.string.nft_collections_empty_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = stringResource(R.string.nft_collections_empty_description), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + PrimaryButton( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing48) + .wrapContentWidth(), + text = stringResourceSafe(R.string.nft_collections_receive), + onClick = state.onReceiveClick, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionsEmpty() { + TangemThemePreview { + NFTCollectionsEmpty( + state = NFTCollectionsUM.Empty( + onReceiveClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt new file mode 100644 index 0000000000..f16d193537 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt @@ -0,0 +1,54 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.features.nft.impl.R + +@Composable +internal fun NFTCollectionsFailed(state: NFTCollectionsUM.Failed, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .padding(TangemTheme.dimens.spacing16), + ) { + UnableToLoadData( + modifier = Modifier + .align(Alignment.Center), + onRetryClick = state.onRetryClick, + ) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResourceSafe(R.string.nft_collections_receive), + onClick = { }, + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionsFailed() { + TangemThemePreview { + NFTCollectionsFailed( + state = NFTCollectionsUM.Failed( + onRetryClick = { }, + onReceiveClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTSalePrice.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTSalePrice.kt new file mode 100644 index 0000000000..ce4a1db2cf --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTSalePrice.kt @@ -0,0 +1,40 @@ +package com.tangem.features.nft.collections.ui + +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.nft.collections.entity.NFTSalePriceUM + +@Composable +internal fun NFTSalePrice(state: NFTSalePriceUM, modifier: Modifier = Modifier) { + when (state) { + is NFTSalePriceUM.Content -> { + Text( + modifier = modifier, + text = state.price, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + is NFTSalePriceUM.Failed -> { + Text( + modifier = modifier, + text = "", + style = TangemTheme.typography.caption2, + ) + } + is NFTSalePriceUM.Loading -> { + TextShimmer( + modifier = modifier + .width(TangemTheme.dimens.size48), + style = TangemTheme.typography.caption2, + ) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt index 1845005df6..dbeb952224 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt @@ -4,4 +4,5 @@ interface OnboardingV2FeatureToggles { val isOnboardingV2Enabled: Boolean val isVisaOnboardingEnabled: Boolean val isNoteRefactoringEnabled: Boolean + val isTwinRefactoringEnabled: Boolean } \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt index 9649b1e0c7..e591e8393f 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt @@ -8,12 +8,14 @@ interface OnboardingEntryComponent : ComposableContentComponent { data class Params( val scanResponse: ScanResponse, - val multiWalletMode: MultiWalletMode, + val mode: Mode, ) - enum class MultiWalletMode { + enum class Mode { Onboarding, - AddBackup, + AddBackupWallet1, + WelcomeOnlyTwin, + RecreateWalletTwin, } interface Factory : ComponentFactory diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index a848d97068..f0b9fcacbb 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -43,6 +43,9 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.onboarding) implementation(projects.domain.visa) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.onramp) /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) @@ -63,6 +66,7 @@ dependencies { implementation(deps.compose.ui) implementation(deps.compose.ui.tooling) implementation(deps.compose.coil) + implementation(deps.compose.shimmer) implementation(deps.lottie.compose) implementation(deps.decompose.ext.compose) implementation(deps.androidx.activity.compose) @@ -73,6 +77,7 @@ dependencies { implementation(deps.kotlin.serialization) implementation(deps.timber) implementation(deps.firebase.crashlytics) + implementation(tangemDeps.card.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt index 41b7796f7a..80bbb18b70 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt @@ -12,4 +12,6 @@ internal class DefaultOnboardingV2FeatureToggles @Inject constructor( get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED") override val isNoteRefactoringEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("NOTE_REFACTORING_ENABLED") + override val isTwinRefactoringEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("TWIN_REFACTORING_ENABLED") } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt similarity index 69% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index f6ffdafe88..053c5f9930 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -1,6 +1,8 @@ -package com.tangem.features.onboarding.v2.multiwallet.impl.analytics +package com.tangem.features.onboarding.v2.common.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.tokens.model.CryptoCurrency sealed class OnboardingEvent( category: String, @@ -38,6 +40,21 @@ sealed class OnboardingEvent( } } + sealed class Topup( + event: String, + params: Map = mapOf(), + ) : OnboardingEvent("Onboarding / Top Up", event, params) { + + object ScreenOpened : Topup("Activation Screen Opened") + + object ButtonShowWalletAddress : Topup("Button - Show the Wallet Address") + + class ButtonBuyCrypto(currency: CryptoCurrency) : Topup( + event = "Button - Buy Crypto", + params = mapOf(AnalyticsParam.CURRENCY to currency.symbol), + ) + } + sealed class Backup( event: String, params: Map = mapOf(), @@ -75,4 +92,22 @@ sealed class OnboardingEvent( params = mapOf("Action" to "Cancel"), ) } + + sealed class Twins( + event: String, + params: Map = mapOf(), + ) : OnboardingEvent("Onboarding / Twins", event, params) { + + data object ScreenOpened : Twins("Twinning Screen Opened") + data object SetupStarted : Twins("Twin Setup Started") + data object SetupFinished : Twins("Twin Setup Finished") + } + + data class OfflineAttestationFailed( + val source: AnalyticsParam.ScreensSources, + ) : OnboardingEvent( + category = "Error", + event = "Offline Attestation Failed", + params = mapOf(AnalyticsParam.SOURCE to source.value), + ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt new file mode 100644 index 0000000000..7fb5ae3b9b --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/Dialogs.kt @@ -0,0 +1,39 @@ +package com.tangem.features.onboarding.v2.common.ui + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.onboarding.v2.impl.R + +internal fun exitOnboardingDialog(onConfirm: () -> Unit): DialogMessage { + return DialogMessage( + title = resourceReference(R.string.onboarding_exit_alert_title), + message = resourceReference(R.string.onboarding_exit_alert_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_yes), + onClick = onConfirm, + ) + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ) + }, + ) +} + +internal fun interruptBackupDialog(onConfirm: () -> Unit) = DialogMessage( + title = resourceReference(R.string.onboarding_exit_alert_title), + message = resourceReference(R.string.onboarding_exit_alert_message), + firstAction = EventMessageAction( + title = resourceReference(R.string.common_ok), + warning = true, + onClick = onConfirm, + ), + secondAction = EventMessageAction( + title = resourceReference(R.string.common_cancel), + onClick = {}, + ), +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/OnboardingDialogUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt similarity index 87% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/OnboardingDialogUM.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt index d5050f50e6..53e6ba8d82 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/OnboardingDialogUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui +package com.tangem.features.onboarding.v2.common.ui import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.extensions.TextReference diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt new file mode 100644 index 0000000000..12cde207b1 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/RefreshButton.kt @@ -0,0 +1,51 @@ +package com.tangem.features.onboarding.v2.common.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onboarding.v2.impl.R + +@Composable +internal fun RefreshButton(isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size48) + .shadow(elevation = 2.dp, shape = CircleShape) + .background(TangemTheme.colors.background.action) + .clickable( + enabled = !isRefreshing, + onClick = onRefreshBalanceClick, + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + ), + ) { + if (isRefreshing) { + CircularProgressIndicator( + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + } else { + Icon( + painter = painterResource(id = R.drawable.ic_refresh_24), + contentDescription = null, + tint = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/api/OnboardingDoneComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/api/OnboardingDoneComponent.kt index 079b169a7e..2863a3876a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/api/OnboardingDoneComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/api/OnboardingDoneComponent.kt @@ -5,7 +5,14 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface OnboardingDoneComponent : ComposableContentComponent { - data class Params(val onDone: () -> Unit) + data class Params( + val mode: Mode, + val onDone: () -> Unit, + ) + + enum class Mode { + WalletCreated, GoodToGo + } interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/DefaultOnboardingDoneComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/DefaultOnboardingDoneComponent.kt index f36c949bfe..a32bb8cc28 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/DefaultOnboardingDoneComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/DefaultOnboardingDoneComponent.kt @@ -1,7 +1,6 @@ package com.tangem.features.onboarding.v2.done.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent @@ -18,7 +17,8 @@ internal class DefaultOnboardingDoneComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { OnboardingDone( - onContinueClick = remember(this) { params.onDone }, + mode = params.mode, + onContinueClick = params.onDone, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt index f3c8aa0111..309b940ac9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/done/impl/ui/OnboardingDone.kt @@ -20,10 +20,16 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.impl.R +@Suppress("LongMethod") @Composable -internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Modifier) { +internal fun OnboardingDone( + mode: OnboardingDoneComponent.Mode, + onContinueClick: () -> Unit, + modifier: Modifier = Modifier, +) { val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti)) val progress by animateLottieCompositionAsState(composition) var showConfetti by remember { mutableStateOf(false) } @@ -57,7 +63,12 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo SpacerH12() Text( modifier = Modifier.padding(horizontal = 32.dp), - text = stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding), + text = when (mode) { + OnboardingDoneComponent.Mode.WalletCreated -> + stringResourceSafe(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) + OnboardingDoneComponent.Mode.GoodToGo -> + stringResourceSafe(R.string.visa_onboarding_success_screen_description) + }, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -100,6 +111,9 @@ internal fun OnboardingDone(onContinueClick: () -> Unit, modifier: Modifier = Mo @Composable private fun Preview() { TangemThemePreview { - OnboardingDone(onContinueClick = {}) + OnboardingDone( + mode = OnboardingDoneComponent.Mode.WalletCreated, + onContinueClick = {}, + ) } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt index 8e6cf0d6c5..ae53ea07e1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt @@ -71,6 +71,7 @@ internal class DefaultOnboardingEntryComponent @AssistedInject constructor( route = configuration, childContext = childByContext(factoryContext), onManageTokensDone = model::onManageTokensDone, + model = model, ) }, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 45def47034..231ffff139 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -15,15 +15,20 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.BiometryFeatureToggles import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent +import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent.Mode import com.tangem.features.onboarding.v2.entry.impl.analytics.OnboardingEntryEvent import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable @@ -43,6 +48,7 @@ internal class OnboardingEntryModel @Inject constructor( private val askBiometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, private val uiMessageSender: UiMessageSender, + private val userWalletsListManager: UserWalletsListManager, ) : Model() { private val params = paramsContainer.require() @@ -56,6 +62,7 @@ internal class OnboardingEntryModel @Inject constructor( } val startRoute = routeByProductType(params.scanResponse) + val onboardingTwinModelCallbacks = OnboardingTwinModelCallbacks() fun onManageTokensDone() { navigateToFinalScreenFlow() @@ -66,28 +73,25 @@ internal class OnboardingEntryModel @Inject constructor( } private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute { - val multiWalletNavigationMode = when (params.multiWalletMode) { - OnboardingEntryComponent.MultiWalletMode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding - OnboardingEntryComponent.MultiWalletMode.AddBackup -> OnboardingMultiWalletComponent.Mode.AddBackup - } - return when (scanResponse.productType) { - ProductType.Wallet -> OnboardingRoute.MultiWallet( - scanResponse = scanResponse, - withSeedPhraseFlow = false, - titleProvider = titleProvider, - onDone = ::onMultiWalletOnboardingDone, - mode = multiWalletNavigationMode, - ) - ProductType.Ring, + ProductType.Wallet, ProductType.Wallet2, - -> OnboardingRoute.MultiWallet( - scanResponse = scanResponse, - withSeedPhraseFlow = true, - titleProvider = titleProvider, - onDone = ::onMultiWalletOnboardingDone, - mode = multiWalletNavigationMode, - ) + ProductType.Ring, + -> { + val multiWalletNavigationMode = when (params.mode) { + Mode.Onboarding -> OnboardingMultiWalletComponent.Mode.Onboarding + Mode.AddBackupWallet1 -> OnboardingMultiWalletComponent.Mode.AddBackup + else -> error("Incorrect onboarding type") + } + + OnboardingRoute.MultiWallet( + scanResponse = scanResponse, + withSeedPhraseFlow = scanResponse.productType != ProductType.Wallet, + titleProvider = titleProvider, + onDone = ::onMultiWalletOnboardingDone, + mode = multiWalletNavigationMode, + ) + } ProductType.Visa -> OnboardingRoute.Visa( scanResponse = scanResponse, titleProvider = titleProvider, @@ -98,14 +102,33 @@ internal class OnboardingEntryModel @Inject constructor( titleProvider = titleProvider, onDone = ::navigateToFinalScreenFlow, ) + ProductType.Twins -> { + val mode = when (params.mode) { + Mode.Onboarding -> OnboardingTwinComponent.Params.Mode.CreateWallet + Mode.WelcomeOnlyTwin -> OnboardingTwinComponent.Params.Mode.WelcomeOnly + Mode.RecreateWalletTwin -> OnboardingTwinComponent.Params.Mode.RecreateWallet + else -> error("Incorrect onboarding type") + } + + OnboardingRoute.Twins( + scanResponse = scanResponse, + titleProvider = titleProvider, + mode = mode, + ) + } else -> error("Unsupported") } } private fun onMultiWalletOnboardingDone(userWallet: UserWallet) { when { - params.multiWalletMode == OnboardingEntryComponent.MultiWalletMode.AddBackup -> { - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + params.mode == Mode.AddBackupWallet1 -> { + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = OnboardingDoneComponent.Mode.WalletCreated, + onDone = ::exitComponentScreen, + ), + ) } userWallet.scanResponse.cardTypesResolver.isMultiwalletAllowed() -> { stackNavigation.replaceAll(OnboardingRoute.ManageTokens(userWallet)) @@ -117,48 +140,104 @@ internal class OnboardingEntryModel @Inject constructor( } private fun onVisaOnboardingDone() { - navigateToFinalScreenFlow() + navigateToFinalScreenFlow(doneMode = OnboardingDoneComponent.Mode.GoodToGo) } - private fun navigateToFinalScreenFlow() { + private fun navigateToFinalScreenFlow( + doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated, + ) { if (askBiometryFeatureToggles.isAskForBiometryEnabled) { modelScope.launch { if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { stackNavigation.replaceAll( - OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks()), + OnboardingRoute.AskBiometry(modelCallbacks = AskBiometryModelCallbacks(doneMode)), ) } else { - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = doneMode, + onDone = ::exitComponentScreen, + ), + ) } } } else { - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = doneMode, + onDone = ::exitComponentScreen, + ), + ) } } - inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks { + inner class AskBiometryModelCallbacks( + private val doneMode: OnboardingDoneComponent.Mode, + ) : AskBiometryComponent.ModelCallbacks { override fun onAllowed() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.On)) - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = doneMode, + onDone = ::exitComponentScreen, + ), + ) } override fun onDenied() { analyticsEventHandler.send(OnboardingEntryEvent.Biometric(OnboardingEntryEvent.Biometric.State.Off)) - stackNavigation.replaceAll(OnboardingRoute.Done(onDone = ::navigateToWalletScreen)) + stackNavigation.replaceAll( + OnboardingRoute.Done( + mode = doneMode, + onDone = ::exitComponentScreen, + ), + ) } } - private fun navigateToWalletScreen() { + private fun exitComponentScreen() { if (askBiometryFeatureToggles.isAskForBiometryEnabled) { - router.replaceAll(AppRoute.Wallet) - } else { - modelScope.launch(NonCancellable) { - router.replaceAll(AppRoute.Wallet) - if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { - delay(timeMillis = 1_800) - router.push(AppRoute.SaveWallet) + if (userWalletsListManager.hasUserWallets) { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } + + if (isLocked) { + router.replaceAll(AppRoute.Welcome()) + } else { + router.replaceAll(AppRoute.Wallet) } + } else { + router.replaceAll(AppRoute.Home) + } + } else { + if (userWalletsListManager.hasUserWallets) { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } + + if (isLocked) { + router.replaceAll(AppRoute.Welcome()) + } else { + modelScope.launch(NonCancellable) { + router.replaceAll(AppRoute.Wallet) + if (tangemSdkManager.checkCanUseBiometry() && + settingsRepository.shouldShowSaveUserWalletScreen() + ) { + delay(timeMillis = 1_800) + router.push(AppRoute.SaveWallet) + } + } + } + } else { + router.replaceAll(AppRoute.Home) } } } + + inner class OnboardingTwinModelCallbacks : OnboardingTwinComponent.ModelCallbacks { + override fun onDone() { + navigateToFinalScreenFlow() + } + + override fun onBack() { + router.pop() + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt index 608d542ebe..d0e8a13f03 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -4,11 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.managetokens.component.OnboardingManageTokensComponent import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent +import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent import javax.inject.Inject +@Suppress("LongParameterList") internal class OnboardingChildFactory @Inject constructor( private val multiWalletComponentFactory: OnboardingMultiWalletComponent.Factory, private val onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory, @@ -16,9 +19,15 @@ internal class OnboardingChildFactory @Inject constructor( private val onBoardingVisaComponentFactory: OnboardingVisaComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val onboardingNoteComponentFactory: OnboardingNoteComponent.Factory, + private val onboardingTwinComponentFactory: OnboardingTwinComponent.Factory, ) { - fun createChild(route: OnboardingRoute, childContext: AppComponentContext, onManageTokensDone: () -> Unit): Any { + fun createChild( + route: OnboardingRoute, + childContext: AppComponentContext, + model: OnboardingEntryModel, + onManageTokensDone: () -> Unit, + ): Any { return when (route) { is OnboardingRoute.MultiWallet -> multiWalletComponentFactory.create( context = childContext, @@ -48,6 +57,7 @@ internal class OnboardingChildFactory @Inject constructor( is OnboardingRoute.Done -> onboardingDoneComponentFactory.create( context = childContext, params = OnboardingDoneComponent.Params( + mode = route.mode, onDone = route.onDone, ), ) @@ -59,6 +69,15 @@ internal class OnboardingChildFactory @Inject constructor( onDone = route.onDone, ), ) + is OnboardingRoute.Twins -> onboardingTwinComponentFactory.create( + context = childContext, + params = OnboardingTwinComponent.Params( + scanResponse = route.scanResponse, + modelCallbacks = model.onboardingTwinModelCallbacks, + titleProvider = route.titleProvider, + mode = route.mode, + ), + ) is OnboardingRoute.AskBiometry -> askBiometryComponentFactory.create( context = childContext, params = AskBiometryComponent.Params( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt index b111c82d91..4d0b03129a 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -5,7 +5,9 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.onboarding.v2.TitleProvider +import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent sealed class OnboardingRoute : Route { @@ -31,6 +33,12 @@ sealed class OnboardingRoute : Route { val onDone: () -> Unit, ) : OnboardingRoute() + data class Twins( + val titleProvider: TitleProvider, + val scanResponse: ScanResponse, + val mode: OnboardingTwinComponent.Params.Mode, + ) : OnboardingRoute() + data class ManageTokens( val userWallet: UserWallet, ) : OnboardingRoute() @@ -40,6 +48,7 @@ sealed class OnboardingRoute : Route { ) : OnboardingRoute() data class Done( + val mode: OnboardingDoneComponent.Mode, val onDone: () -> Unit, ) : OnboardingRoute() } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt index b7d96ab79a..ff138a6d70 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -26,7 +26,7 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation import com.tangem.core.decompose.navigation.inner.InnerNavigationState import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.MultiWalletAccessCodeComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt index bd4a37a0b5..a432deb8d0 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/model/MultiWalletAccessCodeModel.kt @@ -6,7 +6,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.accesscode.ui.state.MultiWalletAccessCodeUM import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt index 8ac18706c6..2e7c8e89c6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeBS.kt @@ -36,7 +36,7 @@ private fun Content(state: MultiWalletAccessCodeUM, modifier: Modifier = Modifie modifier = Modifier.weight(1f), targetState = state.step, transitionSpec = TangemAnimations.AnimatedContent - .slide { initial, target -> initial.ordinal > target.ordinal }, + .slide { initial, target -> target.ordinal > initial.ordinal }, label = "AnimatedContent", ) { step -> when (step) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt index 3f0fe0f290..2e6166a337 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/accesscode/ui/MultiWalletAccessCodeIntro.kt @@ -31,7 +31,7 @@ internal fun MultiWalletAccessCodeIntro(modifier: Modifier = Modifier) { color = TangemTheme.colors.text.primary1, ) Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_access_code), + imageVector = ImageVector.vectorResource(R.drawable.ic_access_code_119), tint = TangemTheme.colors.icon.informative, contentDescription = null, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt index 6728f170e4..e9be6ab78d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/backup/model/MultiWalletBackupModel.kt @@ -1,11 +1,12 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.model import androidx.compose.runtime.Stable -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -19,8 +20,8 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ProductType +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.MultiWalletBackupComponent import com.tangem.features.onboarding.v2.multiwallet.impl.child.backup.ui.backupCardAttestationFailedDialog @@ -48,6 +49,7 @@ class MultiWalletBackupModel @Inject constructor( private val cardSdkConfigRepository: CardSdkConfigRepository, private val tangemSdkManager: TangemSdkManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val uiMessageSender: UiMessageSender, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val cardRepository: CardRepository, @@ -241,7 +243,7 @@ class MultiWalletBackupModel @Inject constructor( ) } } - else -> FirebaseCrashlytics.getInstance().recordException(result.error) + else -> analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(result.error)) } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt index c460852063..c5cd73b286 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/chooseoption/model/Wallet1ChooseOptionModel.kt @@ -10,9 +10,8 @@ import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow @@ -25,7 +24,7 @@ import javax.inject.Inject internal class Wallet1ChooseOptionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val cardRepository: CardRepository, private val saveWalletUseCase: SaveWalletUseCase, private val analyticsHandler: AnalyticsEventHandler, @@ -67,7 +66,7 @@ internal class Wallet1ChooseOptionModel @Inject constructor( private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { return requireNotNull( - value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(), + value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), lazyMessage = { "User wallet not created" }, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 235a5a45ff..86226a42b6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -15,10 +15,9 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog @@ -43,7 +42,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val cardRepository: CardRepository, private val analyticsHandler: AnalyticsEventHandler, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, ) : Model() { @@ -146,7 +145,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { return requireNotNull( - value = UserWalletBuilder(scanResponse, generateWalletNameUseCase).build(), + value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), lazyMessage = { "User wallet not created" }, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt index 6f1af9d0a3..2a6a97474f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/ui/state/MultiWalletCreateWalletUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM internal data class MultiWalletCreateWalletUM( val title: TextReference, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt index ebf937ae3d..b202c464d6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/model/MultiWalletFinalizeModel.kt @@ -20,7 +20,6 @@ import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent @@ -49,7 +48,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private val tangemSdkManager: TangemSdkManager, private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, @@ -279,7 +278,7 @@ internal class MultiWalletFinalizeModel @Inject constructor( private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { return requireNotNull( - value = UserWalletBuilder(scanResponse, generateWalletNameUseCase) + value = userWalletBuilderFactory.create(scanResponse = scanResponse) .backupCardsIds(backupCardIds.toSet()) .hasBackupError(walletHasBackupError) .build(), diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt index b367e365b4..00387df368 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/finalize/ui/state/MultiWalletFinalizeUM.kt @@ -1,6 +1,6 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.finalize.ui.state -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM internal data class MultiWalletFinalizeUM( val step: Step = Step.Primary, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index 7b75b80c4e..fcb2c47870 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -13,13 +13,13 @@ import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.GenerateSeedPhraseUiStateBuilder import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.ImportSeedPhraseUiStateBuilder import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder.SeedPhraseCheckUiStateBuilder import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.resetCardDialog import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt index 3ac5e6a920..985a7b68e4 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhrase.kt @@ -12,7 +12,7 @@ internal fun MultiWalletSeedPhrase(state: MultiWalletSeedPhraseUM, modifier: Mod AnimatedContent( modifier = modifier.navigationBarsPadding(), targetState = state, - transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order < from.order }, + transitionSpec = TangemAnimations.AnimatedContent.slide { from, to -> to.order > from.order }, contentKey = { st -> st::class.java }, label = "animatedContent", ) { st -> diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt index f6dd64808c..c167a59481 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/state/MultiWalletSeedPhraseUM.kt @@ -4,8 +4,8 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.text.input.TextFieldValue import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt index ecf06866f2..9b1a7be5b1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/common/ui/Dialogs.kt @@ -1,6 +1,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.common.ui import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM import com.tangem.features.onboarding.v2.impl.R internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismissButtonClick: () -> Unit) = @@ -19,20 +20,4 @@ internal fun resetCardDialog(onConfirm: () -> Unit, dismiss: () -> Unit, onDismi onDismissButtonClick() dismiss() }, - ) - -internal fun interruptBackupDialog(onConfirm: () -> Unit, dismiss: () -> Unit) = OnboardingDialogUM( - title = resourceReference(R.string.onboarding_exit_alert_title), - message = resourceReference(R.string.onboarding_exit_alert_message), - confirmButtonText = resourceReference(R.string.common_ok), - dismissButtonText = resourceReference(R.string.common_cancel), - dismissWarningColor = true, - onDismiss = dismiss, - onConfirmClick = { - onConfirm() - dismiss() - }, - onDismissButtonClick = { - dismiss() - }, -) \ No newline at end of file + ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt index f1c33ffc66..c55cb1d1a9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -5,34 +5,40 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.wallets.usecase.GetCardImageUseCase import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent -import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.multiwallet.impl.child.MultiWalletChildParams -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.interruptBackupDialog import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletState.FinalizeStage import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class OnboardingMultiWalletModel @Inject constructor( paramsContainer: ParamsContainer, + analyticsHandler: AnalyticsEventHandler, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val analyticsHandler: AnalyticsEventHandler, private val backupServiceHolder: BackupServiceHolder, private val onboardingRepository: OnboardingRepository, + private val getCardImageUseCase: GetCardImageUseCase, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() - private val getCardImageUseCase = GetCardImageUseCase() private val _uiState = MutableStateFlow(OnboardingMultiWalletUM()) val state = MutableStateFlow( @@ -56,19 +62,16 @@ internal class OnboardingMultiWalletModel @Inject constructor( } fun onBack() { - _uiState.update { st -> - st.copy( - dialog = interruptBackupDialog( - onConfirm = { - modelScope.launch { - onboardingRepository.clearUnfinishedFinalizeOnboarding() - router.pop() - } - }, - dismiss = { _uiState.update { it.copy(dialog = null) } }, - ), - ) - } + uiMessageSender.send( + interruptBackupDialog( + onConfirm = { + modelScope.launch { + onboardingRepository.clearUnfinishedFinalizeOnboarding() + router.pop() + } + }, + ), + ) } private fun subscribeToBackups() { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt index 62bc6aaf7a..4af97756ab 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt @@ -8,9 +8,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM @@ -23,23 +20,6 @@ internal fun OnboardingMultiWallet( modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit = {}, ) { - if (state.dialog != null) { - BasicDialog( - title = state.dialog.title.resolveReference(), - message = state.dialog.message.resolveReference(), - confirmButton = DialogButtonUM( - title = state.dialog.confirmButtonText.resolveReference(), - onClick = state.dialog.onConfirmClick, - ), - dismissButton = DialogButtonUM( - title = state.dialog.dismissButtonText.resolveReference(), - warning = state.dialog.dismissWarningColor, - onClick = state.dialog.onDismissButtonClick, - ), - onDismissDialog = state.dialog.onDismiss, - ) - } - Column( modifier = modifier.fillMaxSize(), ) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt index 5aa205dc81..829828761d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt @@ -1,10 +1,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state -import com.tangem.features.onboarding.v2.multiwallet.impl.common.ui.OnboardingDialogUM - internal data class OnboardingMultiWalletUM( val artwork1Url: String? = null, val artwork2Url: String? = null, val artwork3Url: String? = null, - val dialog: OnboardingDialogUM? = null, ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt index d3ca96fa30..bea41ef278 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/DefaultOnboardingNoteComponent.kt @@ -25,15 +25,16 @@ import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel +import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch internal class DefaultOnboardingNoteComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @@ -47,6 +48,7 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( } private val childParams = ChildParams( + commonState = model.commonUiState, onBack = ::onChildBack, parentBackEvent = currentChildBackEventHandle, ) @@ -70,15 +72,13 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( override val state: StateFlow = model.innerNavigationState override fun pop(onComplete: (Boolean) -> Unit) { - // TODO [REDACTED_TASK_KEY] + model.onChildBack( + currentRoute = childStack.value.active.configuration, + ) } } init { - componentScope.launch { - model.onDone.collect { params.onDone() } - } - // sets title and stepper value childStack.subscribe(lifecycle) { stack -> val currentRoute = stack.active.configuration @@ -96,14 +96,17 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( appComponentContext = factoryContext, params = OnboardingNoteCreateWalletComponent.Params( childParams = childParams, - onDone = { model.stackNavigation.push(OnboardingNoteRoute.TopUp) }, + onWalletCreated = { userWallet -> + model.onWalletCreated(userWallet) + model.stackNavigation.push(OnboardingNoteRoute.TopUp) + }, ), ) OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent( appComponentContext = factoryContext, params = OnboardingNoteTopUpComponent.Params( childParams = childParams, - onDone = { model.onDone.tryEmit(Unit) }, + onDone = { params.onDone() }, ), ) } @@ -134,6 +137,7 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor( } data class ChildParams( + val commonState: MutableStateFlow, val onBack: () -> Unit, val parentBackEvent: SharedFlow, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/OnboardingNoteCreateWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/OnboardingNoteCreateWalletComponent.kt index 5751eb80ba..603ce3d688 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/OnboardingNoteCreateWalletComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/OnboardingNoteCreateWalletComponent.kt @@ -9,6 +9,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel import com.tangem.features.onboarding.v2.note.impl.child.create.ui.OnboardingNoteCreateWallet @@ -24,7 +25,7 @@ internal class OnboardingNoteCreateWalletComponent( override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() - BackHandler(onBack = remember(this) { { params.childParams.onBack() } }) + BackHandler(onBack = remember(this) { { params.childParams.onBack } }) OnboardingNoteCreateWallet( modifier = modifier, @@ -34,6 +35,6 @@ internal class OnboardingNoteCreateWalletComponent( data class Params( val childParams: DefaultOnboardingNoteComponent.ChildParams, - val onDone: () -> Unit, + val onWalletCreated: (UserWallet) -> Unit, ) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index 93634beb79..59f7b2e7ab 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -1,26 +1,103 @@ package com.tangem.features.onboarding.v2.note.impl.child.create.model +import com.tangem.common.CompletionResult +import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent import com.tangem.features.onboarding.v2.note.impl.child.create.ui.state.OnboardingNoteCreateWalletUM import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject -@Suppress("UnusedPrivateMember") @ModelScoped internal class OnboardingNoteCreateWalletModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val tangemSdkManager: TangemSdkManager, + private val cardRepository: CardRepository, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, + private val saveWalletUseCase: SaveWalletUseCase, ) : Model() { + private val params = paramsContainer.require() + private val _uiState = MutableStateFlow( - OnboardingNoteCreateWalletUM(), + OnboardingNoteCreateWalletUM( + onCreateClick = ::onCreateWalletClick, + ), ) + init { + Analytics.send(OnboardingEvent.CreateWallet.ScreenOpened) + modelScope.launch { + val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch + if (!cardRepository.isActivationStarted(scanResponse.card.cardId)) { + Analytics.send(OnboardingEvent.Started) + } + } + observeArtwork() + } + val uiState: StateFlow = _uiState + + private fun onCreateWalletClick() { + modelScope.launch { + _uiState.update { + it.copy(createWalletInProgress = true) + } + val scanResponse = params.childParams.commonState.value.scanResponse ?: return@launch + val result = tangemSdkManager.createProductWallet(scanResponse) + when (result) { + is CompletionResult.Success -> { + Analytics.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + cardRepository.startCardActivation(scanResponse.card.cardId) + createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card)) + } + is CompletionResult.Failure -> _uiState.update { + it.copy(createWalletInProgress = false) + } + } + } + } + + private fun createWalletAndNavigateBackWithDone(scanResponse: ScanResponse) { + modelScope.launch { + val userWallet = createUserWallet(scanResponse) + saveWalletUseCase(userWallet, canOverride = true).onRight { + params.onWalletCreated(userWallet) + } + _uiState.update { + it.copy(createWalletInProgress = false) + } + } + } + + private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet { + return requireNotNull( + value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), + lazyMessage = { "User wallet not created" }, + ) + } + + private fun observeArtwork() { + modelScope.launch { + params.childParams.commonState.collect { + _uiState.value = _uiState.value.copy( + artworkUrl = it.cardArtworkUrl, + ) + } + } + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt similarity index 100% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWallet.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/ui/OnboardingNoteCreateWalletScreen.kt diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt index f2a8e3bf60..28a0eda74b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/model/OnboardingNoteTopUpModel.kt @@ -1,24 +1,253 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.model +import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import com.tangem.utils.extensions.isPositive +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject -@Suppress("UnusedPrivateMember") +@Suppress("LongParameterList") @ModelScoped internal class OnboardingNoteTopUpModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, + private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, + private val urlOpener: UrlOpener, + private val clipboardManager: ClipboardManager, + private val shareManager: ShareManager, + private val rampStateManager: RampStateManager, + private val cardRepository: CardRepository, + private val saveWalletUseCase: SaveWalletUseCase, ) : Model() { + private val params = paramsContainer.require() + private val commonState = params.childParams.commonState + private val scanResponse = params.childParams.commonState.value.scanResponse + private var userWallet = params.childParams.commonState.value.userWallet + private val _uiState = MutableStateFlow( - OnboardingNoteTopUpUM(), + OnboardingNoteTopUpUM( + onRefreshBalanceClick = ::refreshBalance, + onBuyCryptoClick = ::onBuyCryptoClick, + onShowWalletAddressClick = ::onShowWalletAddressClick, + onDismissBottomSheet = ::onDismissBottomSheet, + ), ) val uiState: StateFlow = _uiState + + init { + Analytics.send(OnboardingEvent.Topup.ScreenOpened) + observeArtwork() + modelScope.launch { + createUserWalletIfNull() + observeCryptoCurrencyStatus() + refreshBalance() + } + } + + private fun refreshBalance() { + modelScope.launch { + showBalanceLoadingProgress(true) + createUserWalletIfNull() + val userWalletId = requireNotNull(userWallet?.walletId) + fetchCurrencyStatusUseCase( + userWalletId = userWalletId, + refresh = true, + ) + showBalanceLoadingProgress(false) + } + } + + private fun onBuyCryptoClick() { + val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return + modelScope.launch { + getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight { + urlOpener.openUrl(it) + } + } + Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency)) + } + + private fun onShowWalletAddressClick() { + val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return + val networkAddress = currencyStatus.value.networkAddress ?: return + + _uiState.update { + it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress)) + } + Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress) + } + + private fun onDismissBottomSheet() { + _uiState.update { + it.copy(addressBottomSheetConfig = null) + } + } + + private suspend fun createUserWalletIfNull() { + if (userWallet != null) { + return + } + val commonState = params.childParams.commonState.value + userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse) + } + + private fun observeArtwork() { + modelScope.launch { + params.childParams.commonState.collect { + _uiState.value = _uiState.value.copy( + cardArtworkUrl = it.cardArtworkUrl, + ) + } + } + } + + private fun observeCryptoCurrencyStatus() { + val userWalletId = userWallet?.walletId ?: return + getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWalletId).map { it.getOrNull() }.filterNotNull() + .onEach(::applyCryptoCurrencyStatusToState).launchIn(modelScope) + } + + private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { + if (commonState.value.cryptoCurrencyStatus == null) { + loadAvailableForBuy(status) + } + + commonState.update { + it.copy(cryptoCurrencyStatus = status) + } + + val amount = when (status.value) { + is CryptoCurrencyStatus.Loaded -> status.value.amount + is CryptoCurrencyStatus.NoAccount -> status.value.amount + is CryptoCurrencyStatus.NoQuote -> status.value.amount + else -> null + } + val hasCurrentNetworkTransactions = when (status.value) { + is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions + is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions + else -> false + } + val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount + + if (amount?.isPositive() == true || hasCurrentNetworkTransactions) { + modelScope.launch { + cardRepository.finishCardActivation(scanResponse.card.cardId) + params.onDone() + } + } + + _uiState.update { + it.copy( + amountToCreateAccount = amountToCreateAccount + ?.format( + { + crypto( + symbol = status.currency.symbol, + decimals = status.currency.decimals, + ) + }, + ), + balance = amount?.format( + { + crypto( + symbol = status.currency.symbol, + decimals = status.currency.decimals, + ) + }, + ).orEmpty(), + isTopUpDataLoading = status.value.networkAddress == null, + ) + } + } + + private fun showBalanceLoadingProgress(value: Boolean) { + _uiState.update { + it.copy(isRefreshing = value) + } + } + + private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) { + val userWalletId = userWallet?.walletId ?: return + + modelScope.launch { + val availableForBuy = rampStateManager.availableForBuy( + scanResponse = scanResponse, + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + ) + _uiState.update { + it.copy( + availableForBuy = availableForBuy, + availableForBuyLoading = false, + ) + } + } + } + + private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) = + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = uiState.value.onDismissBottomSheet, + content = TokenReceiveBottomSheetConfig( + name = currencyStatus.currency.name, + symbol = currencyStatus.currency.symbol, + network = currencyStatus.currency.network.name, + addresses = networkAddress.availableAddresses + .mapToAddressModels(currencyStatus.currency) + .toImmutableList(), + showMemoDisclaimer = + currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + onCopyClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) + }, + onShareClick = { + Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol)) + shareManager.shareText(text = it) + }, + ), + ) + + private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet { + val wallet = requireNotNull( + value = userWalletBuilderFactory.create(scanResponse = scanResponse).build(), + lazyMessage = { "User wallet not created" }, + ) + saveWalletUseCase(wallet, false) + return wallet + } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardinNoteTopUpHeader.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt similarity index 56% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardinNoteTopUpHeader.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt index e0c96f997b..381b360e79 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardinNoteTopUpHeader.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpHeader.kt @@ -1,46 +1,44 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.SpacerHMax import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalTangemShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.common.ui.RefreshButton import com.tangem.features.onboarding.v2.common.ui.WalletCard import com.tangem.features.onboarding.v2.impl.R +import com.valentinilk.shimmer.shimmer @Composable -fun OnboardinNoteTopUpHeader( - balance: String?, +fun OnboardingNoteTopUpHeader( + balance: String, cardArtworkUrl: String?, isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, modifier: Modifier = Modifier, ) { Box( - modifier = modifier.fillMaxWidth(), + modifier = modifier + .heightIn(min = 228.dp) + .widthIn(max = 450.dp), contentAlignment = Alignment.Center, ) { Box( modifier = Modifier - .padding(vertical = 24.dp, horizontal = 32.dp) + .padding(vertical = 24.dp) .fillMaxSize() .background( TangemTheme.colors.button.secondary, @@ -64,10 +62,17 @@ fun OnboardinNoteTopUpHeader( ) SpacerH8() Text( - text = balance.orEmpty(), + modifier = if (balance.isEmpty()) { + Modifier + .width(120.dp) + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius3)) + .shimmer(LocalTangemShimmer.current) + } else { + Modifier + }, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, + text = balance, ) SpacerHMax() RefreshButton( @@ -78,44 +83,11 @@ fun OnboardinNoteTopUpHeader( } } -@Composable -private fun RefreshButton(isRefreshing: Boolean, onRefreshBalanceClick: () -> Unit, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size48) - .shadow(elevation = 2.dp, shape = CircleShape) - .background(TangemTheme.colors.background.action) - .clickable( - enabled = !isRefreshing, - onClick = onRefreshBalanceClick, - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - ), - ) { - if (isRefreshing) { - Box { - CircularProgressIndicator( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - color = TangemTheme.colors.icon.primary1, - strokeWidth = TangemTheme.dimens.size2, - ) - } - } else { - Icon( - painter = painterResource(id = R.drawable.ic_refresh_24), - contentDescription = null, - tint = TangemTheme.colors.text.disabled, - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - ) - } - } -} - @Preview(showBackground = true) @Composable private fun OnboardinNoteTopUpHeaderPreview() { TangemThemePreview { - OnboardinNoteTopUpHeader( + OnboardingNoteTopUpHeader( balance = "0.00000001 BTC", cardArtworkUrl = "", onRefreshBalanceClick = {}, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt similarity index 69% rename from features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt rename to features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt index a850b14829..e8dbf64055 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUp.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/OnboardingNoteTopUpScreen.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -10,14 +11,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R -import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM @Composable @@ -27,23 +27,25 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif .fillMaxSize() .navigationBarsPadding(), verticalArrangement = Arrangement.Bottom, + horizontalAlignment = Alignment.CenterHorizontally, ) { - OnboardinNoteTopUpHeader( + OnboardingNoteTopUpHeader( balance = state.balance, cardArtworkUrl = state.cardArtworkUrl, onRefreshBalanceClick = state.onRefreshBalanceClick, isRefreshing = state.isRefreshing, modifier = Modifier .padding(horizontal = 34.dp) - .padding(top = 20.dp) - .weight(ALL_STEPS_TOP_CONTAINER_WEIGHT) + .padding(top = 16.dp) + .height(228.dp) .fillMaxWidth(), ) Column( modifier = Modifier .fillMaxWidth() .verticalScroll(rememberScrollState()) - .padding(32.dp) + .padding(horizontal = 32.dp) + .padding(top = 16.dp) .weight(1.0f), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -55,8 +57,16 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif modifier = Modifier.padding(top = 16.dp), ) + val text = if (state.amountToCreateAccount != null) { + stringResourceSafe( + R.string.onboarding_top_up_min_create_account_amount, + state.amountToCreateAccount, + ) + } else { + stringResourceSafe(R.string.onboarding_top_up_body) + } Text( - text = stringResourceSafe(R.string.onboarding_top_up_body), + text = text, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -65,38 +75,43 @@ fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modif } BottomButtons(state) + + state.addressBottomSheetConfig?.let { config -> + TokenReceiveBottomSheet(config = config) + } } } @Composable private fun BottomButtons(state: OnboardingNoteTopUpUM) { if (state.availableForBuy) { - PrimaryButtonIconEnd( + PrimaryButton( modifier = Modifier - .padding(horizontal = 16.dp) + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) .fillMaxWidth(), - iconResId = R.drawable.ic_tangem_24, text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), onClick = state.onBuyCryptoClick, ) - SpacerH8() - SecondaryButton( - modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) - .fillMaxWidth(), - text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), - onClick = state.onShowWalletAddressClick, - ) } else { - PrimaryButtonIconEnd( + PrimaryButton( modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - iconResId = R.drawable.ic_tangem_24, text = stringResourceSafe(R.string.onboarding_button_receive_crypto), onClick = state.onShowWalletAddressClick, ) } + AnimatedVisibility(visible = !state.availableForBuyLoading) { + if (state.availableForBuy) { + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), + onClick = state.onShowWalletAddressClick, + ) + } + } } @Preview(showBackground = true) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt index 762ccae2e8..327de0d586 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/topup/ui/state/OnboardingNoteTopUpUM.kt @@ -1,12 +1,18 @@ package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig + data class OnboardingNoteTopUpUM( val cardArtworkUrl: String? = null, - val availableForBuy: Boolean = true, - val balance: String? = null, + val availableForBuy: Boolean = false, + val availableForBuyLoading: Boolean = true, + val balance: String = "", val isRefreshing: Boolean = false, + val isTopUpDataLoading: Boolean = true, val amountToCreateAccount: String? = null, + val addressBottomSheetConfig: TangemBottomSheetConfig? = null, val onBuyCryptoClick: () -> Unit = {}, val onShowWalletAddressClick: () -> Unit = {}, val onRefreshBalanceClick: () -> Unit = {}, + val onDismissBottomSheet: () -> Unit = {}, ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt new file mode 100644 index 0000000000..8ebe5bcd8b --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteCommonState.kt @@ -0,0 +1,12 @@ +package com.tangem.features.onboarding.v2.note.impl.model + +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet + +internal data class OnboardingNoteCommonState( + val scanResponse: ScanResponse, + val userWallet: UserWallet? = null, + val cardArtworkUrl: String? = null, + val cryptoCurrencyStatus: CryptoCurrencyStatus? = null, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt index 87a00a91e1..30733d3ff8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteModel.kt @@ -4,46 +4,54 @@ import com.arkivanov.decompose.router.stack.StackNavigation import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetCardImageUseCase +import com.tangem.features.onboarding.v2.common.ui.exitOnboardingDialog import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent import com.tangem.features.onboarding.v2.note.impl.OnboardingNoteInnerNavigationState import com.tangem.features.onboarding.v2.note.impl.route.OnboardingNoteRoute import com.tangem.features.onboarding.v2.note.impl.route.stepNum import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import javax.inject.Inject @ModelScoped internal class OnboardingNoteModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val messageSender: UiMessageSender, + private val getCardImageUseCase: GetCardImageUseCase, ) : Model() { @Suppress("UnusedPrivateMember") private val params = paramsContainer.require() - private val _currentScanResponse = MutableStateFlow(params.scanResponse) - - val initialRoute = OnboardingNoteRoute.CreateWallet // TODO [REDACTED_TASK_KEY] + val initialRoute = initializeRoute() private val _innerNavigationState = MutableStateFlow(OnboardingNoteInnerNavigationState(stackSize = initialRoute.stepNum())) - val currentScanResponse = _currentScanResponse.asStateFlow() - val innerNavigationState = _innerNavigationState.asStateFlow() - val onDone = MutableSharedFlow() - val stackNavigation = StackNavigation() private val _uiState = MutableStateFlow( - OnboardingNoteState(), + OnboardingNoteCommonState( + scanResponse = params.scanResponse, + ), ) - val uiState: StateFlow = _uiState + val commonUiState: MutableStateFlow = _uiState + + init { + loadArtworkUrl() + } fun updateStepForNewRoute(route: OnboardingNoteRoute) { _innerNavigationState.value = innerNavigationState.value.copy( @@ -53,6 +61,40 @@ internal class OnboardingNoteModel @Inject constructor( @Suppress("UnusedPrivateMember") fun onChildBack(currentRoute: OnboardingNoteRoute) { - // TODO [REDACTED_TASK_KEY] + messageSender.send( + exitOnboardingDialog( + onConfirm = { + router.pop() + }, + ), + ) + } + + fun onWalletCreated(userWallet: UserWallet) { + commonUiState.update { + it.copy(userWallet = userWallet) + } + } + + private fun initializeRoute(): OnboardingNoteRoute { + val card = params.scanResponse.card + return if (card.wallets.isEmpty()) { + OnboardingNoteRoute.CreateWallet + } else { + OnboardingNoteRoute.TopUp + } + } + + private fun loadArtworkUrl() { + modelScope.launch { + val cardInfo = params.scanResponse.card + val url = getCardImageUseCase( + cardId = cardInfo.cardId, + cardPublicKey = cardInfo.cardPublicKey, + ) + _uiState.update { + it.copy(cardArtworkUrl = url) + } + } } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteState.kt deleted file mode 100644 index 7d183852bb..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/model/OnboardingNoteState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.onboarding.v2.note.impl.model - -data class OnboardingNoteState( - val cardArtworkUrl: String? = null, -) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt new file mode 100644 index 0000000000..2a28cf2e89 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/api/OnboardingTwinComponent.kt @@ -0,0 +1,27 @@ +package com.tangem.features.onboarding.v2.twin.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.TitleProvider + +interface OnboardingTwinComponent : ComposableContentComponent { + + data class Params( + val scanResponse: ScanResponse, + val modelCallbacks: ModelCallbacks, + val titleProvider: TitleProvider, + val mode: Mode, + ) { + enum class Mode { + WelcomeOnly, CreateWallet, RecreateWallet + } + } + + interface ModelCallbacks { + fun onDone() + fun onBack() + } + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt new file mode 100644 index 0000000000..ded3238c83 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/DefaultOnboardingTwinComponent.kt @@ -0,0 +1,67 @@ +package com.tangem.features.onboarding.v2.twin.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerNavigation +import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder +import com.tangem.core.decompose.navigation.inner.InnerNavigationState +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel +import com.tangem.features.onboarding.v2.twin.impl.ui.OnboardingTwin +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.StateFlow + +internal class DefaultOnboardingTwinComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: OnboardingTwinComponent.Params, +) : OnboardingTwinComponent, AppComponentContext by appComponentContext, InnerNavigationHolder { + + private val model: OnboardingTwinModel = getOrCreateModel(params) + + init { + params.titleProvider.changeTitle(resourceReference(R.string.twins_recreate_toolbar)) + } + + override val innerNavigation: InnerNavigation = object : InnerNavigation { + override val state: StateFlow = model.innerNavigationState + + override fun pop(onComplete: (Boolean) -> Unit) { + model.onBack() + } + } + + @Composable + override fun Content(modifier: Modifier) { + BackHandler { model.onBack() } + + val state by model.uiState.collectAsStateWithLifecycle() + + OnboardingTwin( + state = state, + modifier = modifier, + ) + } + + data class TwinInnerNavigationState( + override val stackSize: Int, + ) : InnerNavigationState { + override val stackMaxSize: Int? = 5 + } + + @AssistedFactory + interface Factory : OnboardingTwinComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnboardingTwinComponent.Params, + ): DefaultOnboardingTwinComponent + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..41e30634d5 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/di/ComponentModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.onboarding.v2.twin.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.impl.model.OnboardingTwinModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponent(factory: DefaultOnboardingTwinComponent.Factory): OnboardingTwinComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(OnboardingTwinModel::class) + fun provideModel(model: OnboardingTwinModel): Model +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt new file mode 100644 index 0000000000..a819a60957 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/Dialogs.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onboarding.v2.twin.impl.model + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.onboarding.v2.impl.R + +internal val TwinProcessNotCompleted = DialogMessage( + message = resourceReference(R.string.onboarding_twin_exit_warning), + firstAction = EventMessageAction( + title = resourceReference(R.string.warning_button_ok), + onClick = {}, + ), +) + +internal fun cardVerificationFailedDialog( + errorDescription: TextReference, + onRequestSupport: () -> Unit, + onCancelClick: () -> Unit = {}, +): DialogMessage { + return DialogMessage( + message = errorDescription, + title = resourceReference(id = R.string.security_alert_title), + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(id = R.string.alert_button_request_support), + onClick = onRequestSupport, + ) + }, + secondActionBuilder = { cancelAction(onClick = onCancelClick) }, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt new file mode 100644 index 0000000000..1fb3811bcf --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -0,0 +1,461 @@ +package com.tangem.features.onboarding.v2.twin.impl.model + +import com.tangem.Message +import com.tangem.common.CompletionResult +import com.tangem.common.KeyPair +import com.tangem.common.core.TangemError +import com.tangem.common.core.TangemSdkError +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toHexString +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.toWrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage +import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase +import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent +import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params.Mode +import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent +import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM +import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.sdk.extensions.localizedDescriptionRes +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import java.math.BigDecimal +import javax.inject.Inject + +@Suppress("LongParameterList", "LargeClass") +@ModelScoped +internal class OnboardingTwinModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, + private val userWalletsListManager: UserWalletsListManager, + private val analyticsEventHandler: AnalyticsEventHandler, + private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase, + private val tangemSdkManager: TangemSdkManager, + private val issuersConfigStorage: IssuersConfigStorage, + private val cardRepository: CardRepository, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, + private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase, + private val urlOpener: UrlOpener, + private val uiMessageSender: UiMessageSender, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, +) : Model() { + + private val params = paramsContainer.require() + private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin") + private val cryptoCurrencyStatusJobHolder = JobHolder() + + private val _uiState = MutableStateFlow( + when (params.mode) { + Mode.WelcomeOnly -> { + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::saveWalletAndDone, + ) + } + Mode.RecreateWallet -> { + OnboardingTwinUM.ResetWarning( + onAcceptClick = { toggle -> + update { it.copy(acceptToggle = toggle) } + }, + onContinueClick = { + navigateToFirstScan() + }, + ) + } + Mode.CreateWallet -> { + OnboardingTwinUM.Welcome( + pairCardNumber = firstCardTwinNumber.pairNumber().number, + onContinueClick = ::navigateToFirstScan, + ) + } + }, + ) + + val uiState = _uiState.asStateFlow() + val innerNavigationState = MutableStateFlow(DefaultOnboardingTwinComponent.TwinInnerNavigationState(1)) + + init { + if (_uiState.value is OnboardingTwinUM.Welcome) { + analyticsEventHandler.send(OnboardingEvent.Twins.ScreenOpened) + modelScope.launch { + saveTwinsOnboardingShownUseCase() + } + } + } + + fun onBack() { + val state = _uiState.value + val twinningInProgress = state is OnboardingTwinUM.ScanCard && + (state.step == OnboardingTwinUM.ScanCard.Step.Second || state.step == OnboardingTwinUM.ScanCard.Step.Third) + + if (twinningInProgress) { + uiMessageSender.send(TwinProcessNotCompleted) + } else { + uiMessageSender.send( + interruptBackupDialog( + onConfirm = { params.modelCallbacks.onBack() }, + ), + ) + } + } + + private fun navigateToFirstScan() { + _uiState.value = OnboardingTwinUM.ScanCard( + artworkStep = when (firstCardTwinNumber) { + TwinCardNumber.First -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard + TwinCardNumber.Second -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard + }, + step = OnboardingTwinUM.ScanCard.Step.First, + onScanClick = { createFirstWallet() }, + ) + } + + private fun createFirstWallet() { + modelScope.launch { + setLoading(false) + + val result = tangemSdkManager.createFirstTwinWallet( + cardId = params.scanResponse.card.cardId, + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + firstCardTwinNumber.number, + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + if (params.mode == Mode.CreateWallet) { + cardRepository.startCardActivation(result.data.cardId) + } + + // remove wallet only after first step of retwin + if (params.mode == Mode.RecreateWallet) { + userWalletsListManager.delete( + listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()), + ) + } + + analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully()) + + update { + it.copy( + isLoading = false, + artworkStep = it.artworkStep.next(), + step = OnboardingTwinUM.ScanCard.Step.Second, + onScanClick = { + createSecondWallet(firstPublicKey = result.data.wallet.publicKey.toHexString()) + }, + ) + } + + innerNavigationState.update { + it.copy(stackSize = 2) + } + } + } + } + } + + private fun createSecondWallet(firstPublicKey: String) { + setLoading(true) + + modelScope.launch { + val secondCardNumber = firstCardTwinNumber.pairNumber().number + val result = tangemSdkManager.createSecondTwinWallet( + firstPublicKey = firstPublicKey, + firstCardId = params.scanResponse.card.cardId, + issuerKeys = getIssuerKeys(), + preparingMessage = Message( + tangemSdkManager.getString(R.string.twins_recreate_title_preparing), + ), + creatingWalletMessage = Message( + tangemSdkManager.getString(R.string.twins_recreate_title_creating_wallet), + ), + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + secondCardNumber, + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + if (params.mode == Mode.CreateWallet) { + cardRepository.startCardActivation(result.data.cardId) + } + + update { + it.copy( + isLoading = false, + artworkStep = it.artworkStep.next(), + step = OnboardingTwinUM.ScanCard.Step.Third, + onScanClick = { + createThirdWallet( + secondCardPublicKey = result.data.wallet.publicKey, + ) + }, + ) + } + + innerNavigationState.update { + it.copy(stackSize = 3) + } + } + } + } + } + + private fun createThirdWallet(secondCardPublicKey: ByteArray) { + modelScope.launch { + val result = tangemSdkManager.finalizeTwin( + secondCardPublicKey = secondCardPublicKey, + issuerKeyPair = getIssuerKeys(), + cardId = params.scanResponse.card.cardId, + initialMessage = Message( + tangemSdkManager.getString( + R.string.twins_recreate_title_format, + firstCardTwinNumber.number, + ), + ), + ) + + when (result) { + is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) + is CompletionResult.Success -> { + scanComplete(result.data) + } + } + } + } + + private fun scanComplete(scanResponse: ScanResponse) { + analyticsEventHandler.send(OnboardingEvent.Twins.SetupFinished) + + when (params.mode) { + Mode.WelcomeOnly -> saveWalletAndDone() + Mode.RecreateWallet -> params.modelCallbacks.onDone() + Mode.CreateWallet -> { + modelScope.launch { + setLoading(true) + + val userWallet = userWalletBuilderFactory.create(scanResponse).build() ?: run { + Timber.e("User wallet not created") + setLoading(false) + return@launch + } + + userWalletsListManager.save(userWallet, canOverride = true) + + fetchCurrencyStatusUseCase.invoke( + userWalletId = userWallet.walletId, + refresh = true, + ).onLeft { + Timber.e("Unable to fetch currency status: $it") + setLoading(false) + } + + val cryptoCurrencyStatus = + getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId).firstOrNull()?.getOrNull() + ?: run { + setLoading(false) + Timber.e("Unable to get currency status") + return@launch + } + + launch { + getPrimaryCurrencyStatusUpdatesUseCase.invoke(userWallet.walletId) + .collect { + it.onRight { status -> + applyCryptoCurrencyStatusToState(status) + } + } + } + + _uiState.value = OnboardingTwinUM.TopUp( + onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) }, + onRefreshClick = { onRefreshBalanceClick(userWallet) }, + onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) }, + isLoading = true, + ) + + innerNavigationState.update { + it.copy(stackSize = 4) + } + }.saveIn(cryptoCurrencyStatusJobHolder) + } + } + } + + private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) { + val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return + if (amount > BigDecimal.ZERO) { + params.modelCallbacks.onDone() + } else { + update { + it.copy( + balance = BigDecimal.ZERO.format { crypto(status.currency) }, + onBuyCryptoClick = { onBuyCryptoClick(status) }, + onShowAddressClick = { onShowAddressClick(status) }, + isLoading = false, + ) + } + } + } + + private fun onBuyCryptoClick(status: CryptoCurrencyStatus) { + modelScope.launch { + getLegacyTopUpUrlUseCase(status).onRight { + urlOpener.openUrl(it) + } + } + } + + private fun onShowAddressClick(status: CryptoCurrencyStatus) { + val currency = status.currency + val networkAddress = status.value.networkAddress ?: return + + update { + it.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = { + update { + it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty) + } + }, + content = TokenReceiveBottomSheetConfig( + name = currency.name, + symbol = currency.symbol, + network = currency.network.name, + addresses = networkAddress.availableAddresses + .mapToAddressModels(currency) + .toImmutableList(), + showMemoDisclaimer = + currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, + onCopyClick = {}, + onShareClick = {}, + ), + ), + ) + } + } + + private fun onRefreshBalanceClick(userWallet: UserWallet) { + update { + it.copy(isLoading = true) + } + modelScope.launch { + fetchCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + refresh = true, + ) + } + } + + private fun saveWalletAndDone() { + setLoading(true) + + modelScope.launch { + val userWallet = userWalletBuilderFactory.create(params.scanResponse).build() ?: run { + Timber.e("User wallet not created") + setLoading(false) + return@launch + } + + userWalletsListManager.save(userWallet, canOverride = true) + params.modelCallbacks.onDone() + } + } + + private fun showCardVerificationFailedDialog(error: TangemError) { + if (error !is TangemSdkError.CardVerificationFailed) return + + analyticsEventHandler.send(OnboardingEvent.OfflineAttestationFailed(AnalyticsParam.ScreensSources.Onboarding)) + + val resource = error.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + + uiMessageSender.send( + cardVerificationFailedDialog( + errorDescription = resourceReference(id = resId, resArgs.toWrappedList()), + onRequestSupport = { + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.CardAttestationFailed) + } + }, + ), + ) + } + + private suspend fun getIssuerKeys(): KeyPair { + val issuer = issuersConfigStorage.getConfig() + .first { it.publicKey == params.scanResponse.card.issuer.publicKey.toHexString() } + + return KeyPair( + publicKey = issuer.publicKey.hexToBytes(), + privateKey = issuer.privateKey.hexToBytes(), + ) + } + + private inline fun update(updateBlock: (T) -> OnboardingTwinUM) { + _uiState.update { st -> + (st as? T)?.let { updateBlock(it) } ?: st + } + } + + private fun setLoading(flag: Boolean) { + _uiState.update { it.copySealed(isLoading = flag) } + } + + private fun TwinWalletArtworkUM.Leapfrog.Step.next(): TwinWalletArtworkUM.Leapfrog.Step = when (this) { + TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard + TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt new file mode 100644 index 0000000000..c4535a2f95 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/OnboardingTwin.kt @@ -0,0 +1,326 @@ +package com.tangem.features.onboarding.v2.twin.impl.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.IconToggleButton +import androidx.compose.runtime.* +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.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM + +@Suppress("UnusedPrivateMember", "MagicNumber") +@Composable +internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding(), + ) { + TwinWalletArtworks( + modifier = Modifier + .padding(top = 64.dp) + .padding(horizontal = 24.dp) + .weight(.48f) + .fillMaxWidth(), + state = state.artwork, + balance = (state as? OnboardingTwinUM.TopUp)?.balance ?: "", + isRefreshing = state.isLoading, + onRefreshBalanceClick = { + (state as? OnboardingTwinUM.TopUp)?.onRefreshClick() + }, + ) + + AnimatedContent( + modifier = Modifier.weight(.52f), + targetState = state, + transitionSpec = TangemAnimations.AnimatedContent + .slide { initial, target -> target.stepIndex > initial.stepIndex }, + contentKey = { it::class }, + ) { st -> + when (st) { + is OnboardingTwinUM.ResetWarning -> ResetWarning(st) + is OnboardingTwinUM.ScanCard -> ScanCard(st) + is OnboardingTwinUM.TopUp -> TopUp(st) + is OnboardingTwinUM.Welcome -> Welcome(st) + } + } + } + + if (state is OnboardingTwinUM.TopUp) { + TokenReceiveBottomSheet(config = state.bottomSheetConfig) + } +} + +@Suppress("LongMethod") +@Composable +private fun ResetWarning(state: OnboardingTwinUM.ResetWarning, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.common_warning), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + SpacerH16() + + Text( + text = stringResourceSafe(R.string.twins_recreate_warning), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + + SpacerH16() + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { state.onAcceptClick(!state.acceptToggle) }, + ), + ) { + IconToggleButton(checked = state.acceptToggle, onCheckedChange = state.onAcceptClick) { + AnimatedContent(targetState = state.acceptToggle, label = "Update checked state") { checked -> + Icon( + painter = painterResource( + if (checked) { + R.drawable.ic_accepted_20 + } else { + R.drawable.ic_unticked_20 + }, + ), + contentDescription = null, + tint = if (checked) { + TangemTheme.colors.control.checked + } else { + TangemTheme.colors.icon.secondary + }, + ) + } + } + + Text( + text = stringResourceSafe(R.string.common_understand), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + } + + PrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_continue), + enabled = state.acceptToggle, + onClick = state.onContinueClick, + ) + } +} + +@Composable +private fun TopUp(state: OnboardingTwinUM.TopUp, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.onboarding_topup_title), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + SpacerH16() + + Text( + text = stringResourceSafe(R.string.onboarding_top_up_body), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + + PrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto), + onClick = state.onBuyCryptoClick, + ) + + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address), + onClick = state.onShowAddressClick, + ) + } +} + +@Composable +private fun ScanCard(state: OnboardingTwinUM.ScanCard, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.twins_recreate_title_format, state.cardNumber), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + SpacerH16() + + Text( + text = stringResourceSafe(R.string.onboarding_twins_interrupt_warning), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + + PrimaryButtonIconEnd( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.onboarding_button_backup_card_format, state.cardNumber), + showProgress = state.isLoading, + iconResId = R.drawable.ic_tangem_24, + onClick = state.onScanClick, + ) + } +} + +@Composable +private fun Welcome(state: OnboardingTwinUM.Welcome, modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column( + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, bottom = 16.dp) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = stringResourceSafe(R.string.twins_onboarding_subtitle), + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + style = TangemTheme.typography.h2, + ) + + SpacerH16() + + Text( + text = stringResourceSafe(R.string.twins_onboarding_description_format, state.pairCardNumber), + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.body1, + ) + } + + PrimaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResourceSafe(R.string.common_continue), + onClick = state.onContinueClick, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewTopUp() { + TangemThemePreview { + OnboardingTwin(OnboardingTwinUM.TopUp()) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewWelcome() { + TangemThemePreview { + OnboardingTwin(OnboardingTwinUM.Welcome()) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewScanCard() { + TangemThemePreview { + OnboardingTwin(OnboardingTwinUM.ScanCard()) + } +} + +@Preview(showBackground = true) +@Composable +private fun PreviewResetWarning() { + TangemThemePreview { + var toggle by remember { mutableStateOf(false) } + + OnboardingTwin( + OnboardingTwinUM.ResetWarning( + acceptToggle = toggle, + onAcceptClick = { toggle = it }, + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt new file mode 100644 index 0000000000..5fba3214f8 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/TwinWalletArtwork.kt @@ -0,0 +1,379 @@ +package com.tangem.features.onboarding.v2.twin.impl.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Transition +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.zIndex +import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.wallets.models.Artwork +import com.tangem.features.onboarding.v2.common.ui.RefreshButton +import com.tangem.features.onboarding.v2.common.ui.WalletCard +import com.tangem.features.onboarding.v2.impl.R +import kotlinx.coroutines.delay +import java.util.concurrent.TimeUnit + +@Immutable +internal sealed class TwinWalletArtworkUM { + data object Spread : TwinWalletArtworkUM() + + data class Leapfrog( + val step: Step, + ) : TwinWalletArtworkUM() { + enum class Step { + FirstCard, SecondCard + } + } + + data object TopUp : TwinWalletArtworkUM() +} + +private data class CardsTransitionState( + val walletCard1: WalletCardTransitionState, + val walletCard2: WalletCardTransitionState, +) + +private data class WalletCardTransitionState( + val alpha: Float = 1f, + val xTranslation: Float = 0f, + val yTranslation: Float = 0f, + val xScale: Float = 1f, + val yScale: Float = 1f, + val rotation: Float = 0f, + val zIndex: Float = 0f, +) + +@Suppress("LongMethod") +@Composable +internal fun TwinWalletArtworks( + state: TwinWalletArtworkUM, + balance: String, + isRefreshing: Boolean, + onRefreshBalanceClick: () -> Unit, + modifier: Modifier = Modifier, +) { + BoxWithConstraints( + modifier + .heightIn(min = 180.dp) + .widthIn(max = 450.dp), + ) { + val animationWidth = maxWidth + val density = LocalDensity.current.density + + var animationState by remember { + mutableStateOf( + state.toTransitionSetState( + maxWidthDp = animationWidth.value, + density = density, + maxHeightDp = maxHeight.value, + ).first(), + ) + } + + val transition1 = updateTransition(animationState.walletCard1, label = "transition") + val transition2 = updateTransition(animationState.walletCard2, label = "transition") + + LaunchedEffect(state) { + val animationSetState = state.toTransitionSetState( + maxWidthDp = animationWidth.value, + density = density, + maxHeightDp = maxHeight.value, + ) + animationSetState.fastForEach { + animationState = it + val maxTime = maxOf( + transition1.totalDurationNanos, + transition2.totalDurationNanos, + ) + delay(TimeUnit.NANOSECONDS.toMillis(maxTime)) + } + } + + AnimatedVisibility( + visible = state == TwinWalletArtworkUM.TopUp, + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier + .padding(vertical = 24.dp, horizontal = 16.dp) + .fillMaxSize() + .background( + TangemTheme.colors.button.secondary, + shape = TangemTheme.shapes.roundedCornersMedium, + ), + ) + } + + AnimatedTwinCards( + transition1 = transition1, + transition2 = transition2, + modifier = Modifier + .widthIn(max = 450.dp) + .matchParentSize(), + ) + + AnimatedVisibility( + modifier = Modifier.align(Alignment.Center), + visible = state == TwinWalletArtworkUM.TopUp, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerHMax() + Text( + text = stringResourceSafe(R.string.common_balance_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + SpacerH8() + Text( + text = balance, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + SpacerHMax() + } + } + + AnimatedVisibility( + modifier = Modifier.align(Alignment.BottomCenter), + visible = state == TwinWalletArtworkUM.TopUp, + enter = fadeIn(), + exit = fadeOut(), + ) { + RefreshButton( + isRefreshing = isRefreshing, + onRefreshBalanceClick = onRefreshBalanceClick, + ) + } + } +} + +@Composable +private fun AnimatedTwinCards( + transition1: Transition, + transition2: Transition, + modifier: Modifier = Modifier, +) { + val alpha1 by transition1.animateFloat(label = "alpha1") { it.alpha } + val translationX1 by transition1.animateFloat(label = "translationX1") { it.xTranslation } + val translationY1 by transition1.animateFloat(label = "translationY1") { it.yTranslation } + val rotation1 by transition1.animateFloat(label = "rotation1") { it.rotation } + val scaleX1 by transition1.animateFloat(label = "scaleX1") { it.xScale } + val scaleY1 by transition1.animateFloat(label = "scaleY1") { it.yScale } + val zIndex1 = transition1.currentState.zIndex + + val alpha2 by transition2.animateFloat(label = "alpha2") { it.alpha } + val translationX2 by transition2.animateFloat(label = "translationX2") { it.xTranslation } + val translationY2 by transition2.animateFloat(label = "translationY2") { it.yTranslation } + val rotation2 by transition2.animateFloat(label = "rotation2") { it.rotation } + val scaleX2 by transition2.animateFloat(label = "scaleX2") { it.xScale } + val scaleY2 by transition2.animateFloat(label = "scaleY2") { it.yScale } + val zIndex2 = transition2.currentState.zIndex + + Box(modifier) { + WalletCard( + modifier = Modifier + .zIndex(zIndex2) + .matchParentSize() + .graphicsLayer { + translationX = translationX2 + translationY = translationY2 + } + .graphicsLayer { + alpha = alpha2 + rotationZ = rotation2 + scaleX = scaleX2 + scaleY = scaleY2 + }, + url = Artwork.TWIN_CARD_2_URL, + ) + + WalletCard( + modifier = Modifier + .zIndex(zIndex1) + .matchParentSize() + .graphicsLayer { + scaleX = scaleX1 + scaleY = scaleY1 + } + .graphicsLayer { + alpha = alpha1 + translationX = translationX1 + translationY = translationY1 + rotationZ = rotation1 + }, + url = Artwork.TWIN_CARD_1_URL, + ) + } +} + +@Suppress("MagicNumber", "LongMethod", "UnnecessaryParentheses") +private fun TwinWalletArtworkUM.toTransitionSetState( + maxWidthDp: Float, + maxHeightDp: Float, + density: Float, +): List = when (this) { + TwinWalletArtworkUM.Spread -> { + val scale = 0.8f + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState( + xTranslation = maxWidthDp * -0.15f * density, + yTranslation = -65 * density, + rotation = -5f, + xScale = scale, + yScale = scale, + zIndex = 2f, + ), + walletCard2 = WalletCardTransitionState( + xTranslation = maxWidthDp * 0.15f * density, + yTranslation = 65 * density, + rotation = -5f, + xScale = scale, + yScale = scale, + zIndex = 1f, + ), + ), + ) + } + is TwinWalletArtworkUM.Leapfrog -> { + val translationY = -(maxHeightDp / 2) * density + val secondCardPlace = WalletCardTransitionState().copy( + xTranslation = 0f, + yTranslation = ((maxHeightDp * (1 - 0.83f)) / 4 + 12) * density, + rotation = 0f, + xScale = 0.83f, + yScale = 0.83f, + ) + + if (step == TwinWalletArtworkUM.Leapfrog.Step.FirstCard) { + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState( + yTranslation = translationY, + zIndex = 2f, + ), + walletCard2 = WalletCardTransitionState(), + ), + CardsTransitionState( + walletCard1 = WalletCardTransitionState( + zIndex = 2f, + ), + walletCard2 = secondCardPlace, + ), + ) + } else { + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState(), + walletCard2 = WalletCardTransitionState( + yTranslation = translationY, + zIndex = 2f, + ), + ), + CardsTransitionState( + walletCard1 = secondCardPlace, + walletCard2 = WalletCardTransitionState( + zIndex = 2f, + ), + ), + ) + } + } + TwinWalletArtworkUM.TopUp -> { + val scale = 0.4f + val yTranslation = -maxHeightDp * density - 24 * density + listOf( + CardsTransitionState( + walletCard1 = WalletCardTransitionState( + yTranslation = yTranslation, + xScale = scale, + yScale = scale, + zIndex = 2f, + ), + walletCard2 = WalletCardTransitionState( + yTranslation = yTranslation * 0.35f, + xScale = scale * 0.8f, + yScale = scale * 0.8f, + zIndex = 1f, + ), + ), + ) + } +} + +@Preview(showBackground = true, widthDp = 360, heightDp = 640) +@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) } + + TwinWalletArtworks( + state = state, + modifier = Modifier + .padding(top = 250.dp) + .fillMaxWidth(), + balance = "1 USD", + isRefreshing = false, + onRefreshBalanceClick = {}, + ) + + var index by remember { mutableIntStateOf(0) } + + Button( + modifier = Modifier + .zIndex(5f) + .align(Alignment.TopStart), + onClick = { + val list = listOf( + TwinWalletArtworkUM.Spread, + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard), + TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard), + TwinWalletArtworkUM.TopUp, + ) + + state = list[index % list.size] + index++ + }, + ) { + Text("Animate $state") + } + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt new file mode 100644 index 0000000000..89ffc6efc6 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/ui/state/OnboardingTwinUM.kt @@ -0,0 +1,71 @@ +package com.tangem.features.onboarding.v2.twin.impl.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM + +@Immutable +internal sealed class OnboardingTwinUM { + + abstract val stepIndex: Int + abstract val isLoading: Boolean + abstract val artwork: TwinWalletArtworkUM + + data class Welcome( + override val isLoading: Boolean = false, + val pairCardNumber: Int = 2, + val onContinueClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex = 0 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread + } + + data class ResetWarning( + val acceptToggle: Boolean = false, + val onAcceptClick: (Boolean) -> Unit = {}, + val onContinueClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex: Int = 0 + override val isLoading: Boolean = false + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread + } + + data class ScanCard( + override val isLoading: Boolean = false, + val artworkStep: TwinWalletArtworkUM.Leapfrog.Step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard, + val step: Step = Step.First, + val onScanClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + + enum class Step { + First, Second, Third + } + + val cardNumber: Int = when (artworkStep) { + TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> 1 + TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> 2 + } + + override val stepIndex: Int = 1 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep) + } + + data class TopUp( + override val isLoading: Boolean = false, + val balance: String = "", + val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty, + val onBuyCryptoClick: () -> Unit = {}, + val onShowAddressClick: () -> Unit = {}, + val onRefreshClick: () -> Unit = {}, + ) : OnboardingTwinUM() { + override val stepIndex: Int = 2 + override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp + } + + fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) { + is Welcome -> copy(isLoading = isLoading) + is ResetWarning -> copy() + is ScanCard -> copy(isLoading = isLoading) + is TopUp -> copy(isLoading = isLoading) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt index b36dcef18f..108d8134bc 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/DefaultOnboardingVisaComponent.kt @@ -161,7 +161,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaOtherWalletComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.push(OnboardingVisaRoute.PinCode(activationOrderInfo = it)) + model.stackNavigation.pushNew(OnboardingVisaRoute.PinCode(activationOrderInfo = it)) }, ), ) @@ -174,7 +174,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaPinCodeComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.push( + model.stackNavigation.pushNew( OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode), ) }, @@ -190,7 +190,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaApproveComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.push( + model.stackNavigation.pushNew( OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.Approve), ) }, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt index 1557683bfe..090ce03d84 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/ui/OnboardingVisaAccessCode.kt @@ -23,6 +23,7 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemAnimations import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -43,7 +44,7 @@ internal fun OnboardingVisaAccessCode(state: OnboardingVisaAccessCodeUM, modifie .weight(1f), targetState = state.step, transitionSpec = TangemAnimations.AnimatedContent - .slide { initial, target -> initial.ordinal > target.ordinal }, + .slide { initial, target -> target.ordinal > initial.ordinal }, ) { step -> Content( state = state, @@ -87,9 +88,9 @@ private fun Content( modifier = Modifier .padding(horizontal = 32.dp), text = if (reEnterAccessCodeState) { - "Re-enter your access code" // TODO + stringResourceSafe(R.string.onboarding_access_code_repeat_code_title) } else { - "Create access code" // TODO + stringResourceSafe(R.string.onboarding_access_code_intro_title) }, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, @@ -98,11 +99,10 @@ private fun Content( SpacerH16() - // TODO Text( modifier = Modifier .padding(horizontal = 32.dp), - text = "The access code will be used manage your payment account and protect it from unauthorized access", + text = stringResourceSafe(R.string.visa_onboarding_access_code_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt index f93ebd7cd3..106973d0da 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/approve/ui/OnboardingVisaApprove.kt @@ -14,7 +14,8 @@ import androidx.compose.ui.unit.dp import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -43,7 +44,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod SpacerH(86.dp) Text( - text = "Prepare Tangem Wallet", + text = stringResourceSafe(R.string.visa_onboarding_tangem_approve_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -52,7 +53,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod SpacerH(12.dp) Text( - text = "Prepare the Tangem card\nand tap to approve", + text = stringResourceSafe(R.string.visa_onboarding_tangem_approve_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -65,7 +66,7 @@ internal fun OnboardingVisaApprove(state: OnboardingVisaApproveUM, modifier: Mod .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), primaryButton = NavigationButton( - textReference = TextReference.Str("Approve"), + textReference = resourceReference(R.string.common_approve), iconRes = R.drawable.ic_tangem_24, isIconVisible = true, showProgress = state.approveButtonLoading, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt index 381cfc11aa..3d48e57718 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/model/OnboardingVisaChooseWalletModel.kt @@ -3,7 +3,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.model import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.OnboardingVisaChooseWalletComponent.Params.Event import com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui.state.OnboardingVisaChooseWalletUM @@ -32,7 +32,7 @@ internal class OnboardingVisaChooseWalletModel @Inject constructor( val selectedOption = SelectableChainRowUM( event = Event.TangemWallet, icon = R.drawable.ic_tangem_24, - text = TextReference.Str("Tangem Wallet"), + text = resourceReference(R.string.common_tangem_wallet), ) return OnboardingVisaChooseWalletUM( @@ -41,7 +41,7 @@ internal class OnboardingVisaChooseWalletModel @Inject constructor( SelectableChainRowUM( event = Event.OtherWallet, icon = R.drawable.ic_wallet_filled_24, - text = TextReference.Str("Other Wallet"), + text = resourceReference(R.string.visa_onboarding_other_wallet), ), ), selectedOption = selectedOption, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt index b44e28c2c6..5cd9a11f10 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/choosewallet/ui/OnboardingVisaChooseWallet.kt @@ -21,6 +21,8 @@ import com.tangem.core.ui.components.rows.RowContentContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.outsetBorder import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -45,10 +47,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod ) { Notification( config = NotificationConfig( - subtitle = TextReference.Str( - "Please choose the wallet you started the registration" + - " process with to sign the transaction for creating your account on the Blockchain.", - ), + subtitle = resourceReference(R.string.visa_onboarding_approve_wallet_selector_notification_message), iconResId = R.drawable.ic_alert_circle_24, ), iconTint = TangemTheme.colors.icon.accent, @@ -62,7 +61,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod start = 12.dp, end = 12.dp, ), - text = "Choose wallet", + text = stringResourceSafe(R.string.visa_onboarding_wallet_list_header), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -79,7 +78,7 @@ internal fun OnboardingVisaChooseWallet(state: OnboardingVisaChooseWalletUM, mod .imePadding() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Continue", + text = stringResourceSafe(R.string.common_continue), onClick = state.onContinueClick, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt index 3d79bce2c1..57f424606e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/inprogress/model/OnboardingVisaInProgressModel.kt @@ -14,7 +14,6 @@ import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute @@ -35,7 +34,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( private val visaAuthRepository: VisaAuthRepository, private val visaAuthTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaOTPStorage, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, ) : Model() { @@ -110,9 +109,8 @@ internal class OnboardingVisaInProgressModel @Inject constructor( val newActivationStatus = VisaCardActivationStatus.Activated(visaAuthTokens = authTokens) requireNotNull( - value = UserWalletBuilder( - scanResponse.copy(visaCardActivationStatus = newActivationStatus), - generateWalletNameUseCase, + value = userWalletBuilderFactory.create( + scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus), ).build(), lazyMessage = { "User wallet not created" }, ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt index 53c043fbea..875e5a4b49 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/model/OnboardingVisaOtherWalletModel.kt @@ -68,10 +68,10 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( } private fun onShareClicked() { - shareManager.shareText("https://tangem.com/") + shareManager.shareText("https://tangem.com/") // TODO } private fun onOpenInBrowserClicked() { - urlOpener.openUrl("https://tangem.com/") + urlOpener.openUrl("https://tangem.com/") // TODO } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt index 4b0a8c2f3f..68b39dbc61 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/otherwallet/ui/OnboardingVisaOtherWallet.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -44,7 +45,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif SpacerH12() Text( - text = "Go to Website", + text = stringResourceSafe(R.string.visa_onboarding_wallet_connect_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -53,9 +54,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif SpacerH12() Text( - text = "You will be able to complete your connection \n" + - "on the third-party web-site \n" + - "and back to the Tangem app", + text = stringResourceSafe(R.string.visa_onboarding_wallet_connect_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -68,7 +67,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif modifier = Modifier .padding(horizontal = 16.dp) .fillMaxWidth(), - text = "Open in Browser", + text = stringResourceSafe(R.string.common_open_in_browser), onClick = state.onOpenInBrowserClick, ) @@ -78,7 +77,7 @@ internal fun OnboardingVisaOtherWallet(state: OnboardingVisaOtherWalletUM, modif modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Share Link", + text = stringResourceSafe(R.string.common_share_link), onClick = state.onOpenInBrowserClick, ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt index 0b02574d50..bcb96ade2f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/OnboardingVisaPinCodeModel.kt @@ -5,6 +5,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.resourceReference import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.SetVisaPinCodeUseCase @@ -13,7 +14,7 @@ import com.tangem.domain.visa.model.VisaCardId import com.tangem.domain.wallets.builder.UserWalletBuilder import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.visa.impl.child.pincode.OnboardingVisaPinCodeComponent import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -31,7 +32,7 @@ import javax.inject.Inject internal class OnboardingVisaPinCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, private val userWalletsListManager: UserWalletsListManager, private val authTokenStorage: VisaAuthTokenStorage, private val otpStorage: VisaAuthTokenStorage, @@ -57,24 +58,32 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( } private fun onPinCodeChange(pin: String) { - if (pin.all { it.isDigit() }) { + if (PinCodeValidation.validateAllDigits(pin)) { + val isError = PinCodeValidation.validateLength(pin) && PinCodeValidation.validate(pin).not() + _uiState.update { it.copy( pinCode = pin, - submitButtonEnabled = checkPinCode(pin), + submitButtonEnabled = PinCodeValidation.validate(pin), + error = if (isError) { + resourceReference(R.string.visa_onboarding_pin_validation_error_message) + } else { + null + }, ) } } } private fun onSubmitClick() { - if (checkPinCode(_uiState.value.pinCode).not()) return + val pinCode = _uiState.value.pinCode + if (PinCodeValidation.validate(pinCode).not()) return modelScope.launch { loading(true) setVisaPinCodeUseCase( - pinCode = _uiState.value.pinCode, + pinCode = pinCode, visaCardId = visaCardId, activationOrderId = params.activationOrderInfo.orderId, ).onLeft { @@ -88,10 +97,6 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( } } - private fun checkPinCode(pin: String): Boolean { - return pin.length == PIN_CODE_LENGTH - } - private fun loading(state: Boolean) { _uiState.update { it.copy(submitButtonLoading = state) } } @@ -111,15 +116,10 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( ) requireNotNull( - value = UserWalletBuilder( - scanResponse.copy(visaCardActivationStatus = newActivationStatus), - generateWalletNameUseCase, + value = userWalletBuilderFactory.create( + scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus), ).build(), lazyMessage = { "User wallet not created" }, ) } - - private companion object { - const val PIN_CODE_LENGTH = 4 - } } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt new file mode 100644 index 0000000000..99c721134d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt @@ -0,0 +1,31 @@ +package com.tangem.features.onboarding.v2.visa.impl.child.pincode.model + +internal object PinCodeValidation { + + private const val PIN_CODE_LENGTH = 4 + + fun validate(pinCode: String): Boolean { + return sequenceOf( + ::validateAllDigits, + ::validateLength, + ::validateNoRepeatedDigits, + ::validateNoConsecutiveDigits, + ).all { it(pinCode) } + } + + fun validateAllDigits(pinCode: String): Boolean { + return pinCode.all { it.isDigit() } + } + + fun validateLength(pinCode: String): Boolean { + return pinCode.length == PIN_CODE_LENGTH + } + + private fun validateNoRepeatedDigits(pinCode: String): Boolean { + return pinCode.toSet().size == PIN_CODE_LENGTH + } + + private fun validateNoConsecutiveDigits(pinCode: String): Boolean { + return pinCode.windowed(2).all { it[0] + 1 == it[1] }.not() + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt index c65e7afaeb..cb2d5993c8 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/OnboardingVisaPinCode.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -31,9 +32,14 @@ import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state.OnboardingVisaPinCodeUM import kotlinx.coroutines.delay @@ -53,7 +59,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = "Create\nPIN Code", + text = stringResourceSafe(R.string.visa_onboarding_pin_code_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, @@ -62,7 +68,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod SpacerH16() Text( - text = "Set up a 4-digit code.\nIt will be used for payments.", + text = stringResourceSafe(R.string.visa_onboarding_pin_code_description), style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, textAlign = TextAlign.Center, @@ -79,7 +85,7 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), primaryButton = NavigationButton( - textReference = TextReference.Str("Submit"), + textReference = resourceReference(R.string.common_submit), onClick = state.onSubmitClick, showProgress = state.submitButtonLoading, isEnabled = state.submitButtonEnabled, @@ -91,13 +97,29 @@ internal fun OnboardingVisaPinCode(state: OnboardingVisaPinCodeUM, modifier: Mod @Composable private fun PinCodeSection(state: OnboardingVisaPinCodeUM, modifier: Modifier = Modifier) { val focusRequester = remember { FocusRequester() } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + PinCode( + modifier = modifier, + value = state.pinCode, + onValueChange = state.onPinCodeChange, + focusRequester = focusRequester, + ) - PinCode( - modifier = modifier, - value = state.pinCode, - onValueChange = state.onPinCodeChange, - focusRequester = focusRequester, - ) + AnimatedVisibility( + visible = state.error != null, + ) { + val error = remember(this) { requireNotNull(state.error) } + Column { + SpacerH4() + Text( + text = error.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.warning, + textAlign = TextAlign.Center, + ) + } + } + } LaunchedEffect(Unit) { delay(timeMillis = 300) @@ -134,9 +156,11 @@ private fun PinCode( keyboardType = KeyboardType.Number, imeAction = ImeAction.Done, ), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - },), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + }, + ), modifier = Modifier .alpha(alpha = 0.01f) .focusRequester(focusRequester), @@ -194,6 +218,7 @@ private fun Preview() { OnboardingVisaPinCode( state = OnboardingVisaPinCodeUM( pinCode = "1234", + error = stringReference("PIN Code error"), ), ) } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt index 9b17f93c5a..adc4df53d1 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/ui/state/OnboardingVisaPinCodeUM.kt @@ -1,7 +1,10 @@ package com.tangem.features.onboarding.v2.visa.impl.child.pincode.ui.state +import com.tangem.core.ui.extensions.TextReference + internal data class OnboardingVisaPinCodeUM( val pinCode: String = "", + val error: TextReference? = null, val onPinCodeChange: (String) -> Unit = {}, val submitButtonLoading: Boolean = false, val submitButtonEnabled: Boolean = true, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt index 0a209235b0..9a01f706d6 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/OnboardingVisaWelcome.kt @@ -16,6 +16,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.onboarding.v2.impl.R @@ -47,8 +48,10 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod Text( text = when (state.mode) { - OnboardingVisaWelcomeUM.Mode.Hello -> "Hello, ${state.userName}!" - OnboardingVisaWelcomeUM.Mode.WelcomeBack -> "Welcome back, ${state.userName}!" + OnboardingVisaWelcomeUM.Mode.Hello -> + stringResourceSafe(R.string.visa_onboarding_welcome_title) + OnboardingVisaWelcomeUM.Mode.WelcomeBack -> + stringResourceSafe(R.string.visa_onboarding_welcome_back_title) }, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, @@ -58,8 +61,10 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod Text( text = when (state.mode) { - OnboardingVisaWelcomeUM.Mode.Hello -> "bla bla bla //TODO" - OnboardingVisaWelcomeUM.Mode.WelcomeBack -> "bla bla bla? //TODO" + OnboardingVisaWelcomeUM.Mode.Hello -> + stringResourceSafe(R.string.visa_onboarding_welcome_description) + OnboardingVisaWelcomeUM.Mode.WelcomeBack -> + stringResourceSafe(R.string.visa_onboarding_welcome_back_description) }, style = TangemTheme.typography.body1, color = TangemTheme.colors.text.secondary, @@ -72,7 +77,7 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Start Activation", + text = stringResourceSafe(R.string.visa_onboarding_welcome_button_title), onClick = state.onContinueClick, ) } @@ -81,7 +86,7 @@ internal fun OnboardingVisaWelcome(state: OnboardingVisaWelcomeUM, modifier: Mod modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - text = "Continue Activation", + text = stringResourceSafe(R.string.visa_onboarding_welcome_back_button_title), showProgress = state.continueButtonLoading, iconResId = R.drawable.ic_tangem_24, onClick = state.onContinueClick, diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt index d1f975f9b3..069b63d30f 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/ui/state/OnboardingVisaWelcomeUM.kt @@ -2,7 +2,6 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome.ui.state internal data class OnboardingVisaWelcomeUM( val mode: Mode = Mode.Hello, - val userName: String = "", val continueButtonLoading: Boolean = false, val onContinueClick: () -> Unit = {}, ) { diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt index 5da3a52e3d..87aaac10b3 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/route/Titles.kt @@ -1,16 +1,22 @@ package com.tangem.features.onboarding.v2.visa.impl.route import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R -// TODO add translations [REDACTED_TASK_KEY] internal fun OnboardingVisaRoute.screenTitle(): TextReference = when (this) { - OnboardingVisaRoute.AccessCode -> TextReference.Str("Access code") - is OnboardingVisaRoute.ChooseWallet -> TextReference.Str("Account activation") - is OnboardingVisaRoute.InProgress -> TextReference.Str("In progress") - is OnboardingVisaRoute.OtherWalletApproveOption -> TextReference.Str("Wallet connection") - is OnboardingVisaRoute.PinCode -> TextReference.Str("PIN code") - is OnboardingVisaRoute.TangemWalletApproveOption -> TextReference.Str("Wallet connection") + OnboardingVisaRoute.AccessCode -> + resourceReference(R.string.visa_onboarding_access_code_navigation_title) + is OnboardingVisaRoute.ChooseWallet -> + resourceReference(R.string.visa_onboarding_account_activation_navigation_title) + is OnboardingVisaRoute.InProgress -> + resourceReference(R.string.common_in_progress) + is OnboardingVisaRoute.OtherWalletApproveOption, + is OnboardingVisaRoute.TangemWalletApproveOption, + -> resourceReference(R.string.visa_onboarding_wallet_connection_navigation_title) + is OnboardingVisaRoute.PinCode -> + resourceReference(R.string.visa_onboarding_pin_code_navigation_title) is OnboardingVisaRoute.Welcome, is OnboardingVisaRoute.WelcomeBack, - -> TextReference.Str("Getting started") + -> resourceReference(R.string.onboarding_getting_started) } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt deleted file mode 100644 index 40a9a8597d..0000000000 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/ui/OnboardingVisa.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.onboarding.v2.visa.impl.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -internal fun OnboardingVisa(modifier: Modifier = Modifier, childContent: @Composable (Modifier) -> Unit) { - childContent(modifier) -} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt index 9202fd9fc8..7d46cb6ad1 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt @@ -3,6 +3,4 @@ package com.tangem.features.onramp interface OnrampFeatureToggles { val isFeatureEnabled: Boolean - - val isHotTokensEnabled: Boolean } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt index 5a0c6f5a15..db7f335efe 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt @@ -8,7 +8,4 @@ internal class DefaultOnrampFeatureToggles( override val isFeatureEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "ONRAMP_ENABLED") - - override val isHotTokensEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "ONRAMP_HOT_TOKENS_ENABLED") } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt index 9888b7dddb..f4fe8672a8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt @@ -6,7 +6,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.onramp.hottokens.HotCryptoComponent import com.tangem.features.onramp.selecttoken.model.OnrampOperationModel import com.tangem.features.onramp.selecttoken.ui.OnrampSelectToken @@ -20,7 +19,6 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, onrampTokenListComponentFactory: OnrampTokenListComponent.Factory, @Assisted private val params: OnrampOperationComponent.Params, - private val onrampFeatureToggles: OnrampFeatureToggles, private val hotCryptoComponentFactory: HotCryptoComponent.Factory, ) : AppComponentContext by appComponentContext, OnrampOperationComponent { @@ -54,7 +52,7 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( } private fun createHotCryptoComponent(): HotCryptoComponent? { - return if (onrampFeatureToggles.isHotTokensEnabled && params is OnrampOperationComponent.Params.Buy) { + return if (params is OnrampOperationComponent.Params.Buy) { hotCryptoComponentFactory.create( context = child(key = "hot_crypto"), params = HotCryptoComponent.Params( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 34f4ff9f39..622c7055ba 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -19,6 +19,7 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.onramp.component.OnrampSuccessComponent import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.success.entity.OnrampSuccessClickIntents @@ -42,6 +43,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( private val getOnrampStatusUseCase: GetOnrampStatusUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val messageSender: UiMessageSender, private val router: Router, @@ -79,17 +81,21 @@ internal class OnrampSuccessComponentModel @Inject constructor( } private suspend fun loadTransactionStatus(transaction: OnrampTransaction) { - val multiCryptoCurrency = getCryptoCurrencyUseCase( - transaction.userWalletId, - transaction.toCurrencyId, - ).getOrNull() - - val cryptoCurrency = multiCryptoCurrency - ?: getCryptoCurrencyUseCase.invoke(transaction.userWalletId).getOrElse { - Timber.e("Crypto currency not found") - showErrorAlert(OnrampError.DomainError(null)) - return - } + val userWallet = getUserWalletUseCase(transaction.userWalletId).getOrNull() + if (userWallet == null) { + Timber.e("UserWallet found") + // this case should never happened + showErrorAlert(OnrampError.DomainError("UserWallet not found")) + return + } + val cryptoCurrency = getCryptoCurrencyUseCase( + userWallet = userWallet, + cryptoCurrencyId = transaction.toCurrencyId, + ).getOrElse { + Timber.e("Crypto currency not found") + showErrorAlert(OnrampError.DomainError(null)) + return + } getOnrampStatusUseCase(txId = transaction.txId) .fold( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index 679abe3866..c45da297d2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -6,11 +6,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.key 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.unit.dp @@ -18,6 +16,7 @@ import androidx.compose.ui.util.fastForEachIndexed import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.tokenlist.TokenListItem @@ -74,16 +73,7 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { private fun SearchBar(searchBarUM: SearchBarUM) { SearchBar( state = searchBarUM, - colors = TextFieldDefaults.colors().copy( - focusedContainerColor = TangemTheme.colors.field.focused, - unfocusedContainerColor = TangemTheme.colors.field.focused, - focusedTextColor = TangemTheme.colors.text.primary1, - unfocusedTextColor = TangemTheme.colors.text.primary1, - cursorColor = TangemTheme.colors.icon.primary1, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ), + colors = TangemSearchBarDefaults.secondaryTextFieldColors, ) } diff --git a/features/send-v2/api/.gitignore b/features/send-v2/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/send-v2/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/send-v2/api/build.gradle.kts b/features/send-v2/api/build.gradle.kts new file mode 100644 index 0000000000..7bf806b77c --- /dev/null +++ b/features/send-v2/api/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.send.v2.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain models */ + implementation(projects.domain.wallets.models) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt new file mode 100644 index 0000000000..f416d89a0f --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendComponent.kt @@ -0,0 +1,20 @@ +package com.tangem.features.send.v2.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +interface SendComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + val transactionId: String? = null, + val amount: String? = null, + val tag: String? = null, + val destinationAddress: String? = null, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt new file mode 100644 index 0000000000..14b404270d --- /dev/null +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.send.v2.api + +interface SendFeatureToggles { + val isSendV2Enabled: Boolean +} \ No newline at end of file diff --git a/features/send-v2/impl/.gitignore b/features/send-v2/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/send-v2/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts new file mode 100644 index 0000000000..245cf3a903 --- /dev/null +++ b/features/send-v2/impl/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.send.v2.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.sendV2.api) + implementation(projects.features.txhistory.api) + + /** Libs */ + implementation(projects.libs.crypto) + + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.analytics) + implementation(projects.core.configToggles) + implementation(projects.core.navigation) + + /** Tangem SDK */ + implementation(tangemDeps.blockchain) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.routing) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.legacy) + implementation(projects.domain.tokens.models) + implementation(projects.domain.tokens) + implementation(projects.domain.wallets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.transaction.models) + implementation(projects.domain.transaction) + implementation(projects.domain.txhistory.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.qrScanning.models) + implementation(projects.domain.qrScanning) + implementation(projects.domain.settings) + implementation(projects.domain.feedback) + implementation(projects.domain.txhistory) + + /** Compose libraries */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.material3) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + + /** Other dependencies */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt new file mode 100644 index 0000000000..a36e1cfa6a --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.send.v2 + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.send.v2.api.SendFeatureToggles + +internal class DefaultSendFeatureToggles( + private val featureToggles: FeatureTogglesManager, +) : SendFeatureToggles { + override val isSendV2Enabled: Boolean + get() = featureToggles.isFeatureEnabled("SEND_V2_ENABLED") +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt new file mode 100644 index 0000000000..e8d18db28b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendFeatureModule.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.v2.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.send.v2.DefaultSendFeatureToggles +import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.api.SendFeatureToggles +import com.tangem.features.send.v2.send.DefaultSendComponent +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object SendFeatureModule { + + @Provides + fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): SendFeatureToggles { + return DefaultSendFeatureToggles(featureTogglesManager) + } +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface SendFeatureModuleBinds { + + @Binds + @Singleton + fun provideSendComponentFactory(impl: DefaultSendComponent.Factory): SendComponent.Factory +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt new file mode 100644 index 0000000000..488de424c8 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/di/SendModelModule.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.v2.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.send.v2.send.model.SendModel +import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel +import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface SendModelModule { + + @Binds + @IntoMap + @ClassKey(SendModel::class) + fun provideSendModel(model: SendModel): Model + + @Binds + @IntoMap + @ClassKey(SendAmountModel::class) + fun provideSendAmountModel(model: SendAmountModel): Model + + @Binds + @IntoMap + @ClassKey(SendDestinationModel::class) + fun provideSendDestinationModel(model: SendDestinationModel): Model +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt new file mode 100644 index 0000000000..3a97b61648 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -0,0 +1,120 @@ +package com.tangem.features.send.v2.send + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.api.SendComponent +import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.send.model.SendModel +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterIsInstance + +internal class DefaultSendComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: SendComponent.Params, +) : SendComponent, AppComponentContext by appComponentContext { + + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val initialRoute = SendRoute.Empty + private val currentRoute = MutableStateFlow(initialRoute) + + private val model: SendModel = getOrCreateModel(params = params, router = innerRouter) + + private val childStack = childStack( + key = "sendInnerStack", + source = stackNavigation, + serializer = null, + initialConfiguration = initialRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + createChild( + configuration, + childByContext( + componentContext = factoryContext, + router = innerRouter, + ), + ) + }, + ) + + @Composable + override fun Content(modifier: Modifier) { + BackHandler(onBack = ::onChildBack) + } + + private fun createChild(route: SendRoute, factoryContext: AppComponentContext) = when (route) { + SendRoute.Empty -> getStubComponent() + is SendRoute.Destination -> getDestinationComponent(factoryContext, route) + is SendRoute.Amount -> getAmountComponent(factoryContext, route) + is SendRoute.Fee -> getFeeComponent() + SendRoute.Confirm -> getConfirmComponent() + } + + private fun getDestinationComponent(factoryContext: AppComponentContext, route: SendRoute) = + SendDestinationComponent( + appComponentContext = factoryContext, + params = SendDestinationComponent.Params( + state = model.uiState.value.destinationUM, + currentRoute = currentRoute.filterIsInstance(), + analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + userWallet = model.userWallet, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + callback = model, + isEditMode = route.isEditMode, + ), + ) + + private fun getAmountComponent(factoryContext: AppComponentContext, route: SendRoute) = SendAmountComponent( + appComponentContext = factoryContext, + params = SendAmountComponentParams.AmountParams( + state = model.uiState.value.amountUM, + currentRoute = currentRoute.filterIsInstance(), + analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + userWallet = model.userWallet, + appCurrency = model.appCurrency, + cryptoCurrencyStatus = model.cryptoCurrencyStatus, + callback = model, + isEditMode = route.isEditMode, + predefinedAmountValue = model.predefinedAmountValue, + ), + ) + + private fun getFeeComponent() = getStubComponent() // todo + + private fun getConfirmComponent() = getStubComponent() // todo + + private fun getStubComponent() = ComposableContentComponent { } + + private fun onChildBack() { + if (childStack.value.active.configuration == SendRoute.Empty || childStack.value.backStack.isEmpty()) { + router.pop() + } else { + stackNavigation.pop() + } + } + + @AssistedFactory + interface Factory : SendComponent.Factory { + override fun create(context: AppComponentContext, params: SendComponent.Params): DefaultSendComponent + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt new file mode 100644 index 0000000000..078366292e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/SendRoute.kt @@ -0,0 +1,35 @@ +package com.tangem.features.send.v2.send + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class SendRoute : Route { + + abstract val isEditMode: Boolean + + @Serializable + data object Empty : SendRoute() { + override val isEditMode = false + } + + @Serializable + data object Confirm : SendRoute() { + override val isEditMode: Boolean = false + } + + @Serializable + data class Destination( + override val isEditMode: Boolean, + ) : SendRoute() + + @Serializable + data class Amount( + override val isEditMode: Boolean, + ) : SendRoute() + + @Serializable + data class Fee( + override val isEditMode: Boolean = true, + ) : SendRoute() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt new file mode 100644 index 0000000000..07d650a412 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -0,0 +1,43 @@ +package com.tangem.features.send.v2.send.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +/** + * Send screen analytics + */ +internal sealed class SendAnalyticEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = SEND_CATEGORY, event = event, params = params) { + + /** Recipient address screen opened */ + data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened") + + /** Amount screen opened */ + data object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened") + + /** Fee screen opened */ + data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened") + + /** Confirmation screen opened */ + data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened") + + /** If transaction delays notification is present */ + data class NoticeTransactionDelays( + val token: String, + ) : SendAnalyticEvents( + event = "Notice - Transaction Delays Are Possible", + params = mapOf(TOKEN_PARAM to token), + ) + + /** If error occurs during send transactions */ + data class TransactionError(val token: String) : SendAnalyticEvents( + event = "Error - Transaction Rejected", + params = mapOf(TOKEN_PARAM to token), + ) + + companion object { + const val SEND_CATEGORY = "Token / Send" + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt new file mode 100644 index 0000000000..5c9840758f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -0,0 +1,50 @@ +package com.tangem.features.send.v2.send.model + +import androidx.compose.runtime.Stable +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.ui.state.SendUM +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import kotlin.properties.Delegates + +@Stable +@ModelScoped +@Suppress("LongParameterList") +internal class SendModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, +) : Model(), + SendDestinationComponent.ModelCallback, + SendAmountComponent.ModelCallback { + + private val _uiState = MutableStateFlow(initialState()) + val uiState = _uiState.asStateFlow() + + var userWallet: UserWallet by Delegates.notNull() + var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + var appCurrency: AppCurrency = AppCurrency.Default + var predefinedAmountValue: String? = null + + override fun onDestinationResult(destinationUM: DestinationUM) { + _uiState.update { it.copy(destinationUM = destinationUM) } + } + + override fun onAmountResult(state: AmountState) { + _uiState.update { it.copy(amountUM = state) } + } + + private fun initialState(): SendUM = SendUM( + amountUM = AmountState.Empty(), + destinationUM = DestinationUM.Empty(), + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt new file mode 100644 index 0000000000..41cfc2f614 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/ui/state/SendUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.send.v2.send.ui.state + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM + +internal data class SendUM( + val amountUM: AmountState, + val destinationUM: DestinationUM, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt new file mode 100644 index 0000000000..01594d7266 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountBlockComponent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.send.v2.subcomponents.amount + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountBlockParams +import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +internal class SendAmountBlockComponent( + appComponentContext: AppComponentContext, + private val params: AmountBlockParams, + val onResult: (AmountState) -> Unit, + val onClick: () -> Unit, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendAmountModel = getOrCreateModel(params = params, router = router) + + init { + model.uiState.onEach { + onResult(it) + }.launchIn(componentScope) + } + + fun updateState(amountUM: AmountState) = model.updateState(amountUM) + + @Composable + override fun Content(modifier: Modifier) { + val state = model.uiState.collectAsStateWithLifecycle() + val isClickEnabled = params.blockClickEnableFlow.collectAsStateWithLifecycle() + val isEditingDisabled = params.blockEditDisabledFlow.collectAsStateWithLifecycle() + + AmountBlock( + amountState = state.value, + isClickDisabled = !isClickEnabled.value, + isEditingDisabled = isEditingDisabled.value, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt new file mode 100644 index 0000000000..9cb990495f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt @@ -0,0 +1,41 @@ +package com.tangem.features.send.v2.subcomponents.amount + +import androidx.compose.foundation.background +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.amountScreen.AmountScreenContent +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams.AmountParams +import com.tangem.features.send.v2.subcomponents.amount.model.SendAmountModel + +internal class SendAmountComponent( + appComponentContext: AppComponentContext, + private val params: AmountParams, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendAmountModel = getOrCreateModel(params = params) + + fun updateState(amountUM: AmountState) = model.updateState(amountUM) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + AmountScreenContent( + amountState = state, + isBalanceHidden = false, + clickIntents = model, + modifier = Modifier.background(TangemTheme.colors.background.tertiary), + ) + } + + interface ModelCallback { + fun onAmountResult(amountUM: AmountState) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt new file mode 100644 index 0000000000..e3bafeb146 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -0,0 +1,41 @@ +package com.tangem.features.send.v2.subcomponents.amount + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponent.ModelCallback +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +internal sealed class SendAmountComponentParams { + + abstract val state: AmountState + abstract val analyticsCategoryName: String + abstract val userWallet: UserWallet + abstract val appCurrency: AppCurrency + abstract val cryptoCurrencyStatus: CryptoCurrencyStatus + + data class AmountParams( + override val state: AmountState, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val appCurrency: AppCurrency, + override val cryptoCurrencyStatus: CryptoCurrencyStatus, + val isEditMode: Boolean, + val callback: ModelCallback, + val currentRoute: Flow, + val predefinedAmountValue: String?, + ) : SendAmountComponentParams() + + data class AmountBlockParams( + override val state: AmountState, + override val analyticsCategoryName: String, + override val userWallet: UserWallet, + override val appCurrency: AppCurrency, + override val cryptoCurrencyStatus: CryptoCurrencyStatus, + val blockEditDisabledFlow: StateFlow, + val blockClickEnableFlow: StateFlow, + ) : SendAmountComponentParams() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt new file mode 100644 index 0000000000..4c4f1acb33 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountReduceTrigger.kt @@ -0,0 +1,40 @@ +package com.tangem.features.send.v2.subcomponents.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import java.math.BigDecimal +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Trigger for reducing amount from another component + */ +interface SendAmountReduceTrigger { + suspend fun triggerReduceBy(reduceBy: ReduceByData) + suspend fun triggerReduceTo(reduceTo: BigDecimal) +} + +/** + * Trigger for reducing amount from another component + */ +interface SendAmountReduceListener { + val reduceToTriggerFlow: Flow + val reduceByTriggerFlow: Flow +} + +@Singleton +internal class DefaultSendAmountReduceTrigger @Inject constructor() : + SendAmountReduceTrigger, + SendAmountReduceListener { + override val reduceToTriggerFlow = MutableSharedFlow() + override val reduceByTriggerFlow = MutableSharedFlow() + + override suspend fun triggerReduceBy(reduceBy: ReduceByData) { + reduceByTriggerFlow.emit(reduceBy) + } + + override suspend fun triggerReduceTo(reduceTo: BigDecimal) { + reduceToTriggerFlow.emit(reduceTo) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt new file mode 100644 index 0000000000..97d65a88e9 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/analytics/SendAmountAnalyticEvents.kt @@ -0,0 +1,31 @@ +package com.tangem.features.send.v2.subcomponents.amount.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.TYPE + +internal sealed class SendAmountAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + /** Selected currency */ + data class SelectedCurrency( + val categoryName: String, + val type: SelectedCurrencyType, + ) : SendAmountAnalyticEvents( + category = categoryName, + event = "Selected Currency", + params = mapOf(TYPE to type.value), + ) + + /** Max amount button clicked */ + data class MaxAmountButtonClicked( + val categoryName: String, + ) : SendAmountAnalyticEvents(category = categoryName, event = "Max Amount Taped") + + internal enum class SelectedCurrencyType(val value: String) { + Token("Token"), + AppCurrency("App Currency"), + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt new file mode 100644 index 0000000000..5152941ae4 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/di/SendAmountModule.kt @@ -0,0 +1,27 @@ +package com.tangem.features.send.v2.subcomponents.amount.di + +import com.tangem.features.send.v2.subcomponents.amount.DefaultSendAmountReduceTrigger +import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener +import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal object SendAmountModule { + + @Provides + @Singleton + fun provideSendAmountReduceTrigger(): SendAmountReduceTrigger { + return DefaultSendAmountReduceTrigger() + } + + @Provides + @Singleton + fun provideSendAmountReduceListener(): SendAmountReduceListener { + return DefaultSendAmountReduceTrigger() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt new file mode 100644 index 0000000000..25fa331654 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -0,0 +1,239 @@ +package com.tangem.features.send.v2.subcomponents.amount.model + +import androidx.compose.runtime.Stable +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.converters.* +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer +import com.tangem.common.ui.amountScreen.models.AmountParameters +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.amount.SendAmountComponentParams +import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceListener +import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents +import com.tangem.features.send.v2.subcomponents.amount.analytics.SendAmountAnalyticEvents.SelectedCurrencyType +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.orZero +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.update +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@Stable +@ModelScoped +internal class SendAmountModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, + private val sendAmountReduceListener: SendAmountReduceListener, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model(), AmountScreenClickIntents { + + private val params: SendAmountComponentParams = paramsContainer.require() + + private val _uiState = MutableStateFlow(params.state) + val uiState = _uiState.asStateFlow() + + private val analyticsCategoryName = params.analyticsCategoryName + private val userWallet = params.userWallet + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + + private var minAmountBoundary: EnterAmountBoundary? = null + private var maxAmountBoundary: EnterAmountBoundary = MaxEnterAmountConverter().convert(cryptoCurrencyStatus) + + init { + configAmountNavigation() + initMinBoundary() + subscribeOnAmountReduceByTriggerUpdates() + subscribeOnAmountReduceToTriggerUpdates() + } + + private fun initMinBoundary() { + modelScope.launch { + minAmountBoundary = getMinimumTransactionAmountSyncUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull()?.let { + EnterAmountBoundary( + amount = it, + fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(), + ) + } + + initialState() + } + } + + private fun initialState() { + val predefinedAmountValue = (params as? SendAmountComponentParams.AmountParams)?.predefinedAmountValue + if (uiState.value is AmountState.Empty) { + _uiState.update { + AmountStateConverterV2( + clickIntents = this, + appCurrency = params.appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxAmountBoundary, + iconStateConverter = CryptoCurrencyToIconStateConverter(), + ).convert( + AmountParameters( + title = stringReference(userWallet.name), + value = "", + ), + ) + } + if (predefinedAmountValue != null) { + onAmountValueChange(predefinedAmountValue) + } + } + } + + fun updateState(amountUM: AmountState) { + if (amountUM !is AmountState.Empty) { + _uiState.value = amountUM + } + } + + override fun onCurrencyChangeClick(isFiat: Boolean) { + _uiState.update(AmountCurrencyTransformer(cryptoCurrencyStatus, isFiat)) + } + + override fun onAmountValueChange(value: String) { + _uiState.update( + AmountFieldChangeTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxAmountBoundary, + minimumTransactionAmount = minAmountBoundary, + value = value, + ), + ) + } + + override fun onMaxValueClick() { + val decimalCryptoValue = cryptoCurrencyStatus.value.amount + if (decimalCryptoValue.isNullOrZero()) return + + _uiState.update( + AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxAmount = maxAmountBoundary, + minAmount = minAmountBoundary, + ), + ) + analyticsEventHandler.send( + SendAmountAnalyticEvents.MaxAmountButtonClicked(categoryName = analyticsCategoryName), + ) + } + + override fun onAmountPasteTriggerDismiss() { + _uiState.update(AmountPastedTriggerDismissTransformer) + } + + override fun onAmountNext() { + (uiState.value as? AmountState.Data)?.amountTextField?.isFiatValue?.let { isFiatSelected -> + analyticsEventHandler.send( + SendAmountAnalyticEvents.SelectedCurrency( + categoryName = analyticsCategoryName, + type = if (isFiatSelected) { + SelectedCurrencyType.AppCurrency + } else { + SelectedCurrencyType.Token + }, + ), + ) + } + saveResult() + if ((params as? SendAmountComponentParams.AmountParams)?.isEditMode == true) { + router.pop() + } else { + router.push(SendRoute.Confirm) + } + } + + private fun subscribeOnAmountReduceToTriggerUpdates() { + sendAmountReduceListener.reduceToTriggerFlow + .onEach { reduceTo -> + _uiState.update( + AmountReduceToTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minAmountBoundary, + value = reduceTo, + ), + ) + } + .launchIn(modelScope) + } + + private fun subscribeOnAmountReduceByTriggerUpdates() { + sendAmountReduceListener.reduceByTriggerFlow + .onEach { reduceByData -> + _uiState.update( + AmountReduceByTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minAmountBoundary, + value = reduceByData, + ), + ) + } + .launchIn(modelScope) + } + + private fun saveResult() { + val params = params as? SendAmountComponentParams.AmountParams ?: return + params.callback.onAmountResult(uiState.value) + } + + private fun configAmountNavigation() { + val params = params as? SendAmountComponentParams.AmountParams ?: return + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { (_, _) -> + // todo + // params.callback.onNavigationResult( + // NavigationUM.Content( + // title = resourceReference(R.string.send_amount_label), + // subtitle = null, + // backIconRes = R.drawable.ic_back_24, + // backIconClick = { + // if (route.isEditMode) { + // saveResult() + // } + // router.pop() + // }, + // primaryButton = ButtonsUM.PrimaryButtonUM( + // text = if (route.isEditMode) { + // resourceReference(R.string.common_continue) + // } else { + // resourceReference(R.string.common_next) + // }, + // isEnabled = state.isPrimaryButtonEnabled, + // onClick = ::onAmountNext, + // ), + // prevButton = ButtonsUM.PrimaryButtonUM( + // text = TextReference.EMPTY, + // iconResId = R.drawable.ic_back_24, + // isEnabled = true, + // onClick = { + // saveResult() + // router.pop() + // }, + // ).takeIf { route.isEditMode.not() }, + // secondaryPairButtonsUM = null, + // ), + // ) + }.launchIn(modelScope) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt new file mode 100644 index 0000000000..3110982216 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/SendDestinationComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.send.v2.subcomponents.destination + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationModel +import com.tangem.features.send.v2.subcomponents.destination.ui.SendDestinationContent +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import kotlinx.coroutines.flow.Flow + +internal class SendDestinationComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableContentComponent, AppComponentContext by appComponentContext { + + private val model: SendDestinationModel = getOrCreateModel(params = params, router = router) + + @Composable + override fun Content(modifier: Modifier) { + val state = model.uiState.collectAsStateWithLifecycle() + + SendDestinationContent(state = state.value, model, isBalanceHidden = false) + } + + data class Params( + val state: DestinationUM, + val analyticsCategoryName: String, + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val callback: ModelCallback, + val currentRoute: Flow, + val isEditMode: Boolean, + ) + + interface ModelCallback { + fun onDestinationResult(destinationUM: DestinationUM) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt new file mode 100644 index 0000000000..1c61574dfa --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/EnterAddressSource.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.v2.subcomponents.destination.analytics + +internal enum class EnterAddressSource { + QRCode, + PasteButton, + RecentAddress, + InputField, + ; + + val isPasted: Boolean + get() = this != InputField +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt new file mode 100644 index 0000000000..84c52b4907 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/analytics/SendDestinationAnalyticEvents.kt @@ -0,0 +1,37 @@ +package com.tangem.features.send.v2.subcomponents.destination.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE +import com.tangem.core.analytics.models.AnalyticsParam.Key.VALIDATION + +internal sealed class SendDestinationAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + abstract val categoryName: String + + /** Address to send entered */ + data class AddressEntered( + override val categoryName: String, + val source: EnterAddressSource, + val isValid: Boolean, + ) : SendDestinationAnalyticEvents( + category = categoryName, + event = "Address Entered", + params = mapOf( + SOURCE to source.name, + VALIDATION to if (isValid) "Success" else "Fail", + ), + ) + + /** Qr Code button clicked */ + data class QrCodeButtonClicked( + override val categoryName: String, + ) : + SendDestinationAnalyticEvents( + category = categoryName, + event = "Button - QR Code", + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationClickIntents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationClickIntents.kt new file mode 100644 index 0000000000..73a1ce762b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationClickIntents.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.v2.subcomponents.destination.model + +import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource + +internal interface SendDestinationClickIntents { + + fun onRecipientAddressValueChange(value: String, type: EnterAddressSource) + + fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean = false) + + fun onQrCodeScanClick() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt new file mode 100644 index 0000000000..51c0bc7f63 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -0,0 +1,306 @@ +package com.tangem.features.send.v2.subcomponents.destination.model + +import androidx.compose.runtime.Stable +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.GetNetworkAddressesUseCase +import com.tangem.domain.transaction.usecase.IsUtxoConsolidationAvailableUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase +import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.send.v2.send.SendRoute +import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponent +import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource +import com.tangem.features.send.v2.subcomponents.destination.analytics.SendDestinationAnalyticEvents +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.* +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.transformer.update +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +@Suppress("LongParameterList") +internal class SendDestinationModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, + private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, + private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, + private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, + private val isUtxoConsolidationAvailableUseCase: IsUtxoConsolidationAvailableUseCase, + private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val parseQrCodeUseCase: ParseQrCodeUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Model(), SendDestinationClickIntents { + private val params: SendDestinationComponent.Params = paramsContainer.require() + + private val _uiState = MutableStateFlow(params.state) + val uiState = _uiState.asStateFlow() + + private val analyticsCategoryName = params.analyticsCategoryName + private val userWallet = params.userWallet + private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + private val cryptoCurrency = cryptoCurrencyStatus.currency + + private var validationJobHolder = JobHolder() + + init { + configDestinationNavigation() + initialState() + getWalletsAndRecent() + subscribeOnQRScannerResult() + } + + private fun initialState() { + if (uiState.value is DestinationUM.Empty) { + _uiState.update( + SendDestinationInitialStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + onAddressChange = ::onRecipientAddressValueChange, + onMemoChange = ::onRecipientMemoValueChange, + ), + ) + } + } + + fun updateState(state: DestinationUM) { + if (state !is DestinationUM.Empty) { + _uiState.value = state + } + } + + override fun onRecipientAddressValueChange(value: String, type: EnterAddressSource) { + _uiState.update( + SendDestinationAddressTransformer( + address = value, + isPasted = type.isPasted, + ), + ) + val memo = (uiState.value as? DestinationUM.Content)?.memoTextField?.value + validate(address = value, memo = memo, type) + } + + override fun onRecipientMemoValueChange(value: String, isValuePasted: Boolean) { + _uiState.update( + SendDestinationMemoTransformer( + memo = value, + isPasted = isValuePasted, + ), + ) + val address = (uiState.value as? DestinationUM.Content)?.addressTextField?.value.orEmpty() + validate(address = address, memo = value) + } + + override fun onQrCodeScanClick() { + analyticsEventHandler.send(SendDestinationAnalyticEvents.QrCodeButtonClicked(analyticsCategoryName)) + router.push( + AppRoute.QrScanning( + source = AppRoute.QrScanning.Source.SEND, + networkName = cryptoCurrency.network.name, + ), + ) + } + + private fun subscribeOnQRScannerResult() { + listenToQrScanningUseCase(SourceType.SEND) + .getOrElse { emptyFlow() } + .onEach(::onQrCodeScanned) + .launchIn(modelScope) + } + + private fun onQrCodeScanned(address: String) { + parseQrCodeUseCase(address, cryptoCurrency).fold( + ifRight = { parsedCode -> + onRecipientAddressValueChange(parsedCode.address, EnterAddressSource.QRCode) + parsedCode.memo?.let { onRecipientMemoValueChange(it) } + }, + ifLeft = { + onRecipientAddressValueChange(address, EnterAddressSource.QRCode) + }, + ) + } + + private fun getWalletsAndRecent() { + combine( + flow = getWalletsUseCase().conflate().map { + it.toAvailableWallets() + }, + flow2 = getFixedTxHistoryItemsUseCase( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + pageSize = RECENT_TX_SIZE, + ).getOrElse { flowOf(emptyList()) }.conflate(), + ) { destinationWalletList, txHistoryList -> + val isUtxoConsolidationAvailable = isUtxoConsolidationAvailableUseCase.invokeSync( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + _uiState.update( + SendDestinationRecentListTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isUtxoConsolidationAvailable = isUtxoConsolidationAvailable, + destinationWalletList = destinationWalletList, + txHistoryList, + ), + ) + }.launchIn(modelScope) + } + + private suspend fun List.toAvailableWallets(): List { + return coroutineScope { + val cryptoCurrencyNetwork = cryptoCurrency.network + + return@coroutineScope filterNot { it.isLocked } + .map { wallet -> + async { + val addresses = if (!wallet.isMultiCurrency) { + getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { + if (it.network.id == cryptoCurrencyNetwork.id) { + getNetworkAddressesUseCase.invokeSync(wallet.walletId, it.network) + } else { + null + } + } + } else { + getNetworkAddressesUseCase.invokeSync(wallet.walletId, cryptoCurrencyNetwork) + } + wallet to addresses + } + }.awaitAll() + .asSequence() + .mapNotNull { (wallet, addresses) -> + addresses?.map { (cryptoCurrency, address) -> + DestinationWalletUM( + name = wallet.name, + address = address, + cryptoCurrency = cryptoCurrency, + userWalletId = wallet.walletId, + ) + } + }.flatten() + .toList() + } + } + + private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) { + modelScope.launch { + _uiState.update(SendDestinationValidationStartedTransformer) + + val addressValidationResult = validateWalletAddressUseCase( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + address = address, + currencyAddress = cryptoCurrencyStatus.value.networkAddress?.availableAddresses, + ) + val memoValidationResult = validateWalletMemoUseCase( + memo = memo.orEmpty(), + network = cryptoCurrency.network, + ) + + type?.let { + analyticsEventHandler.send( + SendDestinationAnalyticEvents.AddressEntered( + categoryName = analyticsCategoryName, + source = it, + isValid = addressValidationResult.isRight(), + ), + ) + } + _uiState.update(SendDestinationValidationResultTransformer(addressValidationResult, memoValidationResult)) + autoNextFromRecipient(type, addressValidationResult.isRight(), memoValidationResult.isRight()) + }.saveIn(validationJobHolder) + } + + private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { + val isRecent = type == EnterAddressSource.RecentAddress + if (isRecent && isValidAddress && isValidMemo) onNextClick() + } + + private fun saveResult() { + params.callback.onDestinationResult(uiState.value) + } + + private fun onNextClick() { + saveResult() + if (params.isEditMode) { + router.pop() + } else { + router.push(SendRoute.Amount(isEditMode = false)) + } + } + + private fun configDestinationNavigation() { + combine( + flow = uiState, + flow2 = params.currentRoute, + transform = { state, route -> state to route }, + ).onEach { + // todo + // params.callback.onNavigationResult( + // NavigationUM.Content( + // title = resourceReference(R.string.send_recipient_label), + // subtitle = null, + // backIconRes = if (route.isEditMode) { + // R.drawable.ic_back_24 + // } else { + // R.drawable.ic_close_24 + // }, + // backIconClick = { + // if (route.isEditMode) { + // router.pop() + // } else { + // appRouter.pop() + // } + // }, + // additionalIconRes = R.drawable.ic_qrcode_scan_24, + // additionalIconClick = { + // router.push( + // AppRoute.QrScanning( + // source = AppRoute.QrScanning.Source.SEND, + // networkName = cryptoCurrency.network.name, + // ), + // ) + // }, + // primaryButton = ButtonsUM.PrimaryButtonUM( + // text = if (route.isEditMode) { + // resourceReference(R.string.common_continue) + // } else { + // resourceReference(R.string.common_next) + // }, + // isEnabled = state.isPrimaryButtonEnabled, + // onClick = ::onNextClick, + // ), + // prevButton = null, + // secondaryPairButtonsUM = null, + // ), + // ) + }.launchIn(modelScope) + } + + private companion object { + const val RECENT_TX_SIZE = 100 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt new file mode 100644 index 0000000000..3c77cd1b51 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientHistoryListConverter.kt @@ -0,0 +1,92 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.converters + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.RECENT_DEFAULT_COUNT +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.RECENT_KEY_TAG +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.emptyListState +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList + +internal class SendRecipientHistoryListConverter( + private val cryptoCurrency: CryptoCurrency, +) : Converter, ImmutableList> { + + override fun convert(value: List): ImmutableList { + return value.filterRecipients(cryptoCurrency).ifEmpty { + emptyListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT) + } + } + + private fun List.filterRecipients(cryptoCurrency: CryptoCurrency) = this.filter { item -> + val isTransfer = item.type == TxHistoryItem.TransactionType.Transfer + val isNotContract = item.interactionAddressType is TxHistoryItem.InteractionAddressType.User + val isSingleAddress = if (item.isOutgoing) { + item.destinationType is TxHistoryItem.DestinationType.Single + } else { + item.sourceType is TxHistoryItem.SourceType.Single + } + val notZero = !item.amount.isZero() + isTransfer && isSingleAddress && isNotContract && item.isOutgoing && notZero + } + .take(RECENT_LIST_SIZE) + .mapIndexed { index, tx -> + DestinationRecipientListUM( + id = "${RECENT_KEY_TAG}$index", + title = tx.extractAddress(), + subtitle = stringReference(tx.getAmount(cryptoCurrency).trim()), + timestamp = tx.extractTimestamp(), + subtitleEndOffset = cryptoCurrency.symbol.length, + subtitleIconRes = tx.extractIconRes(), + ) + }.toPersistentList() + + private fun TxHistoryItem.extractAddress(): TextReference = if (isOutgoing) { + when (val destination = destinationType) { + is TxHistoryItem.DestinationType.Multiple -> resourceReference( + R.string.transaction_history_multiple_addresses, + ) + is TxHistoryItem.DestinationType.Single -> stringReference(destination.addressType.address) + } + } else { + when (val source = sourceType) { + is TxHistoryItem.SourceType.Multiple -> resourceReference(R.string.transaction_history_multiple_addresses) + is TxHistoryItem.SourceType.Single -> stringReference(source.address) + } + } + + private fun TxHistoryItem.extractIconRes() = if (isOutgoing) { + R.drawable.ic_arrow_up_24 + } else { + R.drawable.ic_arrow_down_24 + } + + private fun TxHistoryItem.getAmount(cryptoCurrency: CryptoCurrency): String { + return amount.format { crypto(cryptoCurrency) } + } + + private fun TxHistoryItem.extractTimestamp(): TextReference { + val date = timestampInMillis.toDateFormatWithTodayYesterday( + formatter = DateTimeFormatters.dateDDMMYYYY, + ) + val time = timestampInMillis.toTimeFormat() + return TextReference.Res(R.string.send_date_format, wrappedList(date, time)) + } + + companion object { + private const val RECENT_LIST_SIZE = 10 + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt new file mode 100644 index 0000000000..674921ad63 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt @@ -0,0 +1,67 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.converters + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_DEFAULT_COUNT +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_KEY_TAG +import com.tangem.features.send.v2.subcomponents.destination.model.transformers.emptyListState +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal class SendRecipientWalletListConverter( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val isUtxoConsolidationAvailable: Boolean, +) : + Converter, PersistentList> { + override fun convert(value: List): PersistentList { + return value.filterWallets().ifEmpty { + emptyListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT) + } + } + + private fun List.filterWallets(): PersistentList { + var walletsCounter = 0 + val currentAddress: String = runCatching { + cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + }.getOrNull().orEmpty() + + return this.filterNotNull() + .filter { + val isCoin = it.cryptoCurrency is CryptoCurrency.Coin + val isNotSameAddress = it.address != currentAddress + val isNotBlankAddress = it.address.isNotBlank() + + isNotBlankAddress && isCoin && (isNotSameAddress || isUtxoConsolidationAvailable) + } + .groupBy { item -> item.name } + .values.map { wallets -> + val groupedByWallet = wallets.groupBy { it.userWalletId } + var i = 0 + groupedByWallet + .flatMap { item -> + item.value.map { wallet -> + val name = if (groupedByWallet.size > 1) { + "${wallet.name} ${++i}" + } else { + wallet.name + } + + DestinationRecipientListUM( + id = "${WALLET_KEY_TAG}${walletsCounter++}", + title = stringReference(wallet.address), + subtitle = stringReference(name), + address = wallet.address, + userWalletId = wallet.userWalletId, + network = wallet.cryptoCurrency.network, + ) + } + } + } + .flatten() + .toPersistentList() + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt new file mode 100644 index 0000000000..0083e68d7d --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/RecentListUtils.kt @@ -0,0 +1,32 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM +import kotlinx.collections.immutable.toPersistentList + +internal const val WALLET_DEFAULT_COUNT = 1 +internal const val RECENT_DEFAULT_COUNT = 3 +internal const val WALLET_KEY_TAG = "wallet" +internal const val RECENT_KEY_TAG = "recent" + +internal fun loadingListState(tag: String, count: Int) = buildList { + repeat(count) { + add( + DestinationRecipientListUM( + id = "$tag$it", + isLoading = true, + ), + ) + } +}.toPersistentList() + +internal fun emptyListState(tag: String, count: Int) = buildList { + repeat(count) { + add( + DestinationRecipientListUM( + id = "$tag$it", + isLoading = false, + isVisible = false, + ), + ) + } +}.toPersistentList() \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationAddressTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationAddressTransformer.kt new file mode 100644 index 0000000000..e717f65218 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationAddressTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationAddressTransformer( + private val address: String, + private val isPasted: Boolean, +) : Transformer { + + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + return state.copy( + addressTextField = state.addressTextField.copy(value = address, isValuePasted = isPasted), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt new file mode 100644 index 0000000000..fdd0e7f59e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationInitialStateTransformer.kt @@ -0,0 +1,62 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationInitialStateTransformer( + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val onAddressChange: (String, EnterAddressSource) -> Unit, + val onMemoChange: (String, Boolean) -> Unit, +) : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val memoType = when (cryptoCurrencyStatus.currency.network.transactionExtrasType) { + Network.TransactionExtrasType.NONE -> null + Network.TransactionExtrasType.MEMO -> R.string.send_extras_hint_memo + Network.TransactionExtrasType.DESTINATION_TAG -> R.string.send_destination_tag_field + } + return DestinationUM.Content( + isPrimaryButtonEnabled = false, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "", + onValueChange = { onAddressChange(it, EnterAddressSource.InputField) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Text, + ), + error = null, + placeholder = resourceReference(R.string.send_enter_address_field), + label = resourceReference(R.string.send_recipient), + isValuePasted = false, + ), + memoTextField = memoType?.let { + DestinationTextFieldUM.RecipientMemo( + value = "", + onValueChange = { onMemoChange(it, false) }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done, + keyboardType = KeyboardType.Text, + ), + placeholder = resourceReference(R.string.send_optional_field), + label = resourceReference(memoType), + error = resourceReference(R.string.send_memo_destination_tag_error), + disabledText = resourceReference(R.string.send_additional_field_already_included), + isEnabled = true, + isValuePasted = false, + ) + }, + wallets = loadingListState(WALLET_KEY_TAG, WALLET_DEFAULT_COUNT), + recent = loadingListState(RECENT_KEY_TAG, RECENT_DEFAULT_COUNT), + networkName = cryptoCurrencyStatus.currency.network.name, + isValidating = false, + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationMemoTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationMemoTransformer.kt new file mode 100644 index 0000000000..7a7883fcd8 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationMemoTransformer.kt @@ -0,0 +1,16 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationMemoTransformer( + private val memo: String, + private val isPasted: Boolean, +) : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + return state.copy( + memoTextField = state.memoTextField?.copy(value = memo, isValuePasted = isPasted), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt new file mode 100644 index 0000000000..f1a3f20621 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationRecentListTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.txhistory.models.TxHistoryItem +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientHistoryListConverter +import com.tangem.features.send.v2.subcomponents.destination.model.converters.SendRecipientWalletListConverter +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationWalletUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationRecentListTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val isUtxoConsolidationAvailable: Boolean, + private val destinationWalletList: List, + private val txHistoryList: List, +) : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + return state.copy( + wallets = SendRecipientWalletListConverter( + cryptoCurrencyStatus, + isUtxoConsolidationAvailable, + ).convert(destinationWalletList), + recent = SendRecipientHistoryListConverter( + cryptoCurrency = cryptoCurrencyStatus.currency, + ).convert(txHistoryList), + ) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt new file mode 100644 index 0000000000..8830a8ece8 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationResultTransformer.kt @@ -0,0 +1,49 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import arrow.core.Either +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.transaction.error.AddressValidationResult +import com.tangem.domain.transaction.error.ValidateMemoError +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal class SendDestinationValidationResultTransformer( + private val addressValidationResult: AddressValidationResult, + private val memoValidationResult: Either, +) : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + val isValidAddress = addressValidationResult.isRight() + val isValidMemo = memoValidationResult.isRight() + + val addressErrorText = addressValidationResult.mapLeft { + when (it) { + is AddressValidation.Error.DataError, + AddressValidation.Error.InvalidAddress, + -> R.string.send_recipient_address_error + AddressValidation.Error.AddressInWallet -> R.string.send_error_address_same_as_wallet + } + }.leftOrNull() + + return state.copy( + isValidating = false, + isPrimaryButtonEnabled = isValidAddress && isValidMemo, + addressTextField = state.addressTextField.copy( + error = addressErrorText?.let(::resourceReference), + isError = state.addressTextField.value.isNotEmpty() && !isValidAddress, + ), + memoTextField = state.memoTextField?.copy( + isError = state.memoTextField.value.isNotEmpty() && !isValidMemo, + isEnabled = !shouldDisableMemo(), + ), + ) + } + + /** Ripple X-Address contains memo, so memo field is unnecessary */ + private fun shouldDisableMemo(): Boolean { + return addressValidationResult.isRight { it == AddressValidation.Success.ValidXAddress } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationStartedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationStartedTransformer.kt new file mode 100644 index 0000000000..bc2354f57f --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/transformers/SendDestinationValidationStartedTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.features.send.v2.subcomponents.destination.model.transformers + +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.utils.transformer.Transformer + +internal object SendDestinationValidationStartedTransformer : Transformer { + override fun transform(prevState: DestinationUM): DestinationUM { + val state = prevState as? DestinationUM.Content ?: return prevState + + return state.copy(isValidating = true) + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/ListItemWithIcon.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/ListItemWithIcon.kt new file mode 100644 index 0000000000..458fa9d397 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/ListItemWithIcon.kt @@ -0,0 +1,245 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +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.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 androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.rememberHapticFeedback +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.impl.R + +/** + * Row item with title and subtitle + * + * @param title title + * @param subtitle subtitle + * @param onClick click listener + * @param modifier modifier + * @param info info + * @param subtitleEndOffset offset for subtitle ellipsis + * @param subtitleIconRes icon + */ +@Composable +fun ListItemWithIcon( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + info: String? = null, + subtitleEndOffset: Int = 0, + @DrawableRes subtitleIconRes: Int? = null, + isLoading: Boolean = false, +) { + AnimatedContent( + targetState = isLoading, + label = "Recent List Content Animation", + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + ) { isLoadingState -> + if (isLoadingState) { + ListItemLoading(modifier = modifier) + } else { + ListItemWithIcon( + title = title, + subtitle = subtitle, + onClick = onClick, + info = info, + subtitleEndOffset = subtitleEndOffset, + subtitleIconRes = subtitleIconRes, + modifier = modifier, + ) + } + } +} + +@Composable +private fun ListItemWithIcon( + title: String, + subtitle: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + info: String? = null, + subtitleEndOffset: Int = 0, + @DrawableRes subtitleIconRes: Int? = null, +) { + val hapticFeedback = rememberHapticFeedback(state = title, onAction = onClick) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickable { hapticFeedback() } + .padding(horizontal = 12.dp), + ) { + IdentIcon( + address = title, + modifier = Modifier + .padding(vertical = 8.dp) + .size(40.dp) + .clip(RoundedCornerShape(20.dp)), + ) + Column( + modifier = Modifier + .height(36.dp) + .padding(start = 12.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + EllipsisText( + text = title, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Justify, + ellipsis = TextEllipsis.Middle, + modifier = Modifier, + ) + Row { + if (subtitleIconRes != null) { + Icon( + painter = painterResource(id = subtitleIconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier + .padding(end = 2.dp) + .size(16.dp) + .background(TangemTheme.colors.background.tertiary, CircleShape) + .padding(2.dp), + ) + } + val (text, offset) = remember(subtitle, info) { + if (info != null) { + val suffix = ", $info" + subtitle + suffix to suffix.length + subtitleEndOffset + } else { + subtitle to 0 + } + } + EllipsisText( + text = text, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = TextEllipsis.OffsetEnd(offsetEnd = offset), + ) + } + } + } +} + +@Composable +private fun ListItemLoading(modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 12.dp), + ) { + CircleShimmer( + modifier = Modifier + .padding(vertical = 8.dp) + .size(40.dp), + ) + Column( + modifier = Modifier + .height(36.dp) + .padding(start = 12.dp), + verticalArrangement = Arrangement.SpaceBetween, + ) { + RectangleShimmer( + radius = 3.dp, + modifier = Modifier.size( + width = 70.dp, + height = 12.dp, + ), + ) + RectangleShimmer( + radius = 3.dp, + modifier = Modifier.size( + width = 52.dp, + height = 12.dp, + ), + ) + } + } +} + +// region preview +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun ListItemWithIconPreview( + @PreviewParameter(ListItemWithIconPreviewProvider::class) config: ListItemWithIconPreviewConfig, +) { + TangemThemePreview { + ListItemWithIcon( + title = config.title, + subtitle = config.subtitle, + subtitleEndOffset = config.subtitleEndOffset, + subtitleIconRes = config.iconRes, + onClick = {}, + isLoading = config.isLoading, + ) + } +} + +private data class ListItemWithIconPreviewConfig( + val title: String, + val subtitle: String, + val info: String? = null, + val subtitleEndOffset: Int = 0, + val iconRes: Int? = null, + val isLoading: Boolean = false, +) + +private class ListItemWithIconPreviewProvider : CollectionPreviewParameterProvider( + collection = listOf( + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "0.000000000000000000000000000000 BTC", + info = "0.0.0000 at 00:00", + subtitleEndOffset = "BTC".length, + iconRes = R.drawable.ic_arrow_down_24, + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "1 BTC", + info = "0.0.0000 at 00:00", + subtitleEndOffset = "BTC".length, + iconRes = R.drawable.ic_arrow_down_24, + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "Wallet", + ), + ListItemWithIconPreviewConfig( + title = "0x34B4492A412D84A6E606288f3Bd714b89135D4dE", + subtitle = "0.000000000000000000000000000000 BTC", + info = "0.0.0000 at 00:00", + subtitleEndOffset = "BTC".length, + iconRes = R.drawable.ic_arrow_down_24, + isLoading = true, + ), + ), +) +//endregion \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt new file mode 100644 index 0000000000..967f088d14 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/SendDestinationContent.kt @@ -0,0 +1,248 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui + +import androidx.annotation.StringRes +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.components.inputrow.InputRowRecipient +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.send.v2.impl.R +import com.tangem.features.send.v2.subcomponents.destination.analytics.EnterAddressSource +import com.tangem.features.send.v2.subcomponents.destination.model.SendDestinationClickIntents +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationRecipientListUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import kotlinx.collections.immutable.ImmutableList + +private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" +private const val MEMO_FIELD_KEY = "MEMO_FIELD_KEY" + +@Composable +internal fun SendDestinationContent( + state: DestinationUM, + clickIntents: SendDestinationClickIntents, + isBalanceHidden: Boolean, +) { + if (state !is DestinationUM.Content) return + val recipients = state.recent + val wallets = state.wallets + val memoField = state.memoTextField + val address = state.addressTextField + val isValidating by remember(state.isValidating) { derivedStateOf { state.isValidating } } + val isError by remember(address.isError) { derivedStateOf { address.isError } } + LazyColumn( + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) { + addressItem( + address = address, + networkName = state.networkName, + isError = isError, + isValidating = isValidating, + onAddressChange = clickIntents::onRecipientAddressValueChange, + ) + memoField( + memoField = memoField, + onMemoChange = clickIntents::onRecipientMemoValueChange, + ) + listHeaderItem( + titleRes = R.string.send_recipient_wallets_title, + isVisible = wallets.isNotEmpty() && wallets.first().isVisible, + isFirst = true, + ) + listItem( + list = wallets, + clickIntents = clickIntents, + isLast = recipients.any { !it.isVisible }, + isBalanceHidden = isBalanceHidden, + ) + listHeaderItem( + titleRes = R.string.send_recent_transactions, + isVisible = recipients.isNotEmpty() && recipients.first().isVisible, + isFirst = wallets.any { !it.isVisible }, + ) + listItem( + list = recipients, + clickIntents = clickIntents, + isLast = true, + isBalanceHidden = isBalanceHidden, + ) + } +} + +private fun LazyListScope.addressItem( + address: DestinationTextFieldUM.RecipientAddress, + networkName: String, + isError: Boolean, + isValidating: Boolean, + onAddressChange: (String, EnterAddressSource) -> Unit, +) { + item(key = ADDRESS_FIELD_KEY) { + FooterContainer( + footer = resourceReference(R.string.send_recipient_address_footer, wrappedList(networkName)), + ) { + InputRowRecipient( + value = address.value, + title = address.label, + placeholder = address.placeholder, + onValueChange = { onAddressChange(it, EnterAddressSource.InputField) }, + onPasteClick = { onAddressChange(it, EnterAddressSource.PasteButton) }, + isError = isError, + isLoading = isValidating, + error = address.error, + isValuePasted = address.isValuePasted, + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + ) + } + } +} + +private fun LazyListScope.memoField( + memoField: DestinationTextFieldUM.RecipientMemo?, + onMemoChange: (String, Boolean) -> Unit, +) { + if (memoField != null) { + item(key = MEMO_FIELD_KEY) { + val placeholder = if (memoField.isEnabled) memoField.placeholder else memoField.disabledText + TextFieldWithPaste( + value = memoField.value, + label = memoField.label, + placeholder = placeholder, + footer = resourceReference(R.string.send_recipient_memo_footer), + onValueChange = { onMemoChange(it, false) }, + onPasteClick = { onMemoChange(it, true) }, + modifier = Modifier.padding(top = 20.dp), + labelStyle = TangemTheme.typography.subtitle2, + isError = memoField.isError, + error = memoField.error, + isReadOnly = !memoField.isEnabled, + isValuePasted = memoField.isValuePasted, + ) + } + } +} + +private fun LazyListScope.listHeaderItem(@StringRes titleRes: Int, isVisible: Boolean, isFirst: Boolean) { + item(key = titleRes) { + AnimateRecentAppearance(isVisible) { + val (topPadding, paddingFromTop) = if (isFirst) { + 20.dp to 12.dp + } else { + 0.dp to 8.dp + } + val topRadius = if (isFirst) 16.dp else 0.dp + Text( + text = stringResourceSafe(titleRes), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .padding(top = topPadding) + .clip( + RoundedCornerShape( + topEnd = topRadius, + topStart = topRadius, + ), + ) + .background(TangemTheme.colors.background.action) + .padding( + top = paddingFromTop, + bottom = 12.dp, + start = 12.dp, + end = 12.dp, + ), + ) + } + } +} + +private fun LazyListScope.listItem( + list: ImmutableList, + clickIntents: SendDestinationClickIntents, + isLast: Boolean, + isBalanceHidden: Boolean, +) { + items( + count = list.size, + key = { list[it].id }, + contentType = { list[it]::class.java }, + ) { index -> + val item = list[index] + val title = item.title.resolveReference() + + if (item.isVisible) { + ListItemWithIcon( + title = title, + subtitle = item.subtitle.orMaskWithStars(isBalanceHidden).resolveReference(), + info = item.timestamp?.resolveReference(), + subtitleEndOffset = item.subtitleEndOffset, + subtitleIconRes = item.subtitleIconRes, + onClick = { + clickIntents.onRecipientAddressValueChange( + title, + EnterAddressSource.RecentAddress, + ) + }, + isLoading = item.isLoading, + modifier = Modifier + .animateItem() + .then( + if (isLast && index == list.lastIndex) { + Modifier + .clip( + shape = RoundedCornerShape( + bottomStart = 16.dp, + bottomEnd = 16.dp, + ), + ) + } else { + Modifier + }, + ) + .background(TangemTheme.colors.background.action), + ) + } + } +} + +@Composable +private fun AnimateRecentAppearance(isVisible: Boolean, content: @Composable () -> Unit) { + AnimatedContent( + targetState = isVisible, + label = "Item Appearance Animation", + transitionSpec = { + (slideInHorizontally() + fadeIn()) + .togetherWith(slideOutVertically() + fadeOut()) + }, + ) { + if (it) { + content() + } else { + Box(modifier = Modifier.fillMaxWidth()) + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt new file mode 100644 index 0000000000..162ab6bf7b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/TextFieldWithPaste.kt @@ -0,0 +1,103 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +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.Companion.CenterEnd +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.fields.SimpleTextField +import com.tangem.core.ui.components.inputrow.inner.CrossIcon +import com.tangem.core.ui.components.inputrow.inner.PasteButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.containers.FooterContainer + +@Composable +internal fun TextFieldWithPaste( + value: String, + placeholder: TextReference, + label: TextReference, + onValueChange: (String) -> Unit, + onPasteClick: (String) -> Unit, + modifier: Modifier = Modifier, + footer: TextReference? = null, + labelStyle: TextStyle = TangemTheme.typography.body2, + error: TextReference? = null, + isError: Boolean = false, + isReadOnly: Boolean = false, + isValuePasted: Boolean = false, +) { + val (title, color) = when { + isError && error != null -> error to TangemTheme.colors.text.warning + isReadOnly -> label to TangemTheme.colors.text.tertiary + else -> label to TangemTheme.colors.text.secondary + } + val placeholderColor = if (isReadOnly) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.disabled + FooterContainer(modifier, footer) { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(end = 12.dp), + ) { + Row { + Column( + modifier = Modifier + .weight(1f) + .padding(12.dp), + ) { + Text( + text = title.resolveReference(), + style = labelStyle, + color = color, + ) + SimpleTextField( + value = value, + placeholder = placeholder, + placeholderColor = placeholderColor, + onValueChange = onValueChange, + readOnly = isReadOnly, + isValuePasted = isValuePasted, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + ) + } + AnimatedVisibility( + visible = !isReadOnly, + label = "Animate read only status change", + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier + .align(CenterVertically), + ) { + CrossIcon( + onClick = onPasteClick, + ) + } + } + AnimatedVisibility( + visible = !isReadOnly, + label = "Animate read only status change", + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier.align(CenterEnd), + ) { + PasteButton( + isPasteButtonVisible = value.isBlank(), + onClick = onPasteClick, + ) + } + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationRecipientListUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationRecipientListUM.kt new file mode 100644 index 0000000000..679928b142 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationRecipientListUM.kt @@ -0,0 +1,22 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui.state + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +@Immutable +internal data class DestinationRecipientListUM( + val id: String, + val title: TextReference = TextReference.Companion.EMPTY, + val subtitle: TextReference = TextReference.Companion.EMPTY, + val timestamp: TextReference? = null, + val subtitleEndOffset: Int = 0, + @DrawableRes val subtitleIconRes: Int? = null, + val isVisible: Boolean = true, + val isLoading: Boolean = false, + val userWalletId: UserWalletId? = null, + val network: Network? = null, + val address: String? = null, +) \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt new file mode 100644 index 0000000000..b2c3ac8b6e --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationTextFieldUM.kt @@ -0,0 +1,42 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui.state + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class DestinationTextFieldUM { + + /** Current value */ + abstract val value: String + + /** Lambda be invoked when value is been changed */ + abstract val onValueChange: (String) -> Unit + + /** Keyboard options */ + abstract val keyboardOptions: KeyboardOptions + + data class RecipientAddress( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val placeholder: TextReference, + val label: TextReference, + val isError: Boolean = false, + val error: TextReference? = null, + val isValuePasted: Boolean, + ) : DestinationTextFieldUM() + + data class RecipientMemo( + override val value: String, + override val onValueChange: (String) -> Unit, + override val keyboardOptions: KeyboardOptions, + val placeholder: TextReference, + val label: TextReference, + val isError: Boolean = false, + val error: TextReference? = null, + val disabledText: TextReference, + val isEnabled: Boolean, + val isValuePasted: Boolean, + ) : DestinationTextFieldUM() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt new file mode 100644 index 0000000000..7be35bbc1b --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationUM.kt @@ -0,0 +1,25 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui.state + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class DestinationUM { + + abstract val isPrimaryButtonEnabled: Boolean + + data class Content( + override val isPrimaryButtonEnabled: Boolean, + val addressTextField: DestinationTextFieldUM.RecipientAddress, + val memoTextField: DestinationTextFieldUM.RecipientMemo?, + val recent: ImmutableList, + val wallets: ImmutableList, + val networkName: String, + val isValidating: Boolean = false, + val isEditingDisabled: Boolean = false, + ) : DestinationUM() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : DestinationUM() +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt new file mode 100644 index 0000000000..cc7629e559 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt @@ -0,0 +1,21 @@ +package com.tangem.features.send.v2.subcomponents.destination.ui.state + +import androidx.compose.runtime.Immutable +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Available wallet to send + * + * @property name wallet name + * @property userWalletId wallet id + * @property address blockchain address + * @property cryptoCurrency selected crypto currency + */ +@Immutable +data class DestinationWalletUM( + val name: String, + val userWalletId: UserWalletId, + val address: String, + val cryptoCurrency: CryptoCurrency, +) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 788ea93ba2..0a1397271b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -4,7 +4,6 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import javax.inject.Inject @@ -35,7 +34,7 @@ internal class DefaultSendRouter @Inject constructor( override fun openQrCodeScanner(network: String) { router.push( AppRoute.QrScanning( - source = SourceType.SEND, + source = AppRoute.QrScanning.Source.SEND, networkName = network, ), ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt index f2a909d245..5cc5ccdcee 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/model/SendModel.kt @@ -36,8 +36,9 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.transaction.error.AddressValidationResult import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.error.ValidateAddressError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase @@ -116,7 +117,7 @@ internal class SendModel @Inject constructor( private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, @DelayedWork private val coroutineScope: CoroutineScope, private val innerRouter: InnerSendRouter, - private val appRouter: AppRouter, + appRouter: AppRouter, paramsContainer: ParamsContainer, validateTransactionUseCase: ValidateTransactionUseCase, getCurrencyCheckUseCase: GetCurrencyCheckUseCase, @@ -419,7 +420,10 @@ internal class SendModel @Inject constructor( return filterNot { it.isLocked } .mapNotNull { wallet -> val addresses = if (!wallet.isMultiCurrency) { - getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { + getCryptoCurrencyUseCase( + userWallet = wallet, + cryptoCurrencyId = cryptoCurrency.id.value, + ).getOrNull()?.let { if (it.network.id == cryptoCurrency.network.id) { getNetworkAddressesUseCase.invokeSync(wallet.walletId, it.network) } else { @@ -690,7 +694,7 @@ internal class SendModel @Inject constructor( }.saveIn(memoValidationJobHolder) } - private suspend fun validateAddress(value: String): Either = runCatching { + private suspend fun validateAddress(value: String): AddressValidationResult = runCatching { val maybeValidAddress = validateWalletAddressUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, @@ -699,10 +703,10 @@ internal class SendModel @Inject constructor( ) onEnteredValidAddress(maybeValidAddress.isLeft()) maybeValidAddress - }.getOrElse { ValidateAddressError.DataError(it).left() } + }.getOrElse { AddressValidation.Error.DataError(it).left() } private fun validateMemo(value: String?): Boolean { - return value?.let { validateWalletMemoUseCase(cryptoCurrency.network, it).getOrElse { false } } ?: true + return value?.let { validateWalletMemoUseCase(cryptoCurrency.network, it).isRight() } != false } private suspend fun checkIfXrpAddressValue(value: String): Boolean { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt index 3766d7c9c9..a1d6b22fb4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt @@ -19,7 +19,7 @@ internal class SendAmountPastedTriggerDismissConverter( return state.copyWrapped( isEditState = isEditState, - amountState = AmountPastedTriggerDismissTransformer().transform(amountState), + amountState = AmountPastedTriggerDismissTransformer.transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 87b25f2824..6254300fa0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -74,6 +74,7 @@ internal class SendNotificationFactory( val sendState = state.sendState ?: return@map persistentListOf() val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf() val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return@map persistentListOf() + val recipientState = state.getRecipientState(isEditState) val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() val feeValue = feeState.fee?.amount?.value.orZero() @@ -118,6 +119,7 @@ internal class SendNotificationFactory( ) addWarningNotifications( amountState = amountState, + recipientState = recipientState, feeState = feeState, sendState = sendState, sendingAmount = sendingAmount, @@ -233,6 +235,7 @@ internal class SendNotificationFactory( private suspend fun MutableList.addWarningNotifications( amountState: AmountState.Data, + recipientState: SendStates.RecipientState?, feeState: SendStates.FeeState, sendState: SendStates.SendState, sendingAmount: BigDecimal, @@ -247,8 +250,8 @@ internal class SendNotificationFactory( userWalletId = userWalletId, amount = amountValue.convertToSdkAmount(cryptoCurrencyStatus.currency), fee = feeState.fee, - memo = null, - destination = "", + memo = recipientState?.memoTextField?.value.orEmpty(), + destination = recipientState?.addressTextField?.value.orEmpty(), network = cryptoCurrencyStatus.currency.network, ).leftOrNull() } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt index c3fdbfd71b..3b0b10aaad 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/RecipientSendFactory.kt @@ -1,20 +1,18 @@ package com.tangem.features.send.impl.presentation.state.recipient -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.txhistory.models.TxHistoryItem -import com.tangem.domain.transaction.error.ValidateAddressError +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.transaction.error.AddressValidationResult import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase +import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.domain.AvailableWallet import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber internal class RecipientSendFactory( private val stateRouterProvider: Provider, @@ -66,10 +64,7 @@ internal class RecipientSendFactory( ) } - fun getOnRecipientAddressValidState( - value: String, - maybeValidAddress: Either, - ): SendUiState { + fun getOnRecipientAddressValidState(value: String, maybeValidAddress: AddressValidationResult): SendUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState @@ -78,10 +73,7 @@ internal class RecipientSendFactory( val isValidMemo = validateWalletMemoUseCase( memo = recipientState.memoTextField?.value.orEmpty(), network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e("Failed to validateWalletMemoUseCase: $it") - false - } + ).isRight() return state.copyWrapped( isEditState = isEditState, @@ -92,10 +84,10 @@ internal class RecipientSendFactory( error = maybeValidAddress.fold( ifLeft = { when (it) { - ValidateAddressError.InvalidAddress -> resourceReference( + AddressValidation.Error.InvalidAddress -> resourceReference( R.string.send_recipient_address_error, ) - ValidateAddressError.AddressInWallet -> resourceReference( + AddressValidation.Error.AddressInWallet -> resourceReference( R.string.send_error_address_same_as_wallet, ) else -> null @@ -143,10 +135,7 @@ internal class RecipientSendFactory( val isValidMemo = validateWalletMemoUseCase( memo = value, network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e("Failed to validateWalletMemoUseCase: $it") - false - } + ).isRight() return state.copyWrapped( isEditState = isEditState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index b5155c36ac..9b6c29105e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -16,6 +16,8 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -114,6 +116,7 @@ internal class StakingModel @Inject constructor( private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getActionsUseCase: GetActionsUseCase, private val getYieldUseCase: GetYieldUseCase, + private val checkAccountInitializedUseCase: CheckAccountInitializedUseCase, private val paramsInterceptorHolder: ParamsInterceptorHolder, private val shareManager: ShareManager, @DelayedWork private val coroutineScope: CoroutineScope, @@ -229,49 +232,62 @@ internal class StakingModel @Inject constructor( } override fun onNextClick(balanceState: BalanceState?) { - val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo - val noBalanceState = balanceState == null - val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data + modelScope.launch { + val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo + val noBalanceState = balanceState == null + val noYieldBalanceData = cryptoCurrencyStatus.value.yieldBalance !is YieldBalance.Data + val isAccountInitialized = checkAccountInitializedUseCase.invoke( + userWalletId = userWalletId, + network = cryptoCurrencyStatus.currency.network, + ).getOrElse { + Timber.e(it) + false + } - when { - isInitialInfoStep && noBalanceState && yield.allValidatorsFull && noYieldBalanceData -> { - stakingEventFactory.createStakingValidatorsUnavailableAlert() - return - } - isInitialInfoStep && noBalanceState -> { - val list = buildList { - SetConfirmationStateInitTransformer( - isEnter = true, - isExplicitExit = false, - balanceState = null, - cryptoCurrencyStatus = cryptoCurrencyStatus, - stakingApproval = stakingApproval, - stakingAllowance = stakingAllowance, - yieldArgs = yield.args, - ).let(::add) - if (yield.args.enter.isPartialAmountDisabled) { - ValidatorSelectChangeTransformer( - selectedValidator = yield.preferredValidators.firstOrNull(), - yield = yield, - ).let(::add) - SetAmountDataTransformer( - clickIntents = this@StakingModel, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { appCurrency }, - ).let(::add) - AmountMaxValueStateTransformer( - cryptoCurrencyStatus = cryptoCurrencyStatus, - minimumTransactionAmount = minimumTransactionAmount, - actionType = uiState.value.actionType, - yield = yield, - ).let(::add) - } + when { + isInitialInfoStep && noBalanceState && yield.allValidatorsFull && noYieldBalanceData -> { + stakingEventFactory.createStakingValidatorsUnavailableAlert() + return@launch + } + isInitialInfoStep && noBalanceState && !isAccountInitialized -> { + stakingEventFactory.createInitializeAccountAlert() + return@launch + } + isInitialInfoStep && noBalanceState -> { + val list = buildList { + SetConfirmationStateInitTransformer( + isEnter = true, + isExplicitExit = false, + balanceState = null, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingApproval = stakingApproval, + stakingAllowance = stakingAllowance, + yieldArgs = yield.args, + ).let(::add) + if (yield.args.enter.isPartialAmountDisabled) { + ValidatorSelectChangeTransformer( + selectedValidator = yield.preferredValidators.firstOrNull(), + yield = yield, + ).let(::add) + SetAmountDataTransformer( + clickIntents = this@StakingModel, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + ).let(::add) + AmountMaxValueStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + actionType = uiState.value.actionType, + yield = yield, + ).let(::add) + } + } + stateController.updateAll(*list.toTypedArray()) } - stateController.updateAll(*list.toTypedArray()) } + stakingStateRouter.onNextClick() } - stakingStateRouter.onNextClick() } override fun getFee() { @@ -497,19 +513,34 @@ internal class StakingModel @Inject constructor( analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards) val rewardsValidators = stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data - val rewards = rewardsValidators?.rewards - if (rewards != null && rewards.isSingleItem()) { - onActiveStake(rewards.first()) - } else { - analyticsEventHandler.send( - StakingAnalyticsEvent.ButtonValidator( - source = StakeScreenSource.Info, - ), + + val initialInfoState = stateController.value.initialInfoState as? StakingStates.InitialInfoState.Data + val yieldBalance = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data + val rewardBlockType = yieldBalance?.reward?.rewardBlockType + val rewardPendingActionConstraints = yieldBalance?.reward?.rewardConstraints + + if (rewardBlockType == RewardBlockType.RewardsRequirementsError) { + stakingEventFactory.createStakingRewardsMinimumRequirementsErrorAlert( + cryptoCurrencyName = cryptoCurrencyStatus.currency.name, + cryptoAmountValue = rewardPendingActionConstraints?.amountArg?.minimum?.format { + crypto(cryptoCurrencyStatus.currency) + }.orEmpty(), ) - stateController.update { - value.copy(actionType = StakingActionCommonType.Pending.Rewards) + } else { + val rewards = rewardsValidators?.rewards + if (rewards != null && rewards.isSingleItem()) { + onActiveStake(rewards.first()) + } else { + analyticsEventHandler.send( + StakingAnalyticsEvent.ButtonValidator( + source = StakeScreenSource.Info, + ), + ) + stateController.update { + value.copy(actionType = StakingActionCommonType.Pending.Rewards) + } + stakingStateRouter.showRewardsValidators() } - stakingStateRouter.showRewardsValidators() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index f795c3195d..db102751d3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -2,18 +2,14 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.RewardBlockType -import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.* import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal +@Immutable internal sealed class InnerYieldBalanceState { data class Data( - val rewardsCrypto: String, - val rewardsFiat: String, - val rewardBlockType: RewardBlockType, + val reward: YieldReward, val isActionable: Boolean, val balances: ImmutableList, ) : InnerYieldBalanceState() @@ -37,4 +33,12 @@ internal data class BalanceState( val validator: Yield.Validator?, val pendingActions: ImmutableList, val isPending: Boolean, +) + +@Immutable +internal data class YieldReward( + val rewardsCrypto: String, + val rewardsFiat: String, + val rewardBlockType: RewardBlockType, + val rewardConstraints: PendingActionConstraints?, ) \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index ed5cb2cdad..02a6356ad2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -6,8 +6,10 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.YieldReward import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.Provider @@ -33,7 +35,8 @@ internal class YieldBalancesConverter( val balanceToShowItems = balancesToShowProvider() return if (yieldBalance is YieldBalance.Data || balanceToShowItems.any { it.isPending }) { - val cryptoRewardsValue = (yieldBalance as? YieldBalance.Data)?.getRewardStakingBalance() + val yieldBalance = yieldBalance as? YieldBalance.Data + val cryptoRewardsValue = yieldBalance?.getRewardStakingBalance() val fiatRate = cryptoCurrencyStatus.value.fiatRate val fiatRewardsValue = if (fiatRate != null && cryptoRewardsValue != null) { @@ -41,17 +44,25 @@ internal class YieldBalancesConverter( } else { null } - val (type, isActionable) = getRewardBlockType() + val type = getRewardBlockType() + val pendingRewardsConstraints = yieldBalance?.balance?.items + ?.firstOrNull { it.type == BalanceType.REWARDS } + ?.pendingActionsConstraints + ?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS } + InnerYieldBalanceState.Data( - rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, - rewardsFiat = fiatRewardsValue.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - }, - rewardBlockType = type, - isActionable = isActionable, + reward = YieldReward( + rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, + rewardsFiat = fiatRewardsValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + rewardBlockType = type, + rewardConstraints = pendingRewardsConstraints, + ), + isActionable = type.isActionable, balances = balanceToShowItems.mapBalances(), ) } else { @@ -66,7 +77,7 @@ internal class YieldBalancesConverter( .sortedBy { it.type.order } .toPersistentList() - private fun getRewardBlockType(): Pair { + private fun getRewardBlockType(): RewardBlockType { val blockchainId = cryptoCurrencyStatus.currency.network.id.value val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data val rewards = yieldBalance?.balance?.items @@ -76,9 +87,10 @@ internal class YieldBalancesConverter( val isRewardsClaimable = rewards?.isNotEmpty() == true return when { - isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable to false - isRewardsClaimable -> RewardBlockType.Rewards to isActionable - else -> RewardBlockType.NoRewards to false + isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable + isRewardsClaimable && isActionable -> RewardBlockType.Rewards + isRewardsClaimable && !isActionable -> RewardBlockType.RewardsRequirementsError + else -> RewardBlockType.NoRewards } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt index 2e6c9aebf2..661b19506c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -61,4 +61,24 @@ internal sealed class StakingAlertUM : AlertUM { ) override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) } + + data object InitializeAccount : StakingAlertUM() { + override val onConfirmClick: (() -> Unit)? = null + override val title: TextReference = resourceReference(id = R.string.staking_error_no_validators_title) + override val message: TextReference = resourceReference(id = R.string.staking_notification_ton_activate_account) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + } + + data class RewardsMinimumRequirementsError( + val cryptoCurrencyName: String, + val cryptoAmountValue: String, + ) : StakingAlertUM() { + override val onConfirmClick: (() -> Unit)? = null + override val title: TextReference? = null + override val message: TextReference = resourceReference( + id = R.string.staking_details_min_rewards_notification, + formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue), + ) + override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt index 6d54195e71..bb1b446118 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -46,4 +46,20 @@ internal class StakingEventFactory( val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.ValidatorsUnavailable) stateController.updateEvent(alert) } + + fun createInitializeAccountAlert() { + val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.InitializeAccount) + stateController.updateEvent(alert) + } + + fun createStakingRewardsMinimumRequirementsErrorAlert(cryptoCurrencyName: String, cryptoAmountValue: String) { + stateController.updateEvent( + StakingEvent.ShowAlert( + alert = StakingAlertUM.RewardsMinimumRequirementsError( + cryptoCurrencyName = cryptoCurrencyName, + cryptoAmountValue = cryptoAmountValue, + ), + ), + ) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 0aa07c6011..1c9a799920 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -1,9 +1,9 @@ package com.tangem.features.staking.impl.presentation.state.previewdata +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.Yield @@ -11,6 +11,7 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.YieldReward import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { @@ -63,9 +64,12 @@ internal object InitialStakingStatePreview { val stateWithYield = defaultState.copy( yieldBalance = InnerYieldBalanceState.Data( - rewardsFiat = "100 $", - rewardsCrypto = "100 SOL", - rewardBlockType = RewardBlockType.RewardUnavailable, + reward = YieldReward( + rewardsFiat = "100 $", + rewardsCrypto = "100 SOL", + rewardBlockType = RewardBlockType.RewardUnavailable, + rewardConstraints = null, + ), isActionable = true, balances = persistentListOf( BalanceState( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt index 4e5592683d..6e596ae2ec 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInitTransformer.kt @@ -43,28 +43,7 @@ internal class SetConfirmationStateInitTransformer( @Suppress("CyclomaticComplexMethod") override fun transform(prevState: StakingUiState): StakingUiState { - val actionType = when { - isEnter -> StakingActionCommonType.Enter( - skipEnterAmount = yieldArgs.enter.isPartialAmountDisabled, - ) - isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartiallyUnstakeDisabled(prevState)) - else -> when (pendingAction?.type) { - StakingActionType.STAKE -> StakingActionCommonType.Pending.Stake( - skipEnterAmount = yieldArgs.enter.isPartialAmountDisabled, - ) - StakingActionType.UNSTAKE -> StakingActionCommonType.Exit( - partiallyUnstakeDisabled = isPartiallyUnstakeDisabled(prevState), - ) - StakingActionType.CLAIM_REWARDS, - StakingActionType.RESTAKE_REWARDS, - -> StakingActionCommonType.Pending.Rewards - StakingActionType.VOTE_LOCKED, - StakingActionType.RESTAKE, - -> StakingActionCommonType.Pending.Restake - else -> StakingActionCommonType.Pending.Other - } - } - + val actionType = getActionType(prevState) return prevState.copy( actionType = actionType, balanceState = balanceState, @@ -86,6 +65,26 @@ internal class SetConfirmationStateInitTransformer( ) } + private fun getActionType(prevState: StakingUiState): StakingActionCommonType { + val isPartialEnterAmountDisabled = yieldArgs.enter.isPartialAmountDisabled + val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState) + return when { + isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled) + isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartialExitAmountDisabled) + else -> when (pendingAction?.type) { + StakingActionType.STAKE -> StakingActionCommonType.Pending.Stake(isPartialEnterAmountDisabled) + StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(isPartialExitAmountDisabled) + StakingActionType.CLAIM_REWARDS, + StakingActionType.RESTAKE_REWARDS, + -> StakingActionCommonType.Pending.Rewards + StakingActionType.VOTE_LOCKED, + StakingActionType.RESTAKE, + -> StakingActionCommonType.Pending.Restake + else -> StakingActionCommonType.Pending.Other + } + } + } + private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean { val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId) val isValidatorPreferred = balanceState?.validator?.preferred == true diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt index b2d7752952..fa01ef2dec 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt @@ -8,7 +8,7 @@ internal class AmountPasteDismissStateTransformer : Transformer override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - amountState = AmountPastedTriggerDismissTransformer().transform(prevState.amountState), + amountState = AmountPastedTriggerDismissTransformer.transform(prevState.amountState), ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index f82dff2923..2ac8c6e2ef 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -98,9 +98,7 @@ internal class AddStakingNotificationsTransformer( onReload = prevState.clickIntents::getFee, feeValue = feeValue, ) - addStakingErrorNotifications( - stakingError = stakingError, - ) + addStakingErrorNotifications(stakingError = stakingError) // warnings addWarningNotifications( prevState = prevState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index 480126eb20..f9f71dd51e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -38,7 +38,7 @@ internal class ValidatorSelectChangeTransformer( chosenValidator = selectedValidator ?: yield.preferredValidators.first(), availableValidators = filteredValidators, isPrimaryButtonEnabled = true, - isClickable = true, + isClickable = yield.preferredValidators.size > 1, activeValidator = activeValidator, isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked, ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index a815b1b370..5e8683d6a9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.Density import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.components.list.roundedListWithDividersItems @@ -38,19 +39,18 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub -import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.extensions.orZero @@ -214,14 +214,17 @@ private fun StakingRewardBlock( onRewardsClick: () -> Unit, isBalanceHidden: Boolean, ) { - val (text, textColor) = when (yieldBalanceState.rewardBlockType) { - RewardBlockType.Rewards -> { + val reward = yieldBalanceState.reward + val (text, textColor) = when (yieldBalanceState.reward.rewardBlockType) { + RewardBlockType.RewardsRequirementsError, + RewardBlockType.Rewards, + -> { annotatedReference { - append(yieldBalanceState.rewardsFiat.orMaskWithStars(isBalanceHidden)) + append(reward.rewardsFiat.orMaskWithStars(isBalanceHidden)) appendSpace() append(DOT) appendSpace() - append(yieldBalanceState.rewardsCrypto.orMaskWithStars(isBalanceHidden)) + append(reward.rewardsCrypto.orMaskWithStars(isBalanceHidden)) } to TangemTheme.colors.text.primary1 } RewardBlockType.RewardUnavailable -> { @@ -232,7 +235,7 @@ private fun StakingRewardBlock( resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary } } - val isShowIcon = yieldBalanceState.rewardBlockType == RewardBlockType.Rewards && yieldBalanceState.isActionable + val isShowIcon = reward.rewardBlockType == RewardBlockType.Rewards && yieldBalanceState.isActionable InputRowDefault( title = resourceReference(R.string.staking_rewards), text = text, diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt index f43c34f8ac..e0fe076fab 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt @@ -1,5 +1,3 @@ package com.tangem.features.swap -interface SwapFeatureToggles { - val isPromoStoriesEnabled: Boolean -} \ No newline at end of file +interface SwapFeatureToggles \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt index cd341751d8..c202111fb6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt @@ -1,11 +1,5 @@ package com.tangem.feature.swap -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.swap.SwapFeatureToggles -internal class DefaultSwapFeatureToggles( - private val featureToggles: FeatureTogglesManager, -) : SwapFeatureToggles { - override val isPromoStoriesEnabled: Boolean - get() = featureToggles.isFeatureEnabled("SWAP_STORIES_ENABLED") -} \ No newline at end of file +internal class DefaultSwapFeatureToggles : SwapFeatureToggles \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 68d9795026..0f2e700cd8 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -117,6 +117,7 @@ sealed class SwapEvents( "Error Code" to errorCode.toString(), ), ) + // TODO parameters // region Promo activity data class ChangellyActivity( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt index 5b732e9051..990f8c8ec9 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/di/SwapFeatureModule.kt @@ -1,6 +1,5 @@ package com.tangem.feature.swap.di -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.feature.swap.DefaultSwapComponent import com.tangem.feature.swap.DefaultSwapFeatureToggles import com.tangem.features.swap.SwapComponent @@ -18,8 +17,8 @@ internal object SwapFeatureModule { @Provides @Singleton - fun provideSwapFeatureToggles(featureToggles: FeatureTogglesManager): SwapFeatureToggles { - return DefaultSwapFeatureToggles(featureToggles) + fun provideSwapFeatureToggles(): SwapFeatureToggles { + return DefaultSwapFeatureToggles() } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 1728183620..ea78698ede 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -9,6 +9,7 @@ import arrow.core.getOrElse import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull +import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -55,7 +56,6 @@ import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.features.swap.SwapComponent -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero @@ -82,6 +82,7 @@ internal class SwapModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val blockchainInteractor: BlockchainInteractor, private val analyticsEventHandler: AnalyticsEventHandler, + private val analyticsErrorEventHandler: AnalyticsErrorHandler, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, @@ -94,7 +95,6 @@ internal class SwapModel @Inject constructor( private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val featureToggles: SwapFeatureToggles, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, swapInteractorFactory: SwapInteractor.Factory, private val urlOpener: UrlOpener, @@ -164,10 +164,8 @@ internal class SwapModel @Inject constructor( init { modelScope.launch { - if (featureToggles.isPromoStoriesEnabled) { - initStories() - swapRouter.openScreen(SwapNavScreen.PromoStories) - } + initStories() + swapRouter.openScreen(SwapNavScreen.PromoStories) } modelScope.launch(dispatchers.io) { val fromStatus = getCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom.id).getOrNull() @@ -483,7 +481,7 @@ internal class SwapModel @Inject constructor( val receiveToken = dataState.toCryptoCurrency?.currency?.let { "${it.network.backendId}:${it.symbol}" } - analyticsEventHandler.send( + analyticsErrorEventHandler.sendErrorEvent( SwapEvents.NoticeProviderError( sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}", receiveToken = receiveToken ?: "", 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 index 01d0aec7ba..7ce2014bc1 100644 --- 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 @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails import androidx.compose.ui.graphics.Color import androidx.paging.PagingData import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType @@ -27,7 +28,7 @@ internal object TokenDetailsPreviewData { onBackClick = {}, tokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( persistentListOf( - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = { }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt index 4926ba0b4a..c5299ec1d5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt @@ -1,14 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import kotlinx.collections.immutable.ImmutableList -internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList) { - data class MenuItem( - val title: TextReference, - val textColorProvider: @Composable () -> Color, - val onClick: () -> Unit, - ) -} \ No newline at end of file +internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index d4f23daa2d..124447b0a9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.getOrElse import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference @@ -14,9 +15,9 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.converter.Converter @@ -86,7 +87,7 @@ internal class TokenDetailsSkeletonStateConverter( private fun createMenu(cryptoCurrency: CryptoCurrency): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig( items = buildList { addGenerateXPubMenuItem(cryptoCurrency) - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = clickIntents::onHideClick, @@ -94,9 +95,7 @@ internal class TokenDetailsSkeletonStateConverter( }.toImmutableList(), ) - private fun MutableList.addGenerateXPubMenuItem( - cryptoCurrency: CryptoCurrency, - ) { + private fun MutableList.addGenerateXPubMenuItem(cryptoCurrency: CryptoCurrency) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return val isBitcoin = isBitcoin(cryptoCurrency.network.id.value) @@ -107,7 +106,7 @@ internal class TokenDetailsSkeletonStateConverter( if (isBitcoin && hasDerivations) { add( - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = resourceReference(R.string.token_details_generate_xpub), textColorProvider = { TangemTheme.colors.text.primary1 }, onClick = clickIntents::onGenerateExtendedKey, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index ef4a99cb22..72d2697d5e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -155,7 +155,11 @@ internal class TokenDetailsStakingInfoConverter( } return when (rewardBlockType) { - RewardBlockType.Rewards -> resourceReference( + RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim) + RewardBlockType.RewardUnavailable -> TextReference.EMPTY + RewardBlockType.RewardsRequirementsError, + RewardBlockType.Rewards, + -> resourceReference( R.string.staking_details_rewards_to_claim, wrappedList( stakingRewardAmount.format { @@ -166,8 +170,6 @@ internal class TokenDetailsStakingInfoConverter( }, ), ) - RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim) - RewardBlockType.RewardUnavailable -> TextReference.EMPTY } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index d360a8883f..b69153ce42 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference @@ -376,13 +377,13 @@ internal class TokenDetailsStateFactory( return copy( items = buildList { if (isSupported && hasDerivations) { - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = resourceReference(R.string.token_details_generate_xpub), textColorProvider = { TangemTheme.colors.text.primary1 }, onClick = clickIntents::onGenerateExtendedKey, ).let(::add) } - TokenDetailsAppBarMenuConfig.MenuItem( + TangemDropdownMenuItem( title = TextReference.Res(id = R.string.token_details_hide_token), textColorProvider = { TangemTheme.colors.text.warning }, onClick = clickIntents::onHideClick, 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 index 3ea60b1140..648e7aee48 100644 --- 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 @@ -3,8 +3,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.padding import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -14,14 +12,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig import com.tangem.features.tokendetails.impl.R @@ -58,10 +54,9 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { expanded = showDropdownMenu, modifier = Modifier.background(TangemTheme.colors.background.primary), onDismissRequest = { showDropdownMenu = false }, - offset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)), content = { config.tokenDetailsAppBarMenuConfig?.items?.fastForEach { - AppBarDropdownItem( + TangemDropdownItem( item = it, dismissParent = { showDropdownMenu = false }, ) @@ -78,42 +73,6 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) { ) } -@Suppress("ComposableEventParameterNaming") -@Composable -private fun AppBarDropdownItem( - item: TokenDetailsAppBarMenuConfig.MenuItem, - dismissParent: () -> Unit, - modifier: Modifier = Modifier, -) { - Text( - modifier = modifier - .clickable { - dismissParent() - item.onClick() - } - .padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16), - text = item.title.resolveReference(), - style = TangemTheme.typography.body1.copy(color = item.textColorProvider()), - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_TokenDetailsAppBarDropdownItem() { - TangemThemePreview { - AppBarDropdownItem( - modifier = Modifier.background(TangemTheme.colors.background.primary), - dismissParent = {}, - item = TokenDetailsAppBarMenuConfig.MenuItem( - title = TextReference.Res(id = R.string.token_details_hide_token), - textColorProvider = { TangemTheme.colors.text.warning }, - onClick = { }, - ), - ) - } -} - @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt index 99b401cfda..51c265394a 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/analytics/Settings.kt @@ -6,8 +6,7 @@ internal sealed class Settings( category: String = "Settings", event: String, params: Map = mapOf(), - error: Throwable? = null, -) : AnalyticsEvent(category, event, params, error) { +) : AnalyticsEvent(category, event, params) { data object ButtonCreateBackup : Settings(event = "Button - Create Backup") diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 52f23e9529..cc48296697 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -164,7 +164,7 @@ internal class WalletSettingsModel @Inject constructor( router.push( AppRoute.Onboarding( scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.AddBackup, + mode = AppRoute.Onboarding.Mode.AddBackupWallet1, ), ) } else { diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt index 97d2d5800f..8621c8c0d1 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -5,7 +5,4 @@ package com.tangem.features.wallet.featuretoggles * [REDACTED_AUTHOR] */ -interface WalletFeatureToggles { - - val isMainActionButtonsEnabled: Boolean -} \ No newline at end of file +interface WalletFeatureToggles \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index f786056d52..8416888824 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -90,6 +90,8 @@ dependencies { implementation(projects.domain.onramp) implementation(projects.domain.promo) implementation(projects.domain.promo.models) + implementation(projects.domain.nft) + implementation(projects.domain.nft.models) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) @@ -108,6 +110,7 @@ dependencies { implementation(projects.features.wallet.api) implementation(projects.features.walletSettings.api) implementation(projects.features.biometry.api) + implementation(projects.features.nft.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 8f748fd22b..80175b7ddf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -10,9 +10,11 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -38,9 +40,9 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.BiometryFeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -73,9 +75,10 @@ internal class WalletModel @Inject constructor( private val walletImageResolver: WalletImageResolver, private val tokenListStore: MultiWalletTokenListStore, private val onrampStatusFactory: OnrampStatusFactory, - private val walletFeatureToggles: WalletFeatureToggles, private val biometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventsHandler: AnalyticsEventHandler, + private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, + private val walletsRepository: WalletsRepository, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -86,6 +89,7 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() private val expressStatusJobHolder = JobHolder() + private val walletsNFTsUpdateJobHolder = JobHolder() private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -103,6 +107,7 @@ internal class WalletModel @Inject constructor( subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() subscribeOnExpressTransactionsUpdates() + subscribeOnNFTUpdates() clickIntents.initialize(innerWalletRouter, modelScope) } @@ -160,7 +165,10 @@ internal class WalletModel @Inject constructor( .conflate() .distinctUntilChanged() .map { - walletsUpdateActionResolver.resolve(wallets = it, currentState = stateHolder.value) + walletsUpdateActionResolver.resolve( + wallets = it, + currentState = stateHolder.value, + ) } .onEach(::updateWallets) .flowOn(dispatchers.main) @@ -256,6 +264,29 @@ internal class WalletModel @Inject constructor( }.saveIn(expressStatusJobHolder) } + @OptIn(ExperimentalCoroutinesApi::class) + private fun subscribeOnNFTUpdates() { + getWalletsUseCase() + .conflate() + .distinctUntilChanged() + .flatMapLatest { wallets -> + wallets + .map { wallet -> + walletsRepository.nftEnabledStatus(wallet.walletId) + .distinctUntilChanged() + .onEach { nftEnabled -> + if (nftEnabled) { + fetchNFTCollectionsUseCase.invoke(wallet.walletId) + } + } + } + .merge() + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + .saveIn(walletsNFTsUpdateJobHolder) + } + private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) @@ -313,7 +344,6 @@ internal class WalletModel @Inject constructor( wallets = action.wallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ), ) @@ -354,7 +384,6 @@ internal class WalletModel @Inject constructor( newUserWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ), ) } @@ -371,7 +400,6 @@ internal class WalletModel @Inject constructor( userWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ), ) @@ -382,7 +410,7 @@ internal class WalletModel @Inject constructor( } } - private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { + private fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { walletScreenContentLoader.cancel(action.deletedWalletId) tokenListStore.remove(action.deletedWalletId) @@ -427,7 +455,6 @@ internal class WalletModel @Inject constructor( unlockedWallets = action.unlockedWallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 5594553aab..35cf3a44cd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -231,7 +231,10 @@ internal class WalletsUpdateActionResolver @Inject constructor( * @property prevWalletId previous selected wallet id * @property selectedWallet selected wallet */ - data class ReinitializeWallet(val prevWalletId: UserWalletId, val selectedWallet: UserWallet) : Action() { + data class ReinitializeWallet( + val prevWalletId: UserWalletId, + val selectedWallet: UserWallet, + ) : Action() { override fun toString(): String { return "ReinitializeWallet(prevWalletId = $prevWalletId, selectedWallet = ${selectedWallet.walletId})" diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt index 4a0281d436..8fe27fb45f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/VisaWalletIntents.kt @@ -2,29 +2,19 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.tokenreceive.mapToAddressModels -import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.visa.GetVisaCurrencyUseCase import com.tangem.domain.visa.GetVisaTxDetailsUseCase -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.BalancesAndLimitsBottomSheetConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.VisaTxDetailsBottomSheetConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject internal interface VisaWalletIntents { - fun onDepositClick() - fun onBalancesAndLimitsClick() fun onVisaTransactionClick(id: String) @@ -36,7 +26,6 @@ internal interface VisaWalletIntents { internal class VisaWalletIntentsImplementor @Inject constructor( private val stateController: WalletStateController, private val eventSender: WalletEventSender, - private val getCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val getVisaCurrencyUseCase: GetVisaCurrencyUseCase, private val getVisaTxDetailsUseCase: GetVisaTxDetailsUseCase, private val dispatchers: CoroutineDispatcherProvider, @@ -46,38 +35,6 @@ internal class VisaWalletIntentsImplementor @Inject constructor( BalancesAndLimitsBottomSheetConverter(eventSender) } - override fun onDepositClick() { - val userWalletId = stateController.getSelectedWalletId() - - modelScope.launch(dispatchers.main) { - val currencyStatus = getPrimaryCurrencyStatus(userWalletId) ?: return@launch - - createReceiveBottomSheetContent(currencyStatus)?.let { content -> - stateController.showBottomSheet(content) - } - } - } - - private fun createReceiveBottomSheetContent(currencyStatus: CryptoCurrencyStatus): TangemBottomSheetConfigContent? { - val currency = currencyStatus.currency - val addresses = currencyStatus.value.networkAddress?.availableAddresses - - if (addresses == null) { - Timber.e("Addresses should not be null") - return null - } - - return TokenReceiveBottomSheetConfig( - name = currency.name, - symbol = currency.symbol, - network = currency.network.name, - addresses = addresses.mapToAddressModels(currency).toImmutableList(), - showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = { /* no-op */ }, - onShareClick = { /* no-op */ }, - ) - } - override fun onBalancesAndLimitsClick() { modelScope.launch(dispatchers.main) { val userWalletId = stateController.getSelectedWalletId() @@ -95,14 +52,6 @@ internal class VisaWalletIntentsImplementor @Inject constructor( } } - private suspend fun getPrimaryCurrencyStatus(userWalletId: UserWalletId): CryptoCurrencyStatus? { - return getCurrencyStatusUseCase(userWalletId) - .getOrElse { - Timber.e("Failed to get primary currency $it") - null - } - } - override fun onVisaTransactionClick(id: String) { modelScope.launch(dispatchers.main) { val userWalletId = stateController.getSelectedWalletId() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index e84eccf060..ee324b252e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -5,6 +5,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase @@ -14,6 +15,7 @@ import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -50,6 +52,8 @@ internal class WalletClickIntents @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val onrampFeatureToggles: OnrampFeatureToggles, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, + private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, + private val isWalletNFTEnabledSyncUseCase: IsWalletNFTEnabledSyncUseCase, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, @@ -137,8 +141,10 @@ internal class WalletClickIntents @Inject constructor( async { rampStateManager.fetchSellServiceData() }.let(::add) - if (onrampFeatureToggles.isHotTokensEnabled) { - async { fetchHotCryptoUseCase() }.let(::add) + async { fetchHotCryptoUseCase() }.let(::add) + + if (isWalletNFTEnabledSyncUseCase.invoke(userWallet.walletId)) { + async { fetchNFTCollectionsUseCase(userWalletId = userWallet.walletId) }.let(::add) } } .awaitAll() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index d96b0968ad..6e1ec6a43d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -56,7 +56,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensLis import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.features.onramp.OnrampFeatureToggles -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -125,7 +124,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val appRouter: AppRouter, private val rampStateManager: RampStateManager, private val onrampFeatureToggles: OnrampFeatureToggles, - private val swapFeatureToggles: SwapFeatureToggles, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -632,9 +630,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } private suspend fun getSwapRoute(targetRoute: AppRoute): AppRoute { - val isSwapStoriesEnabled = swapFeatureToggles.isPromoStoriesEnabled val maybeSwapStories = getStoryContentUseCase.invokeSync(StoryContentIds.STORY_FIRST_TIME_SWAP.id) - val showSwapStories = maybeSwapStories.getOrNull() != null && isSwapStoriesEnabled + val showSwapStories = maybeSwapStories.getOrNull() != null return if (showSwapStories) { AppRoute.Stories( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt index c3aba027cc..f283fadd39 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -1,13 +1,6 @@ package com.tangem.feature.wallet.featuretoggles -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import javax.inject.Inject -internal class DefaultWalletFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) : WalletFeatureToggles { - - override val isMainActionButtonsEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "MAIN_ACTION_BUTTONS_ENABLED") -} \ No newline at end of file +internal class DefaultWalletFeatureToggles @Inject constructor() : WalletFeatureToggles \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 4f8cb9bdfd..b8cf556b05 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -120,6 +120,12 @@ internal object WalletScreenPreviewData { ), bottomSheetConfig = null, tokensListState = textContentTokensState, + nftState = WalletNFTItemUM.Content( + previews = persistentListOf(WalletNFTItemUM.Content.CollectionPreview.Image("img1")), + collectionsCount = 1, + assetsCount = 3, + isFlickering = false, + ), ) } 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 967c19ebed..05a688f302 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 @@ -54,7 +54,7 @@ internal class DefaultWalletRouter @Inject constructor( AppRoute.Onboarding( scanResponse = scanResponse, mode = if (continueBackup) { - AppRoute.Onboarding.Mode.AddBackup + AppRoute.Onboarding.Mode.AddBackupWallet1 } else { AppRoute.Onboarding.Mode.Onboarding }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index c39efabf74..a07053ba04 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -10,8 +10,7 @@ sealed class WalletScreenAnalyticsEvent { sealed class Basic( event: String, params: Map = mapOf(), - error: Throwable? = null, - ) : AnalyticsEvent(category = "Basic", event = event, params = params, error = error) { + ) : AnalyticsEvent(category = "Basic", event = event, params = params) { class WalletToppedUp(userWalletId: UserWalletId, walletType: AnalyticsParam.WalletType) : Basic( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt new file mode 100644 index 0000000000..e931183ffe --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/IsWalletNFTEnabledSyncUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.nft.NFTFeatureToggles +import kotlinx.coroutines.flow.firstOrNull + +class IsWalletNFTEnabledSyncUseCase( + private val walletsRepository: WalletsRepository, + private val nftFeatureToggles: NFTFeatureToggles, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Boolean = if (nftFeatureToggles.isNFTEnabled) { + walletsRepository + .nftEnabledStatuses() + .firstOrNull() + ?.let { it[userWalletId] } + ?: false + } else { + false + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt index a487bd79bd..136f5c811d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt @@ -8,7 +8,6 @@ import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.wallets.builder.UserWalletBuilder -import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.SaveWalletUseCase import javax.inject.Inject @@ -16,7 +15,7 @@ import javax.inject.Inject internal class ScanCardToUnlockWalletClickHandler @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, - private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val userWalletBuilderFactory: UserWalletBuilder.Factory, ) { private var scanFailsCounter = 0 @@ -34,7 +33,7 @@ internal class ScanCardToUnlockWalletClickHandler @Inject constructor( scanFailsCounter = 0 // If card's public key is null then user wallet will be null - val scannedWallet = UserWalletBuilder(result.data, generateWalletNameUseCase).build() + val scannedWallet = userWalletBuilderFactory.create(scanResponse = result.data).build() ensure(walletId == scannedWallet?.walletId) { ScanCardToUnlockWalletError.WrongCardIsScanned diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 6fe44142e1..eecab67075 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -40,7 +40,7 @@ internal enum class Wallet2CobrandImage( BitcoinGold( cards2ResId = R.drawable.ill_bitcoingold_card2_120_106, cards3ResId = R.drawable.ill_bitcoingold_card3_120_106, - batchIds = setOf("AF71"), + batchIds = setOf("AF71", "AF990016", "AF990009"), ), BitcoinPizzaDay( @@ -169,6 +169,12 @@ internal enum class Wallet2CobrandImage( batchIds = setOf("AF95"), ), + Keiro( + cards2ResId = R.drawable.ill_keiro_card2_120_106, + cards3ResId = R.drawable.ill_keiro_card3_120_106, + batchIds = setOf("BB000017"), + ), + KishuInu( cards2ResId = R.drawable.ill_kishu_inu_card2_120_106, cards3ResId = R.drawable.ill_kishu_inu_card3_120_106, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 402b03dec0..238eecd063 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -3,10 +3,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -21,7 +23,7 @@ import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActi import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber -import com.tangem.features.swap.SwapFeatureToggles +import com.tangem.features.nft.NFTFeatureToggles @Suppress("LongParameterList") @ModelScoped @@ -34,14 +36,16 @@ internal class MultiWalletContentLoader( private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val tokenListStore: MultiWalletTokenListStore, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, + private val nftFeatureToggles: NFTFeatureToggles, + private val walletsRepository: WalletsRepository, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -58,6 +62,15 @@ internal class MultiWalletContentLoader( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, deepLinksRegistry = deepLinksRegistry, ).let(::add) + if (nftFeatureToggles.isNFTEnabled) { + WalletNFTListSubscriber( + userWallet = userWallet, + getNFTCollectionsUseCase = getNFTCollectionsUseCase, + stateHolder = stateHolder, + walletsRepository = walletsRepository, + clickIntents = clickIntents, + ).let(::add) + } MultiWalletWarningsSubscriber( userWallet = userWallet, stateHolder = stateHolder, @@ -66,13 +79,11 @@ internal class MultiWalletContentLoader( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, walletWarningsSingleEventSender = walletWarningsSingleEventSender, ).let(::add) - if (swapFeatureToggles.isPromoStoriesEnabled) { - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - } + MultiWalletActionButtonsSubscriber( + userWallet = userWallet, + stateHolder = stateHolder, + getStoryContentUseCase = getStoryContentUseCase, + ).let(::add) WalletDropDownItemsSubscriber( stateHolder = stateHolder, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 7bc706075d..0f4e9d37b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -3,10 +3,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.nft.GetNFTCollectionsUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -16,7 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.features.swap.SwapFeatureToggles +import com.tangem.features.nft.NFTFeatureToggles import javax.inject.Inject @Suppress("LongParameterList") @@ -34,8 +36,10 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, + private val nftFeatureToggles: NFTFeatureToggles, + private val walletsRepository: WalletsRepository, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -54,8 +58,10 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - swapFeatureToggles = swapFeatureToggles, deepLinksRegistry = deepLinksRegistry, + nftFeatureToggles = nftFeatureToggles, + walletsRepository = walletsRepository, + getNFTCollectionsUseCase = getNFTCollectionsUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index fd9277c21a..109e4deec2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -15,7 +15,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.swap.SwapFeatureToggles @Suppress("LongParameterList") internal class SingleWalletWithTokenContentLoader( @@ -32,7 +31,6 @@ internal class SingleWalletWithTokenContentLoader( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, ) : WalletContentLoader(id = userWallet.walletId) { @@ -57,13 +55,11 @@ internal class SingleWalletWithTokenContentLoader( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, walletWarningsSingleEventSender = walletWarningsSingleEventSender, ).let(::add) - if (swapFeatureToggles.isPromoStoriesEnabled) { - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - } + MultiWalletActionButtonsSubscriber( + userWallet = userWallet, + stateHolder = stateHolder, + getStoryContentUseCase = getStoryContentUseCase, + ).let(::add) WalletDropDownItemsSubscriber( stateHolder = stateHolder, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 1a7e15d40d..829bb4b3d6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -15,7 +15,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.swap.SwapFeatureToggles import javax.inject.Inject // TODO: Refactor @@ -33,7 +32,6 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, - private val swapFeatureToggles: SwapFeatureToggles, private val deepLinksRegistry: DeepLinksRegistry, ) { @@ -52,7 +50,6 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, getStoryContentUseCase = getStoryContentUseCase, shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - swapFeatureToggles = swapFeatureToggles, deepLinksRegistry = deepLinksRegistry, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index cf9c56b44c..e9771676b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/DepositButtonState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/DepositButtonState.kt deleted file mode 100644 index 9a490bf52e..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/DepositButtonState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -internal data class DepositButtonState( - val isEnabled: Boolean, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 42b4839fc4..cd4ed81df9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -22,6 +22,7 @@ internal sealed interface WalletState : WalletStateHolder { sealed class MultiCurrency : WalletState { abstract val tokensListState: WalletTokensListState + abstract val nftState: WalletNFTItemUM data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -30,6 +31,7 @@ internal sealed interface WalletState : WalletStateHolder { override val warnings: ImmutableList, override val bottomSheetConfig: TangemBottomSheetConfig?, override val tokensListState: WalletTokensListState, + override val nftState: WalletNFTItemUM, ) : MultiCurrency() data class Locked( @@ -46,6 +48,7 @@ internal sealed interface WalletState : WalletStateHolder { ) { override val tokensListState = WalletTokensListState.ContentState.Locked + override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden } } @@ -96,7 +99,6 @@ internal sealed interface WalletState : WalletStateHolder { override val bottomSheetConfig: TangemBottomSheetConfig?, override val balancesAndLimitBlockState: BalancesAndLimitsBlockState, override val txHistoryState: TxHistoryState, - val depositButtonState: DepositButtonState, ) : Visa() data class Locked( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index 72d2e0f783..8a258581d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -5,21 +5,18 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList internal class AddWalletTransformer( private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index ac0c59bc42..ee9f47ff70 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -8,7 +8,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.state.utils.createStateByWalletType import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -18,14 +17,12 @@ internal class InitializeWalletsTransformer( private val wallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ) } @@ -38,7 +35,9 @@ internal class InitializeWalletsTransformer( if (userWallet.isLocked) { createLockedState(userWallet) } else { - walletLoadingStateFactory.create(userWallet) + walletLoadingStateFactory.create( + userWallet = userWallet, + ) } } .toImmutableList(), @@ -96,7 +95,7 @@ internal class InitializeWalletsTransformer( private fun createMultiWalletEnabledButtons(userWallet: UserWallet): PersistentList { val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - if (!walletFeatureToggles.isMainActionButtonsEnabled || isSingleWalletWithToken) return persistentListOf() + if (isSingleWalletWithToken) return persistentListOf() return persistentListOf( WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {}), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index ceecfc9cfc..b1b567d556 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -2,11 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList /** @@ -23,14 +22,12 @@ internal class ReinitializeWalletTransformer( private val newUserWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ) } @@ -38,7 +35,11 @@ internal class ReinitializeWalletTransformer( return prevState.copy( wallets = prevState.wallets .filterNot { it.walletCardState.id == prevWalletId } - .plus(element = walletLoadingStateFactory.create(newUserWallet)) + .plus( + element = walletLoadingStateFactory.create( + userWallet = newUserWallet, + ), + ) .toImmutableList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt new file mode 100644 index 0000000000..b66ed6ba9a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState + +internal class RemoveNFTCollectionsTransformer( + userWalletId: UserWalletId, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState = when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy( + nftState = WalletNFTItemUM.Hidden, + ) + is WalletState.SingleCurrency.Content, + is WalletState.Visa.Content, + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + is WalletState.Visa.Locked, + is WalletState.Visa.AccessTokenLocked, + -> prevState + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt new file mode 100644 index 0000000000..f713b64077 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -0,0 +1,95 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.nft.models.NFTCollections +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import kotlinx.collections.immutable.toPersistentList + +internal class SetNFTCollectionsTransformer( + userWalletId: UserWalletId, + private val nftCollections: List, +) : WalletStateTransformer(userWalletId) { + + override fun transform(prevState: WalletState): WalletState = when (prevState) { + is WalletState.MultiCurrency.Content -> prevState.copy( + nftState = when { + allCollectionsFailed() -> WalletNFTItemUM.Failed + anyCollectionFailed() && allLoadedCollectionsEmpty() -> WalletNFTItemUM.Failed + allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Empty + !allCollectionsLoaded() && allCollectionsEmpty() -> WalletNFTItemUM.Loading + else -> createContentNFTItemUM() + }, + ) + is WalletState.SingleCurrency.Content, + is WalletState.Visa.Content, + is WalletState.MultiCurrency.Locked, + is WalletState.SingleCurrency.Locked, + is WalletState.Visa.Locked, + is WalletState.Visa.AccessTokenLocked, + -> prevState + } + + private fun createContentNFTItemUM(): WalletNFTItemUM.Content { + val collectionsContent = nftCollections + .map { it.content } + .filterIsInstance() + + val isFlickering = collectionsContent + .map { it.source } + .any { it == StatusSource.CACHE } + + val collections = collectionsContent + .mapNotNull { it.collections } + .flatten() + + return WalletNFTItemUM.Content( + previews = if (collections.size > NFT_COLLECTIONS_MAX_PREVIEWS_COUNT) { + collections + .take(NFT_COLLECTIONS_MAX_PREVIEWS_COUNT - 1) + .map { WalletNFTItemUM.Content.CollectionPreview.Image(it.logoUrl.orEmpty()) } + .plus(WalletNFTItemUM.Content.CollectionPreview.More) + .toPersistentList() + } else { + collections + .take(NFT_COLLECTIONS_MAX_PREVIEWS_COUNT) + .map { WalletNFTItemUM.Content.CollectionPreview.Image(it.logoUrl.orEmpty()) } + .toPersistentList() + }, + collectionsCount = collections.size, + assetsCount = collections + .sumOf { it.count }, + isFlickering = isFlickering, + ) + } + + private fun allCollectionsFailed() = nftCollections.all { + it.content is NFTCollections.Content.Error + } + + private fun anyCollectionFailed() = nftCollections.any { + it.content is NFTCollections.Content.Error + } + + private fun allLoadedCollectionsEmpty() = nftCollections + .map { it.content } + .filterIsInstance() + .all { it.collections.isNullOrEmpty() } + + private fun allCollectionsLoaded() = nftCollections.all { + val content = it.content + content is NFTCollections.Content.Collections && + content.source != StatusSource.CACHE + } + + private fun allCollectionsEmpty() = nftCollections.all { + val content = it.content + content is NFTCollections.Content.Collections && + content.collections.isNullOrEmpty() + } + + companion object { + private const val NFT_COLLECTIONS_MAX_PREVIEWS_COUNT = 4 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 1465ccc33a..4ba7560ab0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBlockState -import com.tangem.feature.wallet.presentation.wallet.state.model.DepositButtonState import com.tangem.feature.wallet.presentation.wallet.state.model.* import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate @@ -29,8 +28,8 @@ internal class SetRefreshStateTransformer( } is WalletState.Visa.Content -> { prevState.copy( + buttons = prevState.buttons.toUpdatedState(), pullToRefreshConfig = prevState.pullToRefreshConfig.toUpdatedState(isRefreshing), - depositButtonState = prevState.depositButtonState.toUpdatedState(isRefreshing), balancesAndLimitBlockState = prevState.balancesAndLimitBlockState.toUpdatedState(isRefreshing), ) } @@ -75,10 +74,6 @@ internal class SetRefreshStateTransformer( } } - private fun DepositButtonState.toUpdatedState(isRefreshing: Boolean): DepositButtonState { - return copy(isEnabled = !isRefreshing) - } - private fun BalancesAndLimitsBlockState.toUpdatedState(isRefreshing: Boolean): BalancesAndLimitsBlockState { return when (this) { is BalancesAndLimitsBlockState.Content -> copy(isEnabled = !isRefreshing) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt similarity index 68% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt index 2655da4ba9..1d65f129ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetVisaInfoTransformer.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.common.util.getCardsCount +import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.visa.exception.RefreshTokenExpiredException import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet @@ -15,11 +16,14 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditiona import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.utils.extensions.isZero +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import org.joda.time.DateTime import org.joda.time.Days -internal class SetBalancesAndLimitsTransformer( +internal class SetVisaInfoTransformer( private val userWallet: UserWallet, private val maybeVisaCurrency: Either, private val clickIntents: WalletClickIntents, @@ -35,15 +39,15 @@ internal class SetBalancesAndLimitsTransformer( } return prevState.copy( + buttons = createVisaButtonsDimmed(), walletCardState = getErrorWalletCardState(prevState.walletCardState), - depositButtonState = prevState.depositButtonState.copy(isEnabled = false), balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } return prevState.copy( + buttons = createVisaButtons(visaCurrency = visaCurrency), walletCardState = getContentWalletCardState(prevState.walletCardState, visaCurrency), - depositButtonState = prevState.depositButtonState.copy(isEnabled = true), balancesAndLimitBlockState = getContentBlockState(visaCurrency), ) } @@ -115,4 +119,43 @@ internal class SetBalancesAndLimitsTransformer( onUnlockVisaAccessNotificationClick = clickIntents::onUnlockVisaAccessClick, ) } + + private fun createVisaButtonsDimmed(): PersistentList { + return persistentListOf( + WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), + WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}), + ) + } + + private fun createVisaButtons(visaCurrency: VisaCurrency): PersistentList { + // [Second Visa Iteration] Make VisaCurrency contain CryptoCurrencyStatus + val cryptoCurrencyStatus = CryptoCurrencyStatus( + currency = visaCurrency.cryptoCurrency, + value = CryptoCurrencyStatus.Loaded( + amount = visaCurrency.balances.available, + fiatAmount = visaCurrency.balances.available.multiply(visaCurrency.fiatRate), + fiatRate = visaCurrency.fiatRate, + priceChange = visaCurrency.priceChange, + yieldBalance = null, + hasCurrentNetworkTransactions = false, + pendingTransactions = emptySet(), + networkAddress = visaCurrency.paymentAccountAddress, + sources = CryptoCurrencyStatus.Sources(), + ), + ) + + return return persistentListOf( + WalletManageButton.Receive( + enabled = true, + dimContent = false, + onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus = cryptoCurrencyStatus) }, + onLongClick = { clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus) }, + ), + WalletManageButton.Buy( + enabled = true, + dimContent = false, + onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId) }, + ), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 61cb2c07b4..d741af8e9a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -7,7 +7,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenSta import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -15,14 +14,12 @@ internal class UnlockWalletTransformer( private val unlockedWallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, - walletFeatureToggles = walletFeatureToggles, ) } @@ -46,7 +43,9 @@ internal class UnlockWalletTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, is WalletState.Visa.Locked, - -> walletLoadingStateFactory.create(userWallet = unlockedWallet) + -> walletLoadingStateFactory.create( + userWallet = unlockedWallet, + ) is WalletState.MultiCurrency.Content, is WalletState.SingleCurrency.Content, is WalletState.Visa.Content, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 6223bc1d05..439993d834 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -1,15 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents 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.model.* -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class WalletLoadingStateFactory( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, - private val walletFeatureToggles: WalletFeatureToggles, ) { fun create(userWallet: UserWallet): WalletState { @@ -41,6 +39,7 @@ internal class WalletLoadingStateFactory( warnings = persistentListOf(), bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, + nftState = WalletNFTItemUM.Hidden, ) } @@ -67,7 +66,7 @@ internal class WalletLoadingStateFactory( return WalletState.Visa.Content( pullToRefreshConfig = createPullToRefreshConfig(), walletCardState = userWallet.toLoadingWalletCardState(), - buttons = createMultiWalletActions(userWallet), + buttons = createVisaDimmedButtons(), warnings = persistentListOf(), bottomSheetConfig = null, balancesAndLimitBlockState = BalancesAndLimitsBlockState.Loading, @@ -76,12 +75,14 @@ internal class WalletLoadingStateFactory( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), - depositButtonState = DepositButtonState(isEnabled = false, clickIntents::onDepositClick), ) } private fun createPullToRefreshConfig(): PullToRefreshConfig { - return PullToRefreshConfig(onRefresh = { clickIntents.onRefreshSwipe(it.value) }, isRefreshing = false) + return PullToRefreshConfig( + onRefresh = { clickIntents.onRefreshSwipe(it.value) }, + isRefreshing = false, + ) } private fun UserWallet.toLoadingWalletCardState(): WalletCardState { @@ -96,7 +97,7 @@ internal class WalletLoadingStateFactory( private fun createMultiWalletActions(userWallet: UserWallet): PersistentList { val isSingleWalletWithToken = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - if (!walletFeatureToggles.isMainActionButtonsEnabled || isSingleWalletWithToken) return persistentListOf() + if (isSingleWalletWithToken) return persistentListOf() return persistentListOf( WalletManageButton.Buy( @@ -117,6 +118,13 @@ internal class WalletLoadingStateFactory( ) } + private fun createVisaDimmedButtons(): PersistentList { + return persistentListOf( + WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), + WalletManageButton.Buy(enabled = true, dimContent = true, onClick = {}), + ) + } + private fun createDimmedButtons(): PersistentList { return persistentListOf( WalletManageButton.Receive(enabled = true, dimContent = true, onClick = {}, onLongClick = null), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt index 239ee8c80d..03e49e67f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/VisaWalletSubscriber.kt @@ -13,7 +13,7 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetBalancesAndLimitsTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetVisaInfoTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer @@ -62,7 +62,7 @@ internal class VisaWalletSubscriber( private fun setLoadedCurrencyState(maybeCurrency: Either) { stateController.update( - SetBalancesAndLimitsTransformer( + SetVisaInfoTransformer( userWallet = userWallet, maybeVisaCurrency = maybeCurrency, clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt new file mode 100644 index 0000000000..476770ce55 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.nft.GetNFTCollectionsUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.transformers.RemoveNFTCollectionsTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetNFTCollectionsTransformer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +@Suppress("UnusedPrivateMember") +internal class WalletNFTListSubscriber( + private val userWallet: UserWallet, + private val stateHolder: WalletStateController, + private val walletsRepository: WalletsRepository, + private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, + clickIntents: WalletClickIntents, +) : WalletSubscriber() { + + @OptIn(ExperimentalCoroutinesApi::class) + override fun create(coroutineScope: CoroutineScope): Flow<*> = walletsRepository + .nftEnabledStatus(userWallet.walletId) + .distinctUntilChanged() + .flatMapLatest { nftEnabled -> + // if NFT is enabled for this wallet, then start observing changes from store and apply transformer if need + if (nftEnabled) { + getNFTCollectionsUseCase + .launch(userWallet.walletId) + .shareIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + .onEach { + stateHolder.update( + SetNFTCollectionsTransformer(userWallet.walletId, it), + ) + } + } else { + // otherwise, hide NFT from wallet + stateHolder.update( + RemoveNFTCollectionsTransformer(userWallet.walletId), + ) + emptyFlow() + } + } +} \ 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 69c2f73c77..ff7362b3c2 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 @@ -80,12 +80,12 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsB import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* import com.tangem.feature.wallet.presentation.wallet.ui.components.common.actions +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.nftCollections import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.organizeTokensButton import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.BalancesAndLimitsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDetailsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock -import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator import com.tangem.features.markets.entry.BottomSheetState import com.tangem.features.markets.entry.MarketsEntryComponent @@ -230,11 +230,6 @@ private fun WalletContent( } (selectedWallet as? WalletState.Visa.Content)?.let { - depositButton( - modifier = itemModifier.fillMaxWidth(), - state = it.depositButtonState, - ) - balancesAndLimitsBlock( modifier = itemModifier, state = it.balancesAndLimitBlockState, @@ -248,6 +243,8 @@ private fun WalletContent( modifier = movableItemModifier, ) + nftCollections(state = selectedWallet, itemModifier = itemModifier) + organizeTokens(state = selectedWallet, itemModifier = itemModifier) } @@ -706,6 +703,16 @@ internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modi } } +internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modifier) { + (state as? WalletState.MultiCurrency)?.let { + nftCollections( + modifier = itemModifier, + state = it.nftState, + onClick = {}, + ) + } +} + @Composable private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt index d31a09e214..7f2a8515b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem.kt @@ -131,7 +131,20 @@ private fun WalletNFTItemFailed(modifier: Modifier = Modifier) { RowContentContainer( modifier = modifier, icon = { - CollectionsPreviewsPlaceholder() + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(TangemTheme.colors.field.primary), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } }, text = { Text( @@ -158,7 +171,12 @@ private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { RowContentContainer( modifier = modifier, icon = { - CollectionsPreviewsPlaceholder() + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(TangemTheme.colors.field.primary), + ) }, text = { TextShimmer( @@ -178,16 +196,6 @@ private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { ) } -@Composable -private fun CollectionsPreviewsPlaceholder(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .size(TangemTheme.dimens.size36) - .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) - .background(TangemTheme.colors.field.primary), - ) -} - @Composable @Suppress("MagicNumber") private fun BoxScope.CollectionsPreviews(previews: ImmutableList, modifier: Modifier = Modifier) { @@ -207,7 +215,7 @@ private fun BoxScope.CollectionsPreviews(previews: ImmutableList { SubcomposeAsyncImage( modifier = modifier, - model = s, + model = s.url, loading = { RectangleShimmer(radius = 0.dp) }, @@ -386,7 +394,7 @@ private fun RowContentContainer( @Preview(widthDp = 360) @Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview_WalletNFTItem_InLight(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) { +private fun Preview_WalletNFTItem(@PreviewParameter(WalletNFTItemProvider::class) state: WalletNFTItemUM) { TangemThemePreview { WalletNFTItem( state = state, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt new file mode 100644 index 0000000000..da4ce8f1d5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyNFTCollections.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletNFTItem + +private const val NFT_COLLECTIONS_CONTENT_TYPE = "NFTCollections" + +internal fun LazyListScope.nftCollections(state: WalletNFTItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + item(key = NFT_COLLECTIONS_CONTENT_TYPE, contentType = NFT_COLLECTIONS_CONTENT_TYPE) { + WalletNFTItem( + modifier = modifier, + state = state, + onClick = onClick, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt deleted file mode 100644 index 395e9499a4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/VisaDepositButton.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.visa - -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.DepositButtonState - -private const val DEPOSIT_BUTTON_CONTENT_TYPE = "DepositButton" - -internal fun LazyListScope.depositButton(state: DepositButtonState, modifier: Modifier = Modifier) { - item(key = DEPOSIT_BUTTON_CONTENT_TYPE, contentType = DEPOSIT_BUTTON_CONTENT_TYPE) { - PrimaryButtonIconStart( - modifier = modifier, - text = "Deposit", - iconResId = R.drawable.ic_arrow_down_24, - onClick = state.onClick, - enabled = state.isEnabled, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ill_keiro_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_keiro_card2_120_106.webp new file mode 100644 index 0000000000..58ddd441a0 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_keiro_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_keiro_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_keiro_card3_120_106.webp new file mode 100644 index 0000000000..371231a672 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_keiro_card3_120_106.webp differ diff --git a/features/walletconnect/api/.gitignore b/features/walletconnect/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/walletconnect/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/walletconnect/api/build.gradle.kts b/features/walletconnect/api/build.gradle.kts new file mode 100644 index 0000000000..9eea21d4c0 --- /dev/null +++ b/features/walletconnect/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.walletconnect.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain models */ + implementation(projects.domain.wallets.models) +} \ No newline at end of file diff --git a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt new file mode 100644 index 0000000000..c7d60d1091 --- /dev/null +++ b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectEntryComponent.kt @@ -0,0 +1,8 @@ +package com.tangem.features.walletconnect.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface WalletConnectEntryComponent : ComposableContentComponent { + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/walletconnect/impl/.gitignore b/features/walletconnect/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/walletconnect/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts new file mode 100644 index 0000000000..f3822c54db --- /dev/null +++ b/features/walletconnect/impl/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.walletconnect.impl" +} + +dependencies { + implementation(projects.features.walletconnect.api) + + /** Core */ + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain models */ + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /** AndroidX */ + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.coil) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** Other */ + implementation(deps.decompose.ext.compose) + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt new file mode 100644 index 0000000000..09c4b03ff5 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt @@ -0,0 +1,24 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.walletconnect.connections.model.WcConnectionsModel +import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent + +internal class ConnectionsComponent( + appComponentContext: AppComponentContext, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: WcConnectionsModel = getOrCreateModel() + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + WcConnectionsContent(modifier = modifier, state = state) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt new file mode 100644 index 0000000000..3f3ee812d0 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/DefaultWalletConnectEntryComponent.kt @@ -0,0 +1,58 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.walletconnect.components.WalletConnectEntryComponent +import com.tangem.features.walletconnect.connections.routes.ConnectionsInnerRoute +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("UnusedPrivateMember") +internal class DefaultWalletConnectEntryComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted params: Unit, +) : AppComponentContext by appComponentContext, WalletConnectEntryComponent { + + private val contentNavigation = StackNavigation() + private val contentStack = childStack( + source = contentNavigation, + serializer = ConnectionsInnerRoute.serializer(), + initialConfiguration = ConnectionsInnerRoute.Connections, + childFactory = ::screenChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val childStack by contentStack.subscribeAsState() + + BackHandler(onBack = router::pop) + Children(stack = childStack, animation = stackAnimation()) { child -> + child.instance.Content(modifier = modifier) + } + } + + private fun screenChild( + config: ConnectionsInnerRoute, + componentContext: ComponentContext, + ): ComposableContentComponent = when (config) { + ConnectionsInnerRoute.Connections -> ConnectionsComponent(childByContext(componentContext)) + ConnectionsInnerRoute.QrScan -> TODO("[REDACTED_JIRA]") + } + + @AssistedFactory + interface Factory : WalletConnectEntryComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectEntryComponent + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt new file mode 100644 index 0000000000..62474e35b9 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt @@ -0,0 +1,32 @@ +package com.tangem.features.walletconnect.connections.entity + +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import kotlinx.collections.immutable.ImmutableList + +internal data class WcConnectionsState( + val topAppBarConfig: WcConnectionsTopAppBarConfig, + val connections: ImmutableList, + val onNewConnectionClick: () -> Unit, +) + +internal data class WcConnectionsTopAppBarConfig( + val startButtonUM: TopAppBarButtonUM, + val endActions: ImmutableList, +) + +internal data class WcConnectionsUM( + val userWalletId: String, + val walletName: String, + val connectedApps: ImmutableList, +) + +internal data class WcConnectedAppInfo( + val name: String, + val iconUrl: String, + val subtitle: TextReference = stringReference("Connected App"), + val isVerified: Boolean = false, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt new file mode 100644 index 0000000000..0d6c65c6d2 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -0,0 +1,54 @@ +package com.tangem.features.walletconnect.connections.model + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.walletconnect.connections.entity.WcConnectionsState +import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData +import com.tangem.features.walletconnect.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@ModelScoped +internal class WcConnectionsModel @Inject constructor( + private val router: Router, + private val uiMessageSender: UiMessageSender, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val uiState: StateFlow = MutableStateFlow( + WcConnectionsPreviewData.stateForModel( + onBackClicked = router::pop, + openQrScanScreen = ::openQrScanScreen, + disconnectAll = ::disconnectAll, + ), + ).asStateFlow() + + private fun openQrScanScreen() { + // TODO("[REDACTED_JIRA]") + } + + private fun disconnectAll() { + val message = DialogMessage( + title = resourceReference(R.string.wc_disconnect_all_alert_title), + message = resourceReference(R.string.wc_disconnect_all_alert_desc), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_disconnect), + onClick = { + // TODO(wc): disconnect all + }, + ) + }, + secondActionBuilder = { cancelAction() }, + ) + uiMessageSender.send(message) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectionsInnerRoute.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectionsInnerRoute.kt new file mode 100644 index 0000000000..a07346591c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/ConnectionsInnerRoute.kt @@ -0,0 +1,12 @@ +package com.tangem.features.walletconnect.connections.routes + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class ConnectionsInnerRoute { + @Serializable + data object Connections : ConnectionsInnerRoute() + + @Serializable + data object QrScan : ConnectionsInnerRoute() +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt new file mode 100644 index 0000000000..005ff5c7a4 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt @@ -0,0 +1,299 @@ +package com.tangem.features.walletconnect.connections.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +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.util.fastForEach +import coil.compose.AsyncImage +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo +import com.tangem.features.walletconnect.connections.entity.WcConnectionsState +import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig +import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM +import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData +import com.tangem.features.walletconnect.impl.R +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun WcConnectionsContent(state: WcConnectionsState, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + ConnectionsTopBar( + modifier = Modifier.statusBarsPadding(), + config = state.topAppBarConfig, + showEndButton = state.connections.isNotEmpty(), + ) + }, + floatingActionButton = { + if (state.connections.isNotEmpty()) { + NewConnectionButton( + onClick = state.onNewConnectionClick, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + ) + } + }, + floatingActionButtonPosition = FabPosition.Center, + content = { innerPadding -> + Content( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + connections = state.connections, + onNewConnectionClick = state.onNewConnectionClick, + ) + }, + ) +} + +@Composable +private fun Content( + connections: ImmutableList, + onNewConnectionClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (connections.isEmpty()) { + EmptyConnectionsBlock(modifier = modifier, onNewConnectionClick = onNewConnectionClick) + } else { + ConnectionsBlock(modifier = modifier, connections = connections) + } +} + +@Composable +private fun EmptyConnectionsBlock(onNewConnectionClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(R.drawable.img_wallet_connect_76), + contentDescription = "Wallet Connect", + modifier = Modifier.size(76.dp), + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), + text = stringResourceSafe(R.string.wc_no_sessions_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h3, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = stringResourceSafe(R.string.wc_no_sessions_desc), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body1, + ) + NewConnectionButton( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing48), + onClick = onNewConnectionClick, + ) + } +} + +@Composable +private fun ConnectionsBlock(connections: ImmutableList, modifier: Modifier = Modifier) { + Box(modifier = modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing76 + bottomBarHeight, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + items( + items = connections, + key = { it.userWalletId }, + itemContent = { + ConnectionItem( + modifier = Modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius14), + ) + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)), + connection = it, + ) + }, + ) + } + BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) + } +} + +@Composable +private fun ConnectionItem(connection: WcConnectionsUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + key("${connection.userWalletId}_${connection.walletName}") { + Text( + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp, start = 12.dp, end = 12.dp), + text = connection.walletName, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + } + connection.connectedApps.fastForEach { appInfo -> + key("${connection.userWalletId}_${connection.walletName}_${appInfo.name}_${appInfo.iconUrl}") { + AppInfoItem( + appInfo = appInfo, + modifier = Modifier + .clickable(onClick = appInfo.onClick) + .padding(all = 12.dp), + ) + } + } + } +} + +@Composable +private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + AsyncImage( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)), + model = appInfo.iconUrl, + contentDescription = appInfo.name, + ) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Text( + text = appInfo.name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + ) + Text( + text = appInfo.subtitle.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ConnectionsTopBar( + config: WcConnectionsTopAppBarConfig, + showEndButton: Boolean, + modifier: Modifier = Modifier, +) { + var showDropdownMenu by rememberSaveable { mutableStateOf(false) } + // TangemTopAppBar is not friendly with dropdown menu + TopAppBar( + modifier = modifier, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + titleContentColor = TangemTheme.colors.icon.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ), + navigationIcon = { + IconButton(onClick = config.startButtonUM.onIconClicked) { + Icon( + painter = painterResource(id = config.startButtonUM.iconRes), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Back", + ) + } + }, + title = { + Text( + text = stringResourceSafe(R.string.wc_connections), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + actions = { + AnimatedVisibility(showEndButton) { + IconButton(onClick = { showDropdownMenu = true }) { + Icon( + painter = painterResource(id = R.drawable.ic_more_vertical_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "More", + ) + } + } + + TangemDropdownMenu( + expanded = showDropdownMenu, + modifier = Modifier.background(TangemTheme.colors.background.primary), + onDismissRequest = { showDropdownMenu = false }, + content = { + config.endActions.fastForEach { + TangemDropdownItem( + item = it, + dismissParent = { showDropdownMenu = false }, + ) + } + }, + ) + }, + ) +} + +@Composable +private fun NewConnectionButton(onClick: () -> Unit, modifier: Modifier = Modifier) { + PrimaryButton( + modifier = modifier, + text = stringResourceSafe(R.string.wc_new_connection), + onClick = onClick, + ) +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectionsContentPreview( + @PreviewParameter(WcConnectionsStateProvider::class) params: WcConnectionsState, +) { + TangemThemePreview { + WcConnectionsContent(state = params) + } +} + +private class WcConnectionsStateProvider : CollectionPreviewParameterProvider( + listOf( + WcConnectionsPreviewData.stateWithEmptyConnections, + WcConnectionsPreviewData.fullState, + ), +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt new file mode 100644 index 0000000000..57ce635a39 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt @@ -0,0 +1,141 @@ +package com.tangem.features.walletconnect.connections.ui.preview + +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo +import com.tangem.features.walletconnect.connections.entity.WcConnectionsState +import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig +import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM +import com.tangem.features.walletconnect.impl.R +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import timber.log.Timber + +internal object WcConnectionsPreviewData { + private const val BASE_URL = "https://raw.githubusercontent.com/TaranVH/LOGOS/refs/heads/master/Crypto" + private val connections = persistentListOf( + WcConnectionsUM( + userWalletId = "wallet_id_1", + walletName = "Wallet 2.0", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "Moralis", + iconUrl = "$BASE_URL/satoshi.png", + isVerified = true, + onClick = { Timber.d("Moralis clicked") }, + ), + WcConnectedAppInfo( + name = "website.com", + iconUrl = "$BASE_URL/doge_again_lol.jpg", + onClick = { Timber.d("website.com clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_2", + walletName = "Bitcoin", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "React app", + iconUrl = "$BASE_URL/LiteCoin.png", + isVerified = true, + onClick = { Timber.d("React app clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_3", + walletName = "Wallet 2.0", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "Moralis", + iconUrl = "$BASE_URL/satoshi.png", + isVerified = true, + onClick = { Timber.d("Moralis clicked") }, + ), + WcConnectedAppInfo( + name = "website.com", + iconUrl = "$BASE_URL/doge_again_lol.jpg", + onClick = { Timber.d("website.com clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_4", + walletName = "Bitcoin", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "React app", + iconUrl = "$BASE_URL/LiteCoin.png", + isVerified = true, + onClick = { Timber.d("React app clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_5", + walletName = "Wallet 2.0", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "Moralis", + iconUrl = "$BASE_URL/satoshi.png", + isVerified = true, + onClick = { Timber.d("Moralis clicked") }, + ), + WcConnectedAppInfo( + name = "website.com", + iconUrl = "$BASE_URL/doge_again_lol.jpg", + onClick = { Timber.d("website.com clicked") }, + ), + ), + ), + WcConnectionsUM( + userWalletId = "wallet_id_6", + walletName = "Bitcoin", + connectedApps = persistentListOf( + WcConnectedAppInfo( + name = "React app", + iconUrl = "$BASE_URL/LiteCoin.png", + isVerified = true, + onClick = { Timber.d("React app clicked") }, + ), + ), + ), + ) + val stateWithEmptyConnections = WcConnectionsState( + topAppBarConfig = WcConnectionsTopAppBarConfig( + startButtonUM = TopAppBarButtonUM( + iconRes = R.drawable.ic_back_24, + onIconClicked = {}, + enabled = true, + ), + endActions = persistentListOf( + TangemDropdownMenuItem( + title = resourceReference(R.string.wc_disconnect_all), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, + ), + ), + ), + connections = persistentListOf(), + onNewConnectionClick = {}, + ) + val fullState = stateWithEmptyConnections.copy(connections = connections) + fun stateForModel( + onBackClicked: () -> Unit, + openQrScanScreen: () -> Unit, + disconnectAll: () -> Unit, + ): WcConnectionsState { + return fullState.copy( + topAppBarConfig = fullState.topAppBarConfig.copy( + startButtonUM = fullState.topAppBarConfig.startButtonUM.copy(onIconClicked = onBackClicked), + endActions = fullState.topAppBarConfig.endActions + .map { it.copy(onClick = disconnectAll) } + .toImmutableList(), + ), + onNewConnectionClick = openQrScanScreen, + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectFeatureModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectFeatureModule.kt new file mode 100644 index 0000000000..e8a98073d4 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectFeatureModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.walletconnect.di + +import com.tangem.features.walletconnect.components.WalletConnectEntryComponent +import com.tangem.features.walletconnect.connections.components.DefaultWalletConnectEntryComponent +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 WalletConnectFeatureModule { + + @Binds + @Singleton + fun bindWalletConnectEntryComponentFactory( + factory: DefaultWalletConnectEntryComponent.Factory, + ): WalletConnectEntryComponent.Factory +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt new file mode 100644 index 0000000000..9b8d6d4f4f --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.walletconnect.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.walletconnect.connections.model.WcConnectionsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface WalletConnectModelModule { + + @Binds + @IntoMap + @ClassKey(WcConnectionsModel::class) + fun bindWcConnectionsModel(model: WcConnectionsModel): Model +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index b2f75cab44..8fbee2f5e1 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -47,7 +47,7 @@ coil = "2.1.0" compose-shimmer = "1.0.3" coroutine = "1.7.2" # 1.8+ is not compatible with tangem-sdk desugarJdkLibs = "1.1.5" -firebase = "33.7.0" +firebase = "33.10.0" googleMaterialComponent = "1.6.1" googlePlayReview = "2.0.1" googlePlayReviewKtx = "2.0.1" diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts index 3a1001135f..5b0077a1d2 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.configToggles) implementation(projects.core.utils) + implementation(projects.core.analytics) // endregion // region AndroidX libraries diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 1c9cc512e4..d6e075bb8a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -6,7 +6,6 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger -import com.tangem.blockchainsdk.featuretoggles.BlockchainSDKFeatureToggles import com.tangem.blockchainsdk.providers.BlockchainProviderTypes import timber.log.Timber import javax.inject.Inject @@ -24,7 +23,6 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, - private val blockchainSDKFeatureToggles: BlockchainSDKFeatureToggles, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -35,7 +33,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles( - isEthereumEIP1559Enabled = blockchainSDKFeatureToggles.isEthereumEIP1559Enabled, + isEthereumEIP1559Enabled = true, ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index 0e46908b4f..cce9a8c942 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -12,13 +12,11 @@ import com.tangem.blockchainsdk.DefaultBlockchainSDKFactory import com.tangem.blockchainsdk.WalletManagerFactoryCreator import com.tangem.blockchainsdk.accountcreator.DefaultAccountCreator import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage -import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles import com.tangem.blockchainsdk.providers.BlockchainProviderTypesStore import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.DevBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.dev.BlockchainProvidersResponseSerializer -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage @@ -92,13 +90,11 @@ internal object BlockchainSDKFactoryModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, blockchainSDKLogger: BlockchainSDKLogger, - featureTogglesManager: FeatureTogglesManager, ): WalletManagerFactoryCreator { return WalletManagerFactoryCreator( accountCreator = DefaultAccountCreator(tangemTechApi), blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), blockchainSDKLogger = blockchainSDKLogger, - blockchainSDKFeatureToggles = DefaultBlockchainSDKFeatureToggles(featureTogglesManager), ) } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/ExcludedBlockchainsModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/ExcludedBlockchainsModule.kt index c63d1dd071..35b4d2ff9b 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/ExcludedBlockchainsModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/ExcludedBlockchainsModule.kt @@ -1,6 +1,8 @@ package com.tangem.blockchainsdk.di +import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMerger import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import dagger.Module import dagger.Provides @@ -17,4 +19,12 @@ internal object ExcludedBlockchainsModule { fun bindExcludedBlockchains(excludedBlockchainsManager: ExcludedBlockchainsManager): ExcludedBlockchains { return ExcludedBlockchains(excludedBlockchainsManager) } + + @Provides + @Singleton + fun bindBlockchainProvidersResponseMerger( + analyticsExceptionHandler: AnalyticsExceptionHandler, + ): BlockchainProvidersResponseMerger { + return BlockchainProvidersResponseMerger(analyticsExceptionHandler) + } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt index 482df0cd5a..de92518ae5 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/BlockchainSDKFeatureToggles.kt @@ -1,6 +1,3 @@ package com.tangem.blockchainsdk.featuretoggles -internal interface BlockchainSDKFeatureToggles { - - val isEthereumEIP1559Enabled: Boolean -} \ No newline at end of file +internal interface BlockchainSDKFeatureToggles \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt index 2242f2675f..5be43a55e2 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/featuretoggles/DefaultBlockchainSDKFeatureToggles.kt @@ -1,11 +1,3 @@ package com.tangem.blockchainsdk.featuretoggles -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -internal class DefaultBlockchainSDKFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : BlockchainSDKFeatureToggles { - - override val isEthereumEIP1559Enabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "IS_ETHEREUM_EIP_1559_ENABLED") -} \ No newline at end of file +internal class DefaultBlockchainSDKFeatureToggles : BlockchainSDKFeatureToggles \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt index 4336fc4db4..60c5024818 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt @@ -22,6 +22,7 @@ import javax.inject.Singleton internal class BlockchainProvidersResponseLoader @Inject constructor( private val tangemTechApi: TangemTechApi, private val blockchainProvidersStorage: BlockchainProvidersStorage, + private val blockchainProvidersResponseMerger: BlockchainProvidersResponseMerger, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -31,7 +32,7 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( return loadRemote().fold( onSuccess = { remoteResponse -> - BlockchainProvidersResponseMerger.merge( + blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteResponse, ) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt index ec57ec97ac..e64cc2f00e 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt @@ -1,19 +1,21 @@ package com.tangem.blockchainsdk.providers import androidx.core.util.PatternsCompat -import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.local.config.providers.models.ProviderModel import timber.log.Timber +import javax.inject.Inject /** * Merger of [BlockchainProvidersResponse] * [REDACTED_AUTHOR] */ -internal object BlockchainProvidersResponseMerger { - - private val firebaseCrashlytics by lazy(FirebaseCrashlytics::getInstance) +internal class BlockchainProvidersResponseMerger @Inject internal constructor( + private val analyticsExceptionHandler: AnalyticsExceptionHandler, +) { private val forbiddenSchemes = listOf("wss://") @@ -41,7 +43,7 @@ internal object BlockchainProvidersResponseMerger { val missingBlockchains = result.keys - remote.keys val blockchainsWithoutProviders = remote.filterValues { it.isEmpty() }.keys - recordException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) + logException(missingBlockchains = missingBlockchains + blockchainsWithoutProviders) } return result.guaranteeUrlsEndWithSlash() @@ -74,15 +76,19 @@ internal object BlockchainProvidersResponseMerger { return PatternsCompat.WEB_URL.matcher(inputUrl).matches() } - private fun recordException(missingBlockchains: Set) { + private fun logException(missingBlockchains: Set) { val exception = IllegalStateException( - "Remote config does not contain required blockchains or providers information: " + - missingBlockchains.joinToString(), + "Remote config does not contain some blockchains or providers information", ) Timber.e(exception) - firebaseCrashlytics.recordException(exception) + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = exception, + params = mapOf("Missing blockchains" to missingBlockchains.joinToString()), + ), + ) } /* diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt index 54d99add4a..64d82467cc 100644 --- a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoaderTest.kt @@ -4,6 +4,8 @@ import com.google.common.truth.Truth import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.localResponse import com.tangem.blockchainsdk.providers.BlockchainProvidersResponseMergerTest.Companion.remoteResponse +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage import com.tangem.datasource.local.config.providers.models.ProviderModel @@ -20,10 +22,16 @@ internal class BlockchainProvidersResponseLoaderTest { private val tangemTechApi = mockk() private val blockchainProvidersStorage = mockk() + private val analyticsExceptionHandler = object : AnalyticsExceptionHandler { + override fun sendException(event: ExceptionAnalyticsEvent) { + FirebaseCrashlytics.getInstance().recordException(event.exception) + } + } private val loader = BlockchainProvidersResponseLoader( tangemTechApi = tangemTechApi, blockchainProvidersStorage = blockchainProvidersStorage, + blockchainProvidersResponseMerger = BlockchainProvidersResponseMerger(analyticsExceptionHandler), dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt index 53004c77b9..2fc0251d41 100644 --- a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMergerTest.kt @@ -2,7 +2,10 @@ package com.tangem.blockchainsdk.providers import com.google.common.truth.Truth import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.firebase.crashlytics.recordException import com.tangem.blockchainsdk.BlockchainProvidersResponse +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.local.config.providers.models.ProviderModel import io.mockk.* import org.junit.Before @@ -13,6 +16,14 @@ import org.junit.Test */ internal class BlockchainProvidersResponseMergerTest { + private val blockchainProvidersResponseMerger = BlockchainProvidersResponseMerger( + object : AnalyticsExceptionHandler { + override fun sendException(event: ExceptionAnalyticsEvent) { + FirebaseCrashlytics.getInstance().recordException(event.exception) + } + }, + ) + @Before fun setup() { mockkStatic(FirebaseCrashlytics::class) @@ -25,7 +36,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_both_configs_are_empty() { val expected = emptyMap>() - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = emptyMap(), remote = emptyMap(), ) @@ -37,7 +48,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_local_config_is_empty() { val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = emptyMap(), remote = remoteResponse, ) @@ -49,7 +60,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_remote_config_is_empty() { val expected = localResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = emptyMap(), ) @@ -61,7 +72,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_both_configs_are_not_empty() { val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteResponse, ) @@ -73,7 +84,7 @@ internal class BlockchainProvidersResponseMergerTest { fun test_if_configs_are_equal() { val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = remoteResponse, remote = remoteResponse, ) @@ -88,7 +99,7 @@ internal class BlockchainProvidersResponseMergerTest { val expected = localResponseWithEth + remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponseWithEth, remote = remoteResponse, ) @@ -103,7 +114,7 @@ internal class BlockchainProvidersResponseMergerTest { val eth = "ethereum" to emptyList() val remoteWithEth = remoteResponse + eth - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteWithEth, ) @@ -121,7 +132,7 @@ internal class BlockchainProvidersResponseMergerTest { */ val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteWithoutLocal, ) @@ -137,7 +148,7 @@ internal class BlockchainProvidersResponseMergerTest { val expected = remoteResponse + ("ethereum" to listOf(nowNodesProvider)) - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteWithEth, ) @@ -152,7 +163,7 @@ internal class BlockchainProvidersResponseMergerTest { val expected = remoteResponse - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localResponse, remote = remoteWithEth, ) @@ -170,7 +181,7 @@ internal class BlockchainProvidersResponseMergerTest { val expected = remoteResponse + eth.addSlash() + kaspa.addSlash() - val actual = BlockchainProvidersResponseMerger.merge( + val actual = blockchainProvidersResponseMerger.merge( local = localWithKaspa, remote = remoteWithEth, ) diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts index 577365a357..f780f554c2 100644 --- a/libs/tangem-sdk-api/build.gradle.kts +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.visa.models) + implementation(projects.core.configToggles) implementation(projects.core.res) /** Tangem libraries */ diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkFeatureTogglesModule.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkFeatureTogglesModule.kt new file mode 100644 index 0000000000..a7389193ee --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkFeatureTogglesModule.kt @@ -0,0 +1,18 @@ +package com.tangem.sdk.api.di + +import com.tangem.sdk.api.featuretoggles.CardSdkFeatureToggles +import com.tangem.sdk.api.featuretoggles.DefaultCardSdkFeatureToggles +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 CardSdkFeatureTogglesModule { + + @Binds + @Singleton + fun bindCardSdkFeatureToggles(impl: DefaultCardSdkFeatureToggles): CardSdkFeatureToggles +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt new file mode 100644 index 0000000000..6a6cfde251 --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/di/CardSdkModule.kt @@ -0,0 +1,17 @@ +package com.tangem.sdk.api.di + +import com.tangem.operations.attestation.OnlineCardVerifier +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 CardSdkModule { + + @Provides + @Singleton + fun provideOnlineCardVerifier(): OnlineCardVerifier = OnlineCardVerifier() +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt new file mode 100644 index 0000000000..b90f1577cc --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/CardSdkFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.sdk.api.featuretoggles + +/** + * Card SDK feature toggles + * +[REDACTED_AUTHOR] + */ +interface CardSdkFeatureToggles { + + val isNewAttestationEnabled: Boolean +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt new file mode 100644 index 0000000000..4146304590 --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/featuretoggles/DefaultCardSdkFeatureToggles.kt @@ -0,0 +1,12 @@ +package com.tangem.sdk.api.featuretoggles + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import javax.inject.Inject + +internal class DefaultCardSdkFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : CardSdkFeatureToggles { + + override val isNewAttestationEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "NEW_ATTESTATION_ENABLED") +} \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt index 395904554e..957b817ab8 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/DefaultVisaContractInfoProvider.kt @@ -81,7 +81,14 @@ internal class DefaultVisaContractInfoProvider( { fetchBalances(paymentAccount, paymentToken) }, { fetchLimits(paymentAccount, paymentToken, walletAddress) }, { token, balances, (oldLimit, newLimit, changeDate) -> - VisaContractInfo(token, balances, oldLimit, newLimit, changeDate) + VisaContractInfo( + token = token, + balances = balances, + oldLimits = oldLimit, + newLimits = newLimit, + paymentAccountAddress = paymentAccount.contractAddress, + limitsChangeDate = changeDate, + ) }, ) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt index e50c6e3c89..b15cc9a7e7 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/model/VisaContractInfo.kt @@ -9,6 +9,7 @@ data class VisaContractInfo( val balances: Balances, val oldLimits: Limits, val newLimits: Limits, + val paymentAccountAddress: String, val limitsChangeDate: Instant, ) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 6bda05b59e..0cf1c30675 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -134,9 +134,6 @@ dependencyResolutionManagement { enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":app") -include(":common") -include(":common:ui-charts") -include(":common:routing") // region Core modules include(":core:analytics") @@ -154,6 +151,9 @@ include(":core:pagination") // endregion Core modules // region Common modules +include(":common") +include(":common:ui-charts") +include(":common:routing") include(":common:ui") include(":common:google") // endregion @@ -194,6 +194,8 @@ include(":features:tokendetails:impl") include(":features:send:api") include(":features:send:impl") +include(":features:send-v2:api") +include(":features:send-v2:impl") include(":features:manage-tokens:api") include(":features:manage-tokens:impl") @@ -233,6 +235,9 @@ include(":features:biometry:impl") include(":features:nft:api") include(":features:nft:impl") + +include(":features:walletconnect:api") +include(":features:walletconnect:impl") // endregion Feature modules // region Domain modules @@ -268,6 +273,7 @@ include(":domain:qr-scanning:models") include(":domain:staking") include(":domain:staking:models") include(":domain:wallet-connect") +include(":domain:wallet-connect:models") include(":domain:markets") include(":domain:markets:models") include(":domain:manage-tokens") @@ -275,6 +281,7 @@ include(":domain:manage-tokens:models") include(":domain:onramp") include(":domain:onramp:models") include(":domain:promo") +include(":domain:promo:models") include(":domain:nft") include(":domain:nft:models") // endregion Domain modules @@ -302,5 +309,4 @@ include(":data:markets") include(":data:manage-tokens") include(":data:nft") include(":data:onramp") -// endregion Data modules -include(":domain:promo:models") \ No newline at end of file +// endregion Data modules \ No newline at end of file