diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f08ee1b88d..e6d30a7cc6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -81,7 +81,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.res) implementation(projects.core.ui) implementation(projects.core.datasource) diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index b116caf952..81b845f7bc 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -5,7 +5,8 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.signer.TransactionSignerFactory import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.filter.OneTimeEventFilter -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.connection.NetworkConnectionManager @@ -34,6 +35,7 @@ import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @@ -52,6 +54,8 @@ interface ApplicationEntryPoint { fun getFeatureTogglesManager(): FeatureTogglesManager + fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager + fun getNetworkConnectionManager(): NetworkConnectionManager fun getCardScanningFeatureToggles(): CardScanningFeatureToggles @@ -113,4 +117,6 @@ interface ApplicationEntryPoint { fun getOnrampFeatureToggles(): OnrampFeatureToggles fun getOnboardingV2FeatureToggles(): OnboardingV2FeatureToggles + + fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index e0669f7e68..699817b550 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -74,6 +74,7 @@ import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler +import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.Toast @@ -392,6 +393,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun initIntentHandlers() { val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } + intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler)) intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) intentProcessor.addHandler(WalletConnectLinkIntentHandler()) } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 0e2e83f67f..ddb2c9f1bc 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -17,7 +17,8 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager @@ -57,9 +58,11 @@ import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.runBlocking import org.rekotlin.Store import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository @@ -88,6 +91,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val featureTogglesManager: FeatureTogglesManager get() = entryPoint.getFeatureTogglesManager() + private val excludedBlockchainsManager: ExcludedBlockchainsManager + get() = entryPoint.getExcludedBlockchainsManager() + private val networkConnectionManager: NetworkConnectionManager get() = entryPoint.getNetworkConnectionManager() @@ -180,6 +186,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles get() = entryPoint.getOnboardingV2FeatureToggles() + + private val dispatchers: CoroutineDispatcherProvider + get() = entryPoint.getCoroutineDispatcherProvider() // endregion override fun onCreate() { @@ -200,9 +209,11 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { // TODO: Try to performance and user experience. // [REDACTED_JIRA] runBlocking { - featureTogglesManager.init() - - initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) + awaitAll( + async { featureTogglesManager.init() }, + async { excludedBlockchainsManager.init() }, + async { initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) }, + ) } loadNativeLibraries() @@ -216,7 +227,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { derivationsFinder = DerivationsFinder( appPreferencesStore = appPreferencesStore, - dispatchers = AppCoroutineDispatcherProvider(), + dispatchers = dispatchers, ) appStateHolder.mainStore = store diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt deleted file mode 100644 index f97dca3a02..0000000000 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/MainScreen.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.common.analytics.events - -import com.tangem.core.analytics.models.AnalyticsEvent - -/** -[REDACTED_AUTHOR] - */ -sealed class MainScreen( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent("Main Screen", event, params) { - - class ScreenOpened : MainScreen("Screen opened") - class ButtonScanCard : MainScreen("Button - Scan Card") - - class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen( - event = "Enable Biometric", - params = mapOf("State" to state.value), - ) - - class NoticeRateAppButton(result: AnalyticsParam.RateApp) : MainScreen( - event = "Notice - Rate The App Button Tapped", - params = mapOf("Result" to result.value), - ) -} \ No newline at end of file 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 new file mode 100644 index 0000000000..46c647b06f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Push.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.common.analytics.events + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal sealed class Push(event: String) : AnalyticsEvent( + category = "Push", + event = event, + params = emptyMap(), + error = null, +) { + + data object PushNotificationOpened : Push(event = "Push Notification Opened") +} \ No newline at end of file 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 de174fd193..212969481a 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 @@ -11,7 +11,7 @@ sealed class SignIn( error: Throwable? = null, ) : AnalyticsEvent("Sign In", event, params, error) { - class ScreenOpened : SignIn(event = "Sing In Screen Opened") + class ScreenOpened : SignIn(event = "Sign In Screen Opened") class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In") class ButtonCardSignIn : SignIn(event = "Button - Card Sign In") 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 6ea8b5b88f..d2870bbb6a 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 @@ -12,11 +12,12 @@ internal sealed class WalletConnect( ) : AnalyticsEvent("Wallet Connect", event, params, error) { class ScreenOpened : WalletConnect(event = "WC Screen Opened") - class NewSessionEstablished(dAppName: String, dAppUrl: String) : WalletConnect( + class NewSessionEstablished(dAppName: String, dAppUrl: String, blockchainNames: List) : WalletConnect( event = "New Session Established", params = mapOf( AnalyticsParam.DAPP_NAME to dAppName, AnalyticsParam.DAPP_URL to dAppUrl, + AnalyticsParam.BLOCKCHAIN to blockchainNames.joinToString(","), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index edc221250e..42a7f46c75 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -9,7 +9,6 @@ import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.IntroductionProcess -import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.extensions.inject import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState @@ -31,7 +30,7 @@ class CardContextInterceptor( override fun canBeAppliedTo(event: AnalyticsEvent): Boolean { return when (event) { - is IntroductionProcess.ButtonScanCard, is MainScreen.ButtonScanCard -> false + is IntroductionProcess.ButtonScanCard -> false else -> true } } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt deleted file mode 100644 index 08c1b72a81..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.tap.common.extensions - -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.remove - -fun Blockchain.getNetworkStandardName(): String { - return when (this) { - Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20" - Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20" - Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2" - Blockchain.Tron, Blockchain.TronTestnet -> "TRC20" - Blockchain.TON -> "TON" - else -> "" - } -} - -val Blockchain.networkNameWithoutTestnet - get() = this.getNetworkName().remove(" Testnet") \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 4a207104ca..c80f34373b 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -18,6 +18,7 @@ import com.google.firebase.messaging.RemoteMessage import com.tangem.domain.common.LogConfig import com.tangem.tap.MainActivity import com.tangem.tap.common.images.createCoilImageLoader +import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler import com.tangem.wallet.R import timber.log.Timber @@ -37,6 +38,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { // TODO refactoring: [REDACTED_JIRA] val intent = Intent(applicationContext, MainActivity::class.java) + intent.putExtra(OnPushClickedIntentHandler.IS_OPENED_FROM_PUSH, true) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) val pendingIntent = PendingIntent.getActivity( /* context = */ this, diff --git a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index 7fe3ef0081..e189ec9e68 100644 --- a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.url import android.content.Context +import android.content.Intent import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent @@ -34,6 +35,8 @@ internal class CustomTabsUrlOpener : UrlOpener { ) .build() + customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + customTabsIntent.launchUrl(context, Uri.parse(url)) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt index 12b071ad16..38adc6ec9e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -1,6 +1,6 @@ package com.tangem.tap.di.domain -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase 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 8c5ddc6502..db4247ca10 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 @@ -10,6 +10,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object OnrampDomainModule { @@ -125,4 +126,35 @@ internal object OnrampDomainModule { fun provideGetOnrampQuotesUseCase(onrampRepository: OnrampRepository): GetOnrampQuotesUseCase { return GetOnrampQuotesUseCase(onrampRepository) } + + @Provides + @Singleton + fun provideGetOnrampSelectedPaymentMethodUseCase( + onrampRepository: OnrampRepository, + ): GetOnrampSelectedPaymentMethodUseCase { + return GetOnrampSelectedPaymentMethodUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideGetOnrampProviderWithQuoteUseCase( + onrampRepository: OnrampRepository, + ): GetOnrampProviderWithQuoteUseCase { + return GetOnrampProviderWithQuoteUseCase(onrampRepository) + } + + @Provides + @Singleton + fun provideOnrampSaveSelectedPaymentMethod(onrampRepository: OnrampRepository): OnrampSaveSelectedPaymentMethod { + return OnrampSaveSelectedPaymentMethod(onrampRepository) + } + + @Provides + @Singleton + fun provideGetOnrampRedirectUrlUseCase( + onrampRepository: OnrampRepository, + transactionRepository: OnrampTransactionRepository, + ): GetOnrampRedirectUrlUseCase { + return GetOnrampRedirectUrlUseCase(onrampRepository, transactionRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/CardScanningFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/CardScanningFeatureToggles.kt index 904cfe1673..59310f93b5 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/CardScanningFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/CardScanningFeatureToggles.kt @@ -1,6 +1,6 @@ package com.tangem.tap.domain.scanCard -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager /** * Add custom token feature toggles diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 1128c659f5..2885696de1 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -7,6 +7,7 @@ import com.tangem.common.authentication.storage.AuthenticatedStorage import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.services.secure.SecureStorage import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.createEncryptedSharedPreferences @@ -19,7 +20,6 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricU import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository -import com.tangem.tap.domain.userWalletList.utils.json.* import com.tangem.tap.tangemSdkManager import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/ByteArrayListExtensions.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/ByteArrayListExtensions.kt deleted file mode 100644 index fa3d2aefba..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/ByteArrayListExtensions.kt +++ /dev/null @@ -1 +0,0 @@ -package com.tangem.tap.domain.userWalletList.utils \ No newline at end of file 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 9e980da2ba..bcf6ba147c 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 @@ -267,7 +267,7 @@ internal class DefaultLegacyWalletConnectRepository( ) } - override fun approve(userNamespaces: Map>) { + override fun approve(userNamespaces: Map>, blockchainNames: List) { val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal) val userChains = userNamespaces.flatMap { namespace -> @@ -311,6 +311,7 @@ internal class DefaultLegacyWalletConnectRepository( WalletConnect.NewSessionEstablished( dAppName = sessionProposal.name, dAppUrl = sessionProposal.url, + blockchainNames = blockchainNames, ), ) }, 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 cc4a3e16c4..9b7a44b10d 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 @@ -3,7 +3,7 @@ package com.tangem.tap.domain.walletconnect2.di import android.app.Application import com.squareup.moshi.Moshi import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.files.FileReader import com.tangem.domain.tokens.repository.CurrenciesRepository diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 616643a960..d755da5aea 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -21,7 +21,7 @@ interface LegacyWalletConnectRepository { fun disconnect(topic: String) - fun approve(userNamespaces: Map>) + fun approve(userNamespaces: Map>, blockchainNames: List) fun reject() diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 9d3e956799..e53938eaf6 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.walletconnect2.domain +import arrow.core.flatten import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.domain.tokens.model.CryptoCurrency @@ -236,8 +237,12 @@ class WalletConnectInteractor( blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId) ?.let { NetworkNamespace(it) } }.filterNotNull() + + val blockchainNames = blockchainHelper.chainIdsToBlockchains(userNamespaces.values.flatten().map { it.chainId }) + .map { it.fullName } walletConnectRepository.approve( userNamespaces = userNamespaces, + blockchainNames = blockchainNames, ) } 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 26547d68e8..a6da04800a 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 @@ -1,6 +1,6 @@ package com.tangem.tap.domain.walletconnect2.toggles -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager internal class WalletConnectFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, diff --git a/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt index ce8c0bcec3..55ebe3e10d 100644 --- a/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/features/home/featuretoggles/HomeFeatureToggles.kt @@ -1,6 +1,6 @@ package com.tangem.tap.features.home.featuretoggles -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import javax.inject.Inject class HomeFeatureToggles @Inject constructor( diff --git a/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/OnPushClickedIntentHandler.kt b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/OnPushClickedIntentHandler.kt new file mode 100644 index 0000000000..e21d25ea54 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/intentHandler/handlers/OnPushClickedIntentHandler.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.features.intentHandler.handlers + +import android.content.Intent +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.tap.common.analytics.events.Push +import com.tangem.tap.features.intentHandler.IntentHandler + +internal class OnPushClickedIntentHandler(val analyticsEventHandler: AnalyticsEventHandler) : IntentHandler { + + override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean { + val fromPush = intent?.extras?.getBoolean(IS_OPENED_FROM_PUSH) ?: false + + return if (fromPush) { + analyticsEventHandler.send(Push.PushNotificationOpened) + true + } else { + false + } + } + + companion object { + const val IS_OPENED_FROM_PUSH = "IS_OPENED_FROM_PUSH" + } +} \ No newline at end of file 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 c8e093611d..2c7903ad60 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 @@ -5,11 +5,11 @@ import com.tangem.common.core.TangemSdkError 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.* import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchWithMain @@ -110,7 +110,11 @@ internal class SaveWalletMiddleware { // TODO: Remove after onboarding refactoring Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On)) } else { - Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) + Analytics.send( + MainScreenAnalyticsEvent.EnableBiometrics( + state = com.tangem.core.analytics.models.AnalyticsParam.OnOffState.On, + ), + ) } /* @@ -148,7 +152,11 @@ internal class SaveWalletMiddleware { // TODO: Remove after onboarding refactoring Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.Off)) } else { - Analytics.send(MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.Off)) + Analytics.send( + MainScreenAnalyticsEvent.EnableBiometrics( + com.tangem.core.analytics.models.AnalyticsParam.OnOffState.Off, + ), + ) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index b68935424e..be04d0a426 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -56,14 +56,14 @@ object TradeCryptoMiddleware { private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled if (isOnrampEnabled) { - proceedWithOnramp(action.cryptoCurrencyStatus.currency) + proceedWithOnramp(action.userWallet.walletId, action.cryptoCurrencyStatus.currency) } else { proceedWithLegacyBuyAction(state, action) } } - private fun proceedWithOnramp(cryptoCurrency: CryptoCurrency) { - store.dispatchNavigationAction { push(AppRoute.Onramp(cryptoCurrency)) } + private fun proceedWithOnramp(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency) { + store.dispatchNavigationAction { push(AppRoute.Onramp(userWalletId = userWalletId, currency = cryptoCurrency)) } } private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { 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 46a9c70161..59331b1445 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 @@ -197,7 +197,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.Onramp -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = OnrampComponent.Params(route.currency), + params = OnrampComponent.Params(userWalletId = route.userWalletId, cryptoCurrency = route.currency), componentFactory = onrampComponentFactory, ) } 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 da4f005d32..fc52efb20d 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 @@ -295,7 +295,10 @@ sealed class AppRoute(val path: String) : Route { } @Serializable - data class Onramp(val currency: CryptoCurrency) : AppRoute(path = "/onramp/${currency.symbol}"), RouteBundleParams { + data class Onramp( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + ) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) } @@ -322,6 +325,7 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/swap_crypto/${userWalletId.stringValue}") // Onboarding V2 + @Serializable data class Onboarding( val scanResponse: ScanResponse, val startFromBackup: Boolean, diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index b646142403..ce733b8c51 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -76,6 +76,9 @@ sealed class AnalyticsParam { data object MyWallets : ScreensSources("My Wallets") data object Token : ScreensSources("Token") data object Stories : ScreensSources("Stories") + data object Buy : ScreensSources("Buy") + data object Swap : ScreensSources("Swap") + data object Sell : ScreensSources("Sell") } sealed class TxSentFrom(val value: String) { @@ -161,6 +164,12 @@ sealed class AnalyticsParam { } } + enum class Status(val value: String) { + Success(value = "Success"), + Error(value = "Error"), + Pending(value = "Pending"), + } + companion object Key { const val BLOCKCHAIN = "blockchain" const val TOKEN_PARAM = "Token" diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt new file mode 100644 index 0000000000..02c1a6d4c0 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt @@ -0,0 +1,75 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +/** + * Main screen analytics event + * + * @param event event name + * @param params params + */ +sealed class MainScreenAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { + + class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreenAnalyticsEvent( + event = "Enable Biometric", + params = mapOf("State" to state.value), + ) + + // region Action Buttons feature + data class ButtonBuy(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent( + event = "Button - Buy", + params = mapOf(AnalyticsParam.STATUS to status.value), + ) + + data class ButtonSwap(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent( + event = "Button - Swap", + params = mapOf(AnalyticsParam.STATUS to status.value), + ) + + data class ButtonSell(val status: AnalyticsParam.Status) : MainScreenAnalyticsEvent( + event = "Button - Sell", + params = mapOf(AnalyticsParam.STATUS to status.value), + ) + + data object BuyScreenOpened : MainScreenAnalyticsEvent(event = "Buy Screen Opened") + + data object SwapScreenOpened : MainScreenAnalyticsEvent(event = "Swap Screen Opened") + + data object SellScreenOpened : MainScreenAnalyticsEvent(event = "Sell Screen Opened") + + data class BuyTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent( + event = "Buy Token Clicked", + params = mapOf(TOKEN_PARAM to currencySymbol), + ) + + data class SellTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent( + event = "Sell Token Clicked", + params = mapOf(TOKEN_PARAM to currencySymbol), + ) + + data class SwapTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent( + event = "Swap Token Clicked", + params = mapOf(TOKEN_PARAM to currencySymbol), + ) + + data class ReceiveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent( + event = "Receive Token Clicked", + params = mapOf(TOKEN_PARAM to currencySymbol), + ) + + data class RemoveTokenClicked(val currencySymbol: String) : MainScreenAnalyticsEvent( + event = "Remove Button Clicked", + params = mapOf(TOKEN_PARAM to currencySymbol), + ) + + data class ButtonClose(val source: AnalyticsParam.ScreensSources) : MainScreenAnalyticsEvent( + event = "Button - Close", + params = mapOf(AnalyticsParam.SOURCE to source.value), + ) + // endregion +} \ No newline at end of file diff --git a/core/featuretoggles/.gitignore b/core/config-toggles/.gitignore similarity index 100% rename from core/featuretoggles/.gitignore rename to core/config-toggles/.gitignore diff --git a/core/featuretoggles/build.gradle.kts b/core/config-toggles/build.gradle.kts similarity index 94% rename from core/featuretoggles/build.gradle.kts rename to core/config-toggles/build.gradle.kts index 410ef17d0e..0a7bc83fdc 100644 --- a/core/featuretoggles/build.gradle.kts +++ b/core/config-toggles/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } android { - namespace = "com.tangem.core.featuretoggles" + namespace = "com.tangem.core.configtoggle" } dependencies { diff --git a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json new file mode 100644 index 0000000000..4113610e04 --- /dev/null +++ b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json @@ -0,0 +1,10 @@ +[ + { + "name": "", + "version": "undefined" + }, + { + "name": "NEXA", + "version": "undefined" + } +] \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json similarity index 100% rename from core/featuretoggles/src/main/assets/configs/feature_toggles_config.json rename to core/config-toggles/src/main/assets/configs/feature_toggles_config.json diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/ExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/ExcludedBlockchainsManager.kt new file mode 100644 index 0000000000..f5db3637b9 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/ExcludedBlockchainsManager.kt @@ -0,0 +1,8 @@ +package com.tangem.core.configtoggle.blockchain + +interface ExcludedBlockchainsManager { + + val excludedBlockchainsIds: Set + + suspend fun init() +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/MutableExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/MutableExcludedBlockchainsManager.kt new file mode 100644 index 0000000000..1f7fcf4dc0 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/MutableExcludedBlockchainsManager.kt @@ -0,0 +1,10 @@ +package com.tangem.core.configtoggle.blockchain + +interface MutableExcludedBlockchainsManager : ExcludedBlockchainsManager { + + suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) + + fun isMatchLocalConfig(): Boolean + + suspend fun recoverLocalConfig() +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt new file mode 100644 index 0000000000..d0f1dccc44 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt @@ -0,0 +1,74 @@ +package com.tangem.core.configtoggle.blockchain.impl + +import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager +import com.tangem.core.configtoggle.storage.TogglesStorage +import com.tangem.core.configtoggle.utils.associateToggles +import com.tangem.core.configtoggle.version.VersionProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store + +internal class DefaultExcludedBlockchainsManager( + private val localTogglesStorage: TogglesStorage, + private val appPreferencesStore: AppPreferencesStore, + private val versionProvider: VersionProvider, +) : MutableExcludedBlockchainsManager { + + private var isInitialized: Boolean = false + + private lateinit var currentExcludedBlockchainsIds: Set + private lateinit var localExcludedBlockchainsIds: Set + + override val excludedBlockchainsIds: Set + get() { + if (!isInitialized) error("ExcludedBlockchainsManager is not initialized") + + return currentExcludedBlockchainsIds + } + + override suspend fun init() { + localTogglesStorage.populate(path = "configs/excluded_blockchains_config") + + val storedExcludedBlockchainsIds = appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY, + default = emptySet(), + ) + + localExcludedBlockchainsIds = localTogglesStorage.toggles + .associateToggles(currentVersion = versionProvider.get().orEmpty()) + .filterValues { isIncluded -> !isIncluded } + .keys + + currentExcludedBlockchainsIds = localExcludedBlockchainsIds + storedExcludedBlockchainsIds + + isInitialized = true + } + + override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) { + if (isExcluded) { + currentExcludedBlockchainsIds += mainnetId + } else { + currentExcludedBlockchainsIds -= mainnetId + } + + storeCurrent() + } + + override fun isMatchLocalConfig(): Boolean { + return currentExcludedBlockchainsIds == localExcludedBlockchainsIds + } + + override suspend fun recoverLocalConfig() { + currentExcludedBlockchainsIds = localExcludedBlockchainsIds + + storeCurrent() + } + + private suspend fun storeCurrent() { + appPreferencesStore.store( + PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY, + currentExcludedBlockchainsIds, + ) + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt new file mode 100644 index 0000000000..2e4fcddac5 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/ExcludedBlockchainsManagerModule.kt @@ -0,0 +1,49 @@ +package com.tangem.core.configtoggle.di + +import android.content.Context +import com.tangem.core.configtoggle.BuildConfig +import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager +import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager +import com.tangem.core.configtoggle.blockchain.impl.DefaultExcludedBlockchainsManager +import com.tangem.core.configtoggle.storage.LocalTogglesStorage +import com.tangem.core.configtoggle.version.DefaultVersionProvider +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.local.preferences.AppPreferencesStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object ExcludedBlockchainsManagerModule { + + @Provides + @Singleton + fun provideExcludedBlockchainsManager( + @ApplicationContext context: Context, + assetLoader: AssetLoader, + appPreferencesStore: AppPreferencesStore, + ): ExcludedBlockchainsManager { + val localTogglesStorage = LocalTogglesStorage(assetLoader) + val versionProvider = DefaultVersionProvider(context) + + return DefaultExcludedBlockchainsManager( + localTogglesStorage, + appPreferencesStore, + versionProvider, + ) + } + + @Provides + @Singleton + fun provideMutableExcludedBlockchainsManager( + manager: ExcludedBlockchainsManager, + ): MutableExcludedBlockchainsManager? { + if (!BuildConfig.TESTER_MENU_ENABLED) return null + + return manager as MutableExcludedBlockchainsManager + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt similarity index 62% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt index ca45c78a6b..8346378b52 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/di/FeatureTogglesManagerModule.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/di/FeatureTogglesManagerModule.kt @@ -1,12 +1,12 @@ -package com.tangem.core.featuretoggle.di +package com.tangem.core.configtoggle.di import android.content.Context -import com.tangem.core.featuretoggle.manager.DevFeatureTogglesManager -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.core.featuretoggle.manager.ProdFeatureTogglesManager -import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage -import com.tangem.core.featuretoggle.version.DefaultVersionProvider -import com.tangem.core.featuretoggles.BuildConfig +import com.tangem.core.configtoggle.BuildConfig +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager +import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager +import com.tangem.core.configtoggle.storage.LocalTogglesStorage +import com.tangem.core.configtoggle.version.DefaultVersionProvider import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.preferences.AppPreferencesStore import dagger.Module @@ -27,18 +27,18 @@ internal object FeatureTogglesManagerModule { assetLoader: AssetLoader, appPreferencesStore: AppPreferencesStore, ): FeatureTogglesManager { - val localFeatureTogglesStorage = LocalFeatureTogglesStorage(assetLoader) + val localTogglesStorage = LocalTogglesStorage(assetLoader) val versionProvider = DefaultVersionProvider(context) return if (BuildConfig.TESTER_MENU_ENABLED) { DevFeatureTogglesManager( - localFeatureTogglesStorage = localFeatureTogglesStorage, + localTogglesStorage = localTogglesStorage, appPreferencesStore = appPreferencesStore, versionProvider = versionProvider, ) } else { ProdFeatureTogglesManager( - localFeatureTogglesStorage = localFeatureTogglesStorage, + localTogglesStorage = localTogglesStorage, versionProvider = versionProvider, ) } diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesManager.kt similarity index 86% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesManager.kt index 6111e7f73b..86a027924c 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/FeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/FeatureTogglesManager.kt @@ -1,4 +1,4 @@ -package com.tangem.core.featuretoggle.manager +package com.tangem.core.configtoggle.feature /** * Component for getting information about the availability of feature toggles diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/MutableFeatureTogglesManager.kt similarity index 92% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/MutableFeatureTogglesManager.kt index f04b423f4f..d1d09ced03 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/MutableFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/MutableFeatureTogglesManager.kt @@ -1,4 +1,4 @@ -package com.tangem.core.featuretoggle.manager +package com.tangem.core.configtoggle.feature /** * Component for change information about the availability of feature toggles diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt similarity index 81% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt index 303dce3c7b..750dbada43 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/DevFeatureTogglesManager.kt @@ -1,9 +1,10 @@ -package com.tangem.core.featuretoggle.manager +package com.tangem.core.configtoggle.feature.impl import androidx.annotation.VisibleForTesting -import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage -import com.tangem.core.featuretoggle.utils.associateToggles -import com.tangem.core.featuretoggle.version.VersionProvider +import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager +import com.tangem.core.configtoggle.storage.TogglesStorage +import com.tangem.core.configtoggle.utils.associateToggles +import com.tangem.core.configtoggle.version.VersionProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull @@ -13,12 +14,12 @@ import kotlin.properties.Delegates /** * Feature toggles manager implementation in DEV build * - * @property localFeatureTogglesStorage local feature toggles storage + * @property localTogglesStorage local feature toggles storage * @property appPreferencesStore application local store * @property versionProvider application version provider */ internal class DevFeatureTogglesManager( - private val localFeatureTogglesStorage: FeatureTogglesStorage, + private val localTogglesStorage: TogglesStorage, private val appPreferencesStore: AppPreferencesStore, private val versionProvider: VersionProvider, ) : MutableFeatureTogglesManager { @@ -27,13 +28,13 @@ internal class DevFeatureTogglesManager( private var localFeatureTogglesMap: Map by Delegates.notNull() override suspend fun init() { - localFeatureTogglesStorage.init() + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull>( key = PreferencesKeys.FEATURE_TOGGLES_KEY, ) ?: emptyMap() - val localFeatureToggles = localFeatureTogglesStorage.featureToggles + val localFeatureToggles = localTogglesStorage.toggles .associateToggles(currentVersion = versionProvider.get().orEmpty()) localFeatureTogglesMap = localFeatureToggles diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/FeatureTogglesConstants.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/FeatureTogglesConstants.kt new file mode 100644 index 0000000000..63087a2e90 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/FeatureTogglesConstants.kt @@ -0,0 +1,6 @@ +package com.tangem.core.configtoggle.feature.impl + +internal object FeatureTogglesConstants { + + const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config" +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt similarity index 62% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt index 0f88c3791c..d723297a45 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/feature/impl/ProdFeatureTogglesManager.kt @@ -1,27 +1,28 @@ -package com.tangem.core.featuretoggle.manager +package com.tangem.core.configtoggle.feature.impl import androidx.annotation.VisibleForTesting -import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage -import com.tangem.core.featuretoggle.utils.associateToggles -import com.tangem.core.featuretoggle.version.VersionProvider +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.configtoggle.storage.TogglesStorage +import com.tangem.core.configtoggle.utils.associateToggles +import com.tangem.core.configtoggle.version.VersionProvider import kotlin.properties.Delegates /** * Feature toggles manager implementation in PROD build * - * @property localFeatureTogglesStorage local feature toggles storage + * @property localTogglesStorage local feature toggles storage * @property versionProvider application version provider */ internal class ProdFeatureTogglesManager( - private val localFeatureTogglesStorage: FeatureTogglesStorage, + private val localTogglesStorage: TogglesStorage, private val versionProvider: VersionProvider, ) : FeatureTogglesManager { private var featureToggles: Map by Delegates.notNull() override suspend fun init() { - localFeatureTogglesStorage.init() - featureToggles = localFeatureTogglesStorage.featureToggles + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) + featureToggles = localTogglesStorage.toggles .associateToggles(currentVersion = versionProvider.get() ?: "") } diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/ConfigToggle.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/ConfigToggle.kt new file mode 100644 index 0000000000..6078eea70a --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/ConfigToggle.kt @@ -0,0 +1,20 @@ +package com.tangem.core.configtoggle.storage + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Data model with information about config toggle + * + * @property name toggle name + * @property version version in which the toggle will be enabled + * + * IMPORTANT: if the version is "undefined", it means that toggle is disabled! + * +[REDACTED_AUTHOR] + */ +@JsonClass(generateAdapter = true) +internal data class ConfigToggle( + @Json(name = "name") val name: String, + @Json(name = "version") val version: String, +) \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorage.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorage.kt new file mode 100644 index 0000000000..59086d9658 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorage.kt @@ -0,0 +1,24 @@ +package com.tangem.core.configtoggle.storage + +import com.tangem.datasource.asset.loader.AssetLoader +import kotlin.properties.Delegates + +/** + * Storage implementation for storing local feature toggles. + * Feature toggles are declared in file [LOCAL_CONFIG_PATH]. + * + * @property assetLoader asset loader + * +[REDACTED_AUTHOR] + */ +internal class LocalTogglesStorage( + private val assetLoader: AssetLoader, +) : TogglesStorage { + + override var toggles: List by Delegates.notNull() + private set + + override suspend fun populate(path: String) { + toggles = assetLoader.loadList(path) + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/TogglesStorage.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/TogglesStorage.kt new file mode 100644 index 0000000000..39fa7b2f68 --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/storage/TogglesStorage.kt @@ -0,0 +1,15 @@ +package com.tangem.core.configtoggle.storage + +/** + * Component that initializes and stores a list of feature toggles + * +[REDACTED_AUTHOR] + */ +internal interface TogglesStorage { + + /** List of feature toggles */ + val toggles: List + + /** Populate the storage with toggles */ + suspend fun populate(path: String) +} \ No newline at end of file diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt new file mode 100644 index 0000000000..68ee66b88c --- /dev/null +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/utils/CollectionExt.kt @@ -0,0 +1,13 @@ +package com.tangem.core.configtoggle.utils + +import com.tangem.core.configtoggle.storage.ConfigToggle +import com.tangem.core.configtoggle.version.VersionAvailabilityContract + +internal fun List.associateToggles(currentVersion: String): Map { + return associate { localToggle -> + Pair( + first = localToggle.name, + second = VersionAvailabilityContract(currentVersion, localToggle.version), + ) + } +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/DefaultVersionProvider.kt similarity index 96% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/DefaultVersionProvider.kt index c602be463f..b55e9637c3 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/DefaultVersionProvider.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/DefaultVersionProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.core.featuretoggle.version +package com.tangem.core.configtoggle.version import android.content.Context import android.content.pm.PackageManager diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt similarity index 98% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt index b506d80ec4..0822cec520 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/Version.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt @@ -1,4 +1,4 @@ -package com.tangem.core.featuretoggle.contract +package com.tangem.core.configtoggle.version import androidx.annotation.VisibleForTesting import timber.log.Timber diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/VersionAvailabilityContract.kt similarity index 93% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/VersionAvailabilityContract.kt index 2ee5c72e14..2360052af4 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContract.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/VersionAvailabilityContract.kt @@ -1,4 +1,4 @@ -package com.tangem.core.featuretoggle.contract +package com.tangem.core.configtoggle.version /** * Version contract to evaluate availability of feature toggle diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/VersionProvider.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/VersionProvider.kt similarity index 74% rename from core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/VersionProvider.kt rename to core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/VersionProvider.kt index 585caf85b8..27913b7dce 100644 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/version/VersionProvider.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/VersionProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.core.featuretoggle.version +package com.tangem.core.configtoggle.version /** Application version provider */ internal interface VersionProvider { diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/contract/VersionAvailabilityContractTest.kt similarity index 93% rename from core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt rename to core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/contract/VersionAvailabilityContractTest.kt index 476c967367..ba641dfd04 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionAvailabilityContractTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/contract/VersionAvailabilityContractTest.kt @@ -1,6 +1,7 @@ -package com.tangem.core.featuretoggle.contract +package com.tangem.core.configtoggle.contract import com.google.common.truth.Truth +import com.tangem.core.configtoggle.version.VersionAvailabilityContract import org.junit.Test /** diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/contract/VersionTest.kt similarity index 97% rename from core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt rename to core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/contract/VersionTest.kt index f8763c6a00..e61ace5722 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/contract/VersionTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/contract/VersionTest.kt @@ -1,6 +1,7 @@ -package com.tangem.core.featuretoggle.contract +package com.tangem.core.configtoggle.contract import com.google.common.truth.Truth +import com.tangem.core.configtoggle.version.Version import org.junit.Test /** diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt similarity index 78% rename from core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt rename to core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt index e0e8fbef2f..b1822c84d9 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/DevFeatureTogglesManagerTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/DevTogglesManagerTest.kt @@ -1,11 +1,13 @@ -package com.tangem.core.featuretoggle.manager +package com.tangem.core.configtoggle.manager import android.annotation.SuppressLint import com.google.common.truth.Truth -import com.tangem.core.featuretoggle.storage.FeatureToggle -import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage -import com.tangem.core.featuretoggle.utils.associateToggles -import com.tangem.core.featuretoggle.version.VersionProvider +import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager +import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants +import com.tangem.core.configtoggle.storage.ConfigToggle +import com.tangem.core.configtoggle.storage.TogglesStorage +import com.tangem.core.configtoggle.utils.associateToggles +import com.tangem.core.configtoggle.version.VersionProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull @@ -19,13 +21,13 @@ import kotlin.collections.set [REDACTED_AUTHOR] */ @SuppressLint("CheckResult") -internal class DevFeatureTogglesManagerTest { +internal class DevTogglesManagerTest { - private val localFeatureTogglesStorage = mockk() + private val localTogglesStorage = mockk() private val appPreferenceStore = mockk(relaxed = true) private val versionProvider = mockk() private val manager = DevFeatureTogglesManager( - localFeatureTogglesStorage = localFeatureTogglesStorage, + localTogglesStorage = localTogglesStorage, appPreferencesStore = appPreferenceStore, versionProvider = versionProvider, ) @@ -34,17 +36,17 @@ internal class DevFeatureTogglesManagerTest { fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest { val currentVersion = "0.1.0" - coEvery { localFeatureTogglesStorage.init() } just Runs + coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs coEvery { appPreferenceStore.getObjectSyncOrNull>(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureTogglesMap - coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { localTogglesStorage.toggles } returns localFeatureToggles coEvery { versionProvider.get() } returns currentVersion manager.init() coVerifyOrder { - localFeatureTogglesStorage.init() + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) versionProvider.get() } @@ -61,17 +63,17 @@ internal class DevFeatureTogglesManagerTest { fun `successfully initialize storage if shared prefs kept empty list`() = runTest { val currentVersion = "0.1.0" - coEvery { localFeatureTogglesStorage.init() } just Runs + coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs coEvery { appPreferenceStore.getObjectSyncOrNull>(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns emptyMap() - coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { localTogglesStorage.toggles } returns localFeatureToggles coEvery { versionProvider.get() } returns currentVersion manager.init() coVerifyOrder { - localFeatureTogglesStorage.init() + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) versionProvider.get() } @@ -88,17 +90,17 @@ internal class DevFeatureTogglesManagerTest { fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest { val currentVersion = "0.1.0" - coEvery { localFeatureTogglesStorage.init() } just Runs + coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs coEvery { appPreferenceStore.getObjectSyncOrNull>(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns null - coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { localTogglesStorage.toggles } returns localFeatureToggles coEvery { versionProvider.get() } returns currentVersion manager.init() coVerifyOrder { - localFeatureTogglesStorage.init() + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) versionProvider.get() } @@ -111,15 +113,15 @@ internal class DevFeatureTogglesManagerTest { @Test fun `successfully initialize storage if versionProvider returns null`() = runTest { - coEvery { localFeatureTogglesStorage.init() } just Runs + coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles - coEvery { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { localTogglesStorage.toggles } returns localFeatureToggles coEvery { versionProvider.get() } returns null manager.init() coVerifyOrder { - localFeatureTogglesStorage.init() + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) versionProvider.get() } @@ -222,8 +224,8 @@ internal class DevFeatureTogglesManagerTest { ) val localFeatureToggles = listOf( - FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), - FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), + ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), + ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), ) } } \ No newline at end of file diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdTogglesManagerTest.kt similarity index 64% rename from core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt rename to core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdTogglesManagerTest.kt index 35af42ae54..6453d3eb4d 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/manager/ProdFeatureTogglesManagerTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/manager/ProdTogglesManagerTest.kt @@ -1,11 +1,13 @@ -package com.tangem.core.featuretoggle.manager +package com.tangem.core.configtoggle.manager import android.content.pm.PackageManager import com.google.common.truth.Truth -import com.tangem.core.featuretoggle.storage.FeatureToggle -import com.tangem.core.featuretoggle.storage.FeatureTogglesStorage -import com.tangem.core.featuretoggle.utils.associateToggles -import com.tangem.core.featuretoggle.version.VersionProvider +import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants +import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager +import com.tangem.core.configtoggle.storage.ConfigToggle +import com.tangem.core.configtoggle.storage.TogglesStorage +import com.tangem.core.configtoggle.utils.associateToggles +import com.tangem.core.configtoggle.version.VersionProvider import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.Test @@ -13,24 +15,24 @@ import org.junit.Test /** [REDACTED_AUTHOR] */ -internal class ProdFeatureTogglesManagerTest { +internal class ProdTogglesManagerTest { - private val localFeatureTogglesStorage = mockk() + private val localTogglesStorage = mockk() private val versionProvider = mockk() - private val manager = ProdFeatureTogglesManager(localFeatureTogglesStorage, versionProvider) + private val manager = ProdFeatureTogglesManager(localTogglesStorage, versionProvider) @Test fun `successfully initialize storage`() = runTest { val currentVersion = "1.0.0" - coEvery { localFeatureTogglesStorage.init() } just Runs - every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs + every { localTogglesStorage.toggles } returns localFeatureToggles every { versionProvider.get() } returns currentVersion manager.init() coVerifyOrder { - localFeatureTogglesStorage.init() + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) versionProvider.get() } @@ -40,14 +42,14 @@ internal class ProdFeatureTogglesManagerTest { @Test fun `successfully initialize storage if versionProvider returns null`() = runTest { - coEvery { localFeatureTogglesStorage.init() } just Runs - every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs + every { localTogglesStorage.toggles } returns localFeatureToggles every { versionProvider.get() } returns null manager.init() coVerifyOrder { - localFeatureTogglesStorage.init() + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) versionProvider.get() } @@ -56,8 +58,8 @@ internal class ProdFeatureTogglesManagerTest { @Test fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest { - coEvery { localFeatureTogglesStorage.init() } just Runs - every { localFeatureTogglesStorage.featureToggles } throws IllegalStateException( + coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs + every { localTogglesStorage.toggles } throws IllegalStateException( "Property featureToggles should be initialized before get.", ) @@ -72,14 +74,14 @@ internal class ProdFeatureTogglesManagerTest { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) } - coVerifyOrder { localFeatureTogglesStorage.init() } + coVerifyOrder { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } verifyAll(inverse = true) { versionProvider.get() } } @Test fun `failure initialize storage if versionProvider throws exception`() = runTest { - coEvery { localFeatureTogglesStorage.init() } just Runs - every { localFeatureTogglesStorage.featureToggles } returns localFeatureToggles + coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs + every { localTogglesStorage.toggles } returns localFeatureToggles every { versionProvider.get() } throws PackageManager.NameNotFoundException() runCatching { manager.init() } @@ -89,7 +91,7 @@ internal class ProdFeatureTogglesManagerTest { } coVerifyOrder { - localFeatureTogglesStorage.init() + localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) versionProvider.get() } } @@ -122,8 +124,8 @@ internal class ProdFeatureTogglesManagerTest { private companion object { val localFeatureToggles = listOf( - FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), - FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), + ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), + ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), ) val disabledFeatureToggles = mapOf( diff --git a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt similarity index 71% rename from core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt rename to core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt index 3950e2c649..4a8e6550b8 100644 --- a/core/featuretoggles/src/test/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorageTest.kt +++ b/core/config-toggles/src/test/kotlin/com/tangem/core/configtoggle/storage/LocalTogglesStorageTest.kt @@ -1,10 +1,11 @@ -package com.tangem.core.featuretoggle.storage +package com.tangem.core.configtoggle.storage import android.annotation.SuppressLint import com.google.common.truth.Truth import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi import com.squareup.moshi.Types +import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.asset.reader.AssetReader import io.mockk.* @@ -16,16 +17,16 @@ import java.io.IOException [REDACTED_AUTHOR] */ @SuppressLint("CheckResult") -internal class LocalFeatureTogglesStorageTest { +internal class LocalTogglesStorageTest { private val assetReader = mockk() private val moshi = mockk() - private val jsonAdapter = mockk>>() + private val jsonAdapter = mockk>>() // Impossible to mockk AssetLoader because it implement inline functions private val assetLoader = AssetLoader(assetReader = assetReader, moshi = moshi) - private val storage = LocalFeatureTogglesStorage(assetLoader) + private val storage = LocalTogglesStorage(assetLoader) @Test fun `successfully initialize storage`() = runTest { @@ -33,14 +34,14 @@ internal class LocalFeatureTogglesStorageTest { everyCreatingMoshiAdapter() returns jsonAdapter everyMappingJson() returns featureToggles - storage.init() + storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) jsonAdapter.fromJson(json) } - Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(featureToggles) + Truth.assertThat(storage.toggles).containsExactlyElementsIn(featureToggles) } @Test @@ -49,33 +50,33 @@ internal class LocalFeatureTogglesStorageTest { everyCreatingMoshiAdapter() returns jsonAdapter everyMappingJson() throws IOException() - storage.init() + storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) jsonAdapter.fromJson(json) } - Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList()) + Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList()) } @Test fun `failure initialize storage if jsonAdapter throws exception`() = runTest { everyReadingJson() throws IOException() - storage.init() + storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) } verifyAll(inverse = true) { jsonAdapter.fromJson(any()) } - Truth.assertThat(storage.featureToggles).containsExactlyElementsIn(emptyList()) + Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList()) } private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) } private fun everyCreatingMoshiAdapter() = every { - val types = Types.newParameterizedType(List::class.java, FeatureToggle::class.java) - moshi.adapter>(types) + val types = Types.newParameterizedType(List::class.java, ConfigToggle::class.java) + moshi.adapter>(types) } private fun everyMappingJson() = every { jsonAdapter.fromJson(json) } @@ -97,8 +98,8 @@ internal class LocalFeatureTogglesStorageTest { """.trimIndent() val featureToggles = listOf( - FeatureToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), - FeatureToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), + ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"), + ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"), ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt index 3b27e36068..3146bb435d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampDataResponse.kt @@ -20,6 +20,18 @@ data class OnrampDataResponse( val signature: String, ) +@JsonClass(generateAdapter = true) +data class OnrampDataJson( + @Json(name = "widgetUrl") + val widgetUrl: String, + + @Json(name = "externalTxId") + val externalTxId: String, + + @Json(name = "externalTxUrl") + val externalTxUrl: String?, +) + @JsonClass(generateAdapter = true) data class OnrampTxDetails( @Json(name = "fromCurrencyCode") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt index 509352375a..46ca99bbb6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampQuoteResponse.kt @@ -39,8 +39,8 @@ data class OnrampQuoteResponse( val maxFromAmount: String, @Json(name = "minToAmount") - val minToAmount: String, + val minToAmount: String?, @Json(name = "maxToAmount") - val maxToAmount: String, + val maxToAmount: String?, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/onramp/paymentmethods/OnrampPaymentMethodsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/onramp/paymentmethods/OnrampPaymentMethodsStore.kt index fc1eca40d4..6626726313 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/onramp/paymentmethods/OnrampPaymentMethodsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/onramp/paymentmethods/OnrampPaymentMethodsStore.kt @@ -1,11 +1,14 @@ package com.tangem.datasource.local.onramp.paymentmethods import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO +import kotlinx.coroutines.flow.Flow interface OnrampPaymentMethodsStore { suspend fun getSyncOrNull(key: String): List? + fun get(key: String): Flow> + suspend fun store(key: String, value: List) suspend fun contains(key: String): Boolean diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 5fd333db8a..cf9ecaf3f0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -55,6 +55,8 @@ object PreferencesKeys { val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } + val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringSetPreferencesKey(name = "excludedBlockchains") } + val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } val IS_TANGEM_TOS_ACCEPTED_KEY by lazy { booleanPreferencesKey(name = "tangem_tos_accepted") } diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt index 3c31ec55a7..9282116415 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt @@ -2,11 +2,13 @@ package com.tangem.core.deeplink.global import com.tangem.core.deeplink.DeepLink -class BuyCurrencyDeepLink(val onReceive: () -> Unit) : DeepLink { +class BuyCurrencyDeepLink(val onReceive: (txId: String) -> Unit) : DeepLink { override val uri: String = "tangem://redirect?action=dismissBrowser" override fun onReceive(params: Map) { - onReceive() + onReceive( + params["txId"] ?: return, + ) } } \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureToggle.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureToggle.kt deleted file mode 100644 index b019bfc810..0000000000 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureToggle.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.core.featuretoggle.storage - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -/** - * Data model with information about feature toggle - * - * @property name feature toggle name - * @property version version number in which the feature will be included - * - * IMPORTANT: if the version is "undefined", it means that feature toggle is disabled! - * -[REDACTED_AUTHOR] - */ -@JsonClass(generateAdapter = true) -internal data class FeatureToggle( - @Json(name = "name") val name: String, - @Json(name = "version") val version: String, -) \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt deleted file mode 100644 index dd788efacb..0000000000 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/FeatureTogglesStorage.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.core.featuretoggle.storage - -/** - * Component that initializes and stores a list of feature toggles - * -[REDACTED_AUTHOR] - */ -internal interface FeatureTogglesStorage { - - /** List of feature toggles */ - val featureToggles: List - - /** Initialize storage */ - suspend fun init() -} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt deleted file mode 100644 index dbd92dee48..0000000000 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/storage/LocalFeatureTogglesStorage.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.core.featuretoggle.storage - -import com.tangem.core.featuretoggle.storage.LocalFeatureTogglesStorage.Companion.LOCAL_CONFIG_PATH -import com.tangem.datasource.asset.loader.AssetLoader -import kotlin.properties.Delegates - -/** - * Storage implementation for storing local feature toggles. - * Feature toggles are declared in file [LOCAL_CONFIG_PATH]. - * - * @property assetLoader asset loader - * -[REDACTED_AUTHOR] - */ -internal class LocalFeatureTogglesStorage( - private val assetLoader: AssetLoader, -) : FeatureTogglesStorage { - - override var featureToggles: List by Delegates.notNull() - private set - - override suspend fun init() { - featureToggles = assetLoader.loadList(LOCAL_CONFIG_PATH) - } - - private companion object { - const val LOCAL_CONFIG_PATH: String = "configs/feature_toggles_config" - } -} \ No newline at end of file diff --git a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/utils/CollectionExt.kt b/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/utils/CollectionExt.kt deleted file mode 100644 index 990f95cf18..0000000000 --- a/core/featuretoggles/src/main/kotlin/com/tangem/core/featuretoggle/utils/CollectionExt.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.core.featuretoggle.utils - -import com.tangem.core.featuretoggle.contract.VersionAvailabilityContract -import com.tangem.core.featuretoggle.storage.FeatureToggle - -internal fun List.associateToggles(currentVersion: String): Map { - return associate { localToggle -> - Pair( - first = localToggle.name, - second = VersionAvailabilityContract(currentVersion, localToggle.version), - ) - } -} \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index d5b6d3041c..4197e69d71 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,13 +1,13 @@ Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Kauf hinzuzufügen - Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Verkauf hinzuzufügen + Token nicht in Ihrem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Verkauf hinzuzufügen Wähle den Token Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es für Swaps zu finden und hinzuzufügen Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen. Kein verfügbares Paar - Du möchtest erhalten - Du möchtest tauschen + Wähle den Token aus, das Du erhalten möchtest + Wähle den Token, den Du tauschen möchtest Netzwerk wählen Token anlegen Token verwalten @@ -126,7 +126,7 @@ Markt Langsam Gebühren - Adressen abrufen + Adressen synchronisieren Zum Anbieter gehen Zum Token stunde @@ -251,6 +251,7 @@ Tangem bietet Token-Swaps über Drittanbieter gemäß den jeweiligen Bedingungen des jeweiligen Anbieters an. Anbieter wählen Es ist ein Fehler aufgetreten. Code: %s + Fehler %1$s. Der ausgewählte Anbieter kann die angegebene Transaktion nicht verarbeiten. Bitte runde den Wert auf %2$s auf oder änder diesen. Der gewählte Anbieter ist im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s) Swaps sind im Moment nicht verfügbar. Bitte versuche es später noch einmal. (Code: %s) Geschätzter Betrag @@ -296,7 +297,7 @@ Empfohlen Gekauft %s Kauf %s - Kauf %s... + Kauf %s… Keine Token gefunden. Bitte versuche eine andere Anfrage ID: %s Transaktions-ID kopiert @@ -565,6 +566,8 @@ Zugangscode wiederherstellen Identische Karten oder Ring Zugangscode + Verfügbar mit %s + Anbieter erleichtern Transaktionen und sorgen für einen reibungslosen und effizienten Token-Austausch Suche nach Land Nicht verfügbar Suche nach Währung @@ -572,10 +575,11 @@ Der zu kaufende Betrag muss mindestens %s betragen Keine verfügbaren Anbieter für diese Währung Bezahlen mit + Verfügbar bis zu %s + Erhältlich bei %s Du kannst Deine Transaktion beim Drittanbieter %s abschließen. Umleitung auf %s… Unsere Dienstleistungen sind in diesem Land nicht verfügbar - Änder oder bestätige bitte Dein Wohnsitz wurde identifiziert als Residenz Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten. @@ -590,8 +594,11 @@ Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt Kamerazugriff verweigert + Kein Memo erforderlich %1$s ( %2$s ) im %3$s Netzwerk + Das Senden einer anderen Währung führt zu deren irreversiblem Verlust. Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust. + Sende nur %s an diese Adresse. Überweise Geld von einem anderen Wallet oder einer anderen Börse Teilnehmen Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal. @@ -1004,12 +1011,12 @@ Bei dieser Karte oder Ring kann es sich um ein Produktionsmuster oder eine Fälschung handeln Echtheitsprüfung fehlgeschlagen zuordnen - Dieser Token muss mit Ihrem Hedera-Konto verbunden sein, bevor du diesen erhalten kannst. Zuordnungsgebühr ~%1$s %2$s + Dieser Token muss mit Ihrem Hedera-Konto verbunden sein, bevor du diesen erhalten kannst. Zuordnungsgebühr ~%1$s %2$s Dieses Token muss mit deinem Hedera-Konto verknüpft sein, bevor du ihn erhalten kannst. Verknüpfe deinen Token Nicht genug %s. Lade dein Hedera-Konto auf, um dieses Token zuzuordnen Bist Du sicher, dass Du die Transaktion stornieren willst? Du kannst die Transaktion nicht erneut starten. - Deine Transaktion mit einem Betrag von %1$s %2$s wurde nicht abgeschlossen. Du kannst versuchen, diese erneut abzuschließen. + Deine Transaktion mit einem Betrag von %1$s %2$s wurde nicht abgeschlossen. Du kannst versuchen, diese erneut abzuschließen. Du hast eine nicht abgeschlossene Transaktion Auf dieser Karte oder Ring sind nur noch %s Unterschriften übrig. Du musst dein gesamtes Guthaben abheben. Geringe Anzahl von Unterschriften diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index c65acf41fa..d4817e7fdf 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -6,8 +6,6 @@ ¿No encuentras el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio No hay tokens disponibles para intercambiar con el token seleccionado. Por favor elige otro. No hay par disponible - Desea recibir - Quiere intercambiar Elige red Agregue un token personalizado Gestionar tokens @@ -126,7 +124,7 @@ Mercado Lento Velocidad y tarifa - Obtener direcciones + Sincronizar direcciones Ir al proveedor Ir al token hora @@ -432,7 +430,6 @@ El valor teórico total de una criptomoneda si todas las monedas que podrían existir estuvieran en circulación, incluidas las que actualmente no circulan. Valoración totalmente diluida Dato de Genesis - Vacío Alto Titulares El cambio en el número de poseedores de tokens dentro de un período de tiempo específico @@ -443,7 +440,6 @@ El cambio en la cantidad de liquidez disponible para el token durante el período de tiempo especificado Liquidez Índice de liquidez - Vacío Listado en Bajo Capital. de mercado @@ -517,12 +513,12 @@ Creando un backup Leer más sobre seed phrase - Vacío + Escriba estas %d palabras en el orden que se indica a continuación y guárdelas en un lugar seguro y secreto. Su seed phrase - Vacío + %d palabras Para importar su billetera, ingrese su seed phrase en el campo de abajo @@ -574,10 +570,11 @@ La cantidad a comprar debe ser como mínimo %s No hay proveedores disponibles para esta moneda Pagar con + Disponible hasta %s + Disponible desde %s Podrá completar su transacción en el proveedor externo, %s Redirigiendo a %s... Nuestros servicios no están disponibles en este país - Cámbielo o confírmelo Su residencia ha sido identificada como Residencia Seleccione el país correcto para garantizar opciones de pago y servicios precisos. @@ -1006,12 +1003,12 @@ Esta tarjeta podría ser una muestra de producción o una falsificación La verificación de autenticidad falló Asociar - Este token debe estar asociado a tu cuenta de Hedera antes de que pueda recibirlo. Tarifa de asociación ~%1$s %2$s + Este token debe estar asociado a tu cuenta de Hedera antes de que pueda recibirlo. Tarifa de asociación ~%1$s %2$s Este token debe estar asociado con su cuenta de Hedera antes de poder recibirlo. Asocie su token No hay suficiente %s. Recargue su cuenta de Hedera para asociar este token ¿Está seguro(a) de que desea cancelar la transacción? No podrá volver a intentar realizarla. - Su transacción con un importe de %1$s %2$s no se ha completado. Puede volver a intentarlo para completarla. + Su transacción con un importe de %1$s %2$s no se ha completado. Puede volver a intentarlo para completarla. Tiene una transacción sin terminar Solo quedan %s firmas en esta tarjeta. Deba retirar todos sus fondos. Recuento de firmas bajo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 1d37bbc265..4cbdce726e 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -6,8 +6,6 @@ Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'échange Il n\'y a pas de token disponible pour échanger avec le token sélectionné. Veuillez en choisir un autre. Aucune paire disponible - Vous souhaitez recevoir - Vous souhaitez échanger Choisissez le réseau Ajouter un jeton personnalisé Gérer les jetons @@ -126,7 +124,7 @@ Marché Lent Vitesse et frais - Obtenir des adresses + Synchroniser les adresses Aller au fournisseur Aller au jeton heure @@ -370,7 +368,7 @@ Choisissez un portefeuille Le portefeuille ne prend pas en charge plus d\'un réseau Pour acheter, échanger ou recevoir cet actif, ajoutez-le à votre portefeuille - Cet actif n\'est plus pris en charge dans le portefeuille + Cet actif n\'est pas pris en charge dans le portefeuille Cet actif n\'est pas disponible pour ce portefeuille Ajouter au portfolio Ajoutez un jeton @@ -432,7 +430,6 @@ La valeur théorique totale d\'une crypto-monnaie si toutes les pièces qui pourraient exister étaient en circulation, y compris celles qui ne circulent pas actuellement Valorisation entièrement diluée Date de la Genesis - Vide Haut Détenteurs L\'évolution du nombre de détenteurs de jetons au cours d\'une période donnée @@ -443,7 +440,6 @@ Le changement dans la quantité de liquidité disponible pour le jeton pendant la période spécifiée Liquidité Indice de liquidité - Vide Listé sur Faible Cap. boursière @@ -517,12 +513,12 @@ Sauvegarde en cours En savoir plus sur les seed phrases - Vide + Écrivez ces %d mots dans l\'ordre indiqué ci-dessous et conservez-les dans un endroit sûr et secret. Votre seed phrase - Vide + %d mots Pour importer votre portefeuille, entrez votre seed phrase dans le champ ci-dessous @@ -574,10 +570,11 @@ Le montant à acheter doit être au moins %s Aucun fournisseur disponible pour cette devise Payer avec + Disponible jusqu\'à %s + Disponible à partir de %s Vous pourrez finaliser votre transaction sur le fournisseur tiers, %s Redirection vers %s... Nos services ne sont pas disponibles dans ce pays - Modifiez-le ou confirmez-le Votre résidence a été identifiée comme Résidence Veuillez sélectionner le bon pays pour garantir des options de paiement et des services précis. @@ -607,9 +604,9 @@ pour le portefeuille %d pour les portefeuilles %d - Obtiendras ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^ + Obtiendrez ^^%1$s^^ pour chaque portefeuille acheté par votre ami sur votre adresse de réseau %2$s %3$s ^^30 jours après^^ Vous - Obtiendrez + Obtiendra lors de l\'achat d\'un portefeuille sur tangem.com %s réduction Votre ami @@ -1006,12 +1003,12 @@ Cette carte pourrait être un échantillon de production ou une contrefaçon Échec de la vérification d\'authenticité Associer - Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir. Frais d\'association ~%1$s %2$s + Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir. Frais d\'association ~%1$s %2$s Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir Associez votre jeton %s insuffisant. Renflouez votre compte Hedera pour associer ce jeton Êtes-vous sûr(e) de vouloir annuler la transaction ? Vous ne pourrez plus réessayer. - Votre transaction d\'un montant de %1$s %2$s n\'a pas été finalisée. Vous pouvez réessayer plus tard pour la finaliser. + Votre transaction d\'un montant de %1$s %2$s n\'a pas été finalisée. Vous pouvez réessayer plus tard pour la finaliser. Vous avez une transaction inachevée Il ne reste que %s signatures sur cette carte. Vous devez retirer tous vos fonds. Faible nombre de signatures diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 9f3c536b40..5849f7d083 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -2,12 +2,13 @@ ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、買付のために追加してください ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、売却のために追加してください + 暗号資産を売却 トークンを選択 ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、スワップのために追加してください 選択したトークンと交換できるトークンがありません。別のトークンを選択してください。 利用可能なペアがありません - 受け取りたい - 交換したい + 受け取りたいトークンを選択します + スワップしたいトークンを選択します ネットワークを選択 カスタムトークンの追加 トークンの管理 @@ -123,7 +124,7 @@ マーケット 遅い 速度と料金 - アドレスを取得する + アドレスを同期する プロバイダーへ移動 トークンへ移動 時間 @@ -248,6 +249,7 @@ Tangemは、各プロバイダーの条件に従って、サードパーティプロバイダーを介してトークンスワップを提供します。 プロバイダーを選択 エラーが発生しました。コード: %s + エラー%1$s 。選択したプロバイダーは指定された取引を処理できません。値を%2$sに切り上げるか、変更してください。 選択したプロバイダーは現在利用できません。しばらくしてからもう一度お試しください。(コード: %s ) 現在、交換はご利用いただけません。しばらくしてからもう一度お試しください。(コード: %s ) 推定金額 @@ -557,6 +559,8 @@ アクセスコードの復元 同一のカード アクセスコード + %sで利用可能 + プロバイダーは取引を促進し、スムーズで効率的なトークン交換を保証します 国で検索 利用不可 通貨で検索 @@ -564,10 +568,11 @@ 買付金額は少なくとも%sである必要があります この通貨で利用可能なプロバイダーはありません 支払う + 最大 %s まで使用可能 + %s 以上で利用可能 サードパーティプロバイダー%sで取引を完了できます。 %sにリダイレクトしています... この国では当社のサービスはご利用いただけません - 変更または確認 あなたの住居は次のように識別されています 住居 正確なお支払い方法とサービスを確保するため、正しい国を選択してください。 @@ -582,8 +587,11 @@ 設定 カメラへのアクセスを許可していません。 カメラへのアクセスが拒否されました + メモ不要 %3$sネットワーク上の%1$s ( %2$s ) + 他の暗号資産を送信すると、取り返しのつかない損失が発生します。 このアドレスには%sのみを送金してください。他のトークンを送信すると、取り返しのつかない損失が発生します。 + このアドレスには%sのみを送信してください。 ウォレットや取引所から資金を送金する 参加する 紹介プログラムに関する情報を読み込めませんでした。しばらくしてからもう一度お試しください。 @@ -994,12 +1002,12 @@ このカードは、サンプル品または偽造品である可能性があります 真正性チェックに失敗しました 関連付ける - このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。関連付け手数料 ~ %1$s %2$s + このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。関連付け手数料 ~ %1$s %2$s このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。 トークンを関連付ける %sが不足しています。このトークンを関連付けるには、Hederaアカウントに資金を追加してください。 取引をキャンセルしてもよろしいですか? 取引を再試行することはできません - %1$s %2$sの取引が完了しませんでした。もう一度試して完了してください。 + %1$s %2$sの取引が完了しませんでした。もう一度試して完了してください。 未完了の取引があります このカードには%sの署名のみが残っています。資金をすべて引き出す必要があります。 署名数が少ないです diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 94815eb793..88c9836ebf 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,10 +1,14 @@ + Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для покупки. + Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для продажи. + Продать криптовалюту Выберите токен - Нет доступных токенов для обмена с выбранным токеном, пожалуйста, выберите другой. + Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для обмена. + Нет доступных вариантов для обмена с выбранным токеном, пожалуйста, выберите другой. Нет доступных пар - Вы хотите получить - Вы хотите обменять + Выберите токен для получения + Выберите токен для обмена Выберите сеть Добавить токен Валюты @@ -129,7 +133,7 @@ По рынку Медленно Скорость и комиссия - Получить ваши адреса + Синхронизировать адреса К провайдеру Перейти в токен час @@ -254,6 +258,7 @@ Tangem предоставляет доступ к обмену через сторонних поставщиков в соответствии с их правилами Выберите провайдера Произошла ошибка. Код: %s + Ошибка %1$s. Выбранный провайдер не может обработать указанную транзакцию. Округлите значение до %2$s или измените его. Выбранный провайдер недоступен для обмена. Попробуйте позже. (Код: %s) В данный момент обмен невозможен. Попробуйте позже. (Код: %s) Курс обмена @@ -297,6 +302,9 @@ Недоступно для этой пары Требуется разрешение Рекомендовано + Куплено %s + Покупка %s + Покупка %s... Токены не найдены. Пожалуйста, попробуйте другой запрос ID: %s ID транзакции скопирован @@ -573,17 +581,25 @@ Восстановление кода доступа Идентичные карты Код доступа + Доступно с %s + Tangem предоставляет доступ к покупке через сторонних провайдеров в соответствии с их правилами Поиск по стране Недоступно Поиск по валюте Сумма покупки не может быть больше, чем %s Сумма покупки должна составлять минимум %s - Для данной валюты нет доступных провайдеров + Нет доступных провайдеров для выбранной валюты Оплата с - Вы сможете завершить транзакцию через сервис стороннего провайдера, %s + Доступно до %s + Доступно с %s + Вы сможете продолжить транзакцию через сервис стороннего провайдера, %s Переход к %s... Наши сервисы недоступны в данной стране + Ваше местоположение определено как + Страна проживания + Пожалуйста, укажите правильную страну, чтобы обеспечить точность доступных способов оплаты и услуг. Настройки + Вы можете закрыть этот экран и проверить статус транзакции на экране информации о токене. Через Группы По балансу @@ -593,8 +609,11 @@ Настройки Вы не предоставили доступ к вашей камере Доступ к камере запрещен + Memo не требуется %1$s (%2$s) в сети %3$s + Использование другой сети может привести к утрате средств. Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. + Отправляйте только %s на этот адрес Переводите средства с любого кошелька или биржи Участвовать Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже. @@ -650,6 +669,10 @@ Сканировать Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку. Приготовьте свой Tangem! + На вашем балансе недостаточно средств для продажи этой криптовалюты. Пожалуйста, пополните нужный актив, чтобы продолжить. + Недостаточно средств + Продажа криптовалюты в вашем регионе временно недоступна. Мы активно работаем над тем, чтобы добавить эту возможность. Следите за нашими новостями! + Региональные ограничения Уже содержится во введенном адресе Сумма комиссии в %s раз превышает рекомендованную. Убедитесь, что указанная комиссия верна. Вы указали комиссию ниже рекомендуемой, это может привести к задержке исполнения вашей транзакции. Продолжить? @@ -690,6 +713,7 @@ Установлена высокая комиссия Ввиду особенности сети %1$s комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить %2$s. Комиссия повышена + Баланс вашего счета не может быть ниже арендной платы. Пожалуйста, оставьте на счете не менее %1$s или выведите все средства. Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению Недопустимая сумма Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %2$s. @@ -889,6 +913,7 @@ Обменять Недоступно для покупки Недоступно для продажи + Недоступно для обмена с %s контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. @@ -966,7 +991,7 @@ Адрес скопирован в буфер обмена Нет соединения с интернетом Получить с 10%% скидкой - Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменяйте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. + Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. Откройте Tangem Wallet Настройки кошелька Tangem @@ -1009,12 +1034,12 @@ Возможно, данная карта является образцом или подделкой Ошибка проверки подлинности Ассоциировать - Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%1$s %2$s + Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять. Стоимость ассоциации ~%1$s %2$s Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять Ассоциируете свой токен Недостаточно %s. Пополните ваш аккаунт Hedera для ассоциации этого токена Вы уверены что хотите отменить эту транзакцию? Вы не сможете отправить ее еще раз - Ваша транзакция %1$s %2$s не была завершена. Вы можете попробовать отправить ее еще раз. + Ваша транзакция %1$s %2$s не была завершена. Вы можете попробовать отправить ее еще раз. У вас есть незавершенная транзакция На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства. Малое количество подписей diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index b425f91e95..745f163055 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -6,8 +6,6 @@ Токен не знайдено у вашому портфелі? Перевірте ринки, щоб знайти та додати його для обміну Немає доступних токенів для обміну з обраним токеном. Будь ласка, оберіть інший. Немає вільної пари - Ви хочете отримати - Ви хочете обміняти Оберіть мережу Додати токен Токени @@ -132,7 +130,7 @@ За ринком Повільно Швидкість та комісія - Отримати ваші адреси + Синхронізувати адреси Перейти до провайдера Перейти до токену година @@ -301,7 +299,7 @@ Потрібен дозвіл Рекомендовано Купив %s - Купівля %s... + Купівля %s Купівля %s... Токенів не знайдено. Будь ласка, спробуйте інший запит ID: %s @@ -588,10 +586,11 @@ Сума покупки повинна бути не менше %s Для данної валюти немає доступних провайдерів Оплата з + Доступно до %s + Доступно від %s Ви зможете завершити транзакцію у стороннього постачальника, %s Перенаправлення на %s... Наші сервіси недоступні в цій країні - Змінити або підтвердити Ваше місце проживання визначено як Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги. Параметри @@ -1023,12 +1022,12 @@ Ця картка може бути виробничим зразком або підробкою Перевірка автентичності не вдалася Асоціювати - Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти. Вартість асоціації ~ %1$s %2$s + Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти. Вартість асоціації ~ %1$s %2$s Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти Асоціюйте свій токен Недостатньо %s. Поповніть ваш обліковий запис Hedera для асоціації цього токена Ви впевнені, що хочете скасувати цю транзакцію? Ви не зможете відправити її ще раз - Ваша транзакція %1$s %2$s не була завершена. Ви можете спробувати відправити її ще раз. + Ваша транзакція %1$s %2$s не була завершена. Ви можете спробувати відправити її ще раз. У вас є незавершена транзакція На цій картці залишається лише %s підписів. Ви повинні вивести всі свої кошти. Низька кількість підписів diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 60cf7b6306..ef80d3a558 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,13 +1,14 @@ - Token not found in your portfolio? Check the Markets to find and add it for purchase - Token not found in your portfolio? Check the Markets to find and add it for sell + Can’t find your token? Go to the Market section on the main page and add it to your portfolio for purchase + Can’t find your token? Go to the Market section on the main page and add it to your portfolio for selling. + Sell crypto Choose the Token - Token not found in your portfolio? Check the Markets to find and add it for swap + Can’t find your token? Go to the Market section on the main page and add it to your portfolio for swapping. There are no available tokens to swap with the selected token. Please choose another one. No available pair - You want to Receive - You want to Swap + Choose the token you want to receive + Choose the token you want to swap Choose network Add custom token Manage tokens @@ -126,7 +127,7 @@ Market Slow Speed and fee - Get your addresses + Synchronize addresses Go to provider Go to token hour @@ -251,6 +252,7 @@ Tangem offers token swaps via 3rd-party providers according to each provider\'s terms Choose provider An error occurred. Code: %s + Error %1$s. The selected provider cannot process the specified transaction. Please round the value up to %2$s or change it Selected provider is unavailable at the moment. Please try again later. (Code: %s) Swaps are unavailable at the moment. Please try again later. (Code: %s) Estimated amount @@ -565,6 +567,8 @@ Access code restore Identical cards Access code + Available with %s + Buying crypto in Tangem is powered by third-party providers on their terms. Search by country Unavailable Search by currency @@ -572,15 +576,17 @@ The amount to buy must be at least %s No available providers for this currency Pay with + Available up to %s + Available from %s You will be able to complete your transaction on the third-party provider, %s Redirecting to %s... Our services are not available in this country - Change or confirm it Your residence has been identified as Residence Please select the correct country to ensure accurate payment options and services. Settings - You can check the transaction status from the token page + You can check the transaction status from the token page. + You can close this screen and check the transaction status on the token details screen. Via Group By balance @@ -590,8 +596,11 @@ Settings You have not given access to your camera Camera access denied + No memo required %1$s (%2$s) on %3$s network + Sending any other currency will result in its irreversible loss. Send only %s to this address. Sending any other currency will result in its irreversible loss. + Send only %s to this address. Transfer funds from any wallet or exchange Participate Failed to load the information about the referral program. Please try again later. @@ -643,6 +652,10 @@ Scan card or ring Scan the card or ring to change its settings. The changes will impact only the card or ring you\'ve scanned and will not affect other devices tied to your wallet. Get your Tangem ready! + You don’t have enough funds in your balance to sell this cryptocurrency. Please deposit the desired asset to proceed. + Insufficient Balance + Selling cryptocurrency is unavailable in your region at the moment. We’re actively working to bring this option to you soon—stay tuned! + Regional Restriction Already included in the entered address Your commission amount is %s times higher than the recommended amount. Please review and adjust your custom settings. You specified a commission below the recommended amount, which could cause a delay in your transaction. Continue? @@ -683,6 +696,7 @@ Custom fee is high Due to the peculiarities of the %1$s network, the fee for transferring the entire balance is higher. To reduce the commission, you can leave %2$s. The fee is higher + Your account balance cannot be lower than the rent fee. Please maintain at least %1$s on your account or withdraw all funds. The included commission exceeds the transfer amount, leading to a negative value Invalid amount The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %2$s. @@ -1004,12 +1018,12 @@ This card might be a production sample or counterfeit Authenticity check failed Associate - This token must be associated with your Hedera account before you can receive it. Association fee ~%1$s %2$s + This token must be associated with your Hedera account before you can receive it. Association fee ~%1$s %2$s This token must be associated with your Hedera account before you can receive it Associate your token Not enough %s. Top up your Hedera account to associate this token Are you sure you want to cancel the transaction? You will not be able to retry the transaction again - Your transaction with an amount of %1$s %2$s was not completed. You can try again to complete it. + Your transaction with an amount of %1$s %2$s was not completed. You can try again to complete it. You have unfinished transaction Only %s signatures are left on this card. You must withdraw all of your funds. Low signature count diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index 728eb3a49c..14d96a5225 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -23,6 +23,7 @@ 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 com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.extensions.TextReference @@ -78,6 +79,7 @@ fun ActionButton( ) } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun Button( config: ActionButtonConfig, @@ -85,6 +87,7 @@ private fun Button( modifier: Modifier = Modifier, color: Color = TangemTheme.colors.button.secondary, ) { + val context = LocalContext.current val backgroundColor by animateColorAsState( targetValue = if (config.enabled) color else TangemTheme.colors.button.disabled, label = "Update background color", @@ -92,30 +95,9 @@ private fun Button( Box( modifier = modifier - .heightIn(min = TangemTheme.dimens.size36) + .heightIn(min = 36.dp) + .widthIn(min = 100.dp) .clip(shape) - .background(color = backgroundColor), - ) { - Content(config = config) - - if (config.isInProgress) { - Loading( - backgroundColor = backgroundColor, - modifier = Modifier - .matchParentSize() - .align(alignment = Alignment.Center), - ) - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun Content(config: ActionButtonConfig) { - val context = LocalContext.current - - Row( - modifier = Modifier .combinedClickable( enabled = config.enabled, onClick = config.onClick, @@ -128,38 +110,55 @@ private fun Content(config: ActionButtonConfig) { } }, ) - .padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing24) + .background(color = backgroundColor), + ) { + Content( + modifier = Modifier.align(Alignment.Center), + config = config, + ) + + if (config.isInProgress) { + Loading( + modifier = Modifier + .align(alignment = Alignment.Center) + .matchParentSize(), + backgroundColor = backgroundColor, + ) + } + } +} + +@Composable +private fun Content(config: ActionButtonConfig, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing24, + ) .padding(vertical = TangemTheme.dimens.spacing8), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - val iconTint by animateColorAsState( - targetValue = when { - !config.enabled -> TangemTheme.colors.icon.informative - config.dimContent -> TangemTheme.colors.icon.informative - else -> TangemTheme.colors.icon.primary1 - }, - label = "Update tint color", - ) - + val iconTint = when { + !config.enabled -> TangemTheme.colors.icon.informative + config.dimContent -> TangemTheme.colors.icon.informative + else -> TangemTheme.colors.icon.primary1 + } Icon( + modifier = Modifier.size(size = TangemTheme.dimens.size20), painter = painterResource(id = config.iconResId), contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size20), tint = iconTint, ) SpacerW8() - val textColor by animateColorAsState( - targetValue = when { - !config.enabled -> TangemTheme.colors.text.disabled - config.dimContent -> TangemTheme.colors.text.tertiary - else -> TangemTheme.colors.text.primary1 - }, - label = "Update text color", - ) - + val textColor = when { + !config.enabled -> TangemTheme.colors.text.disabled + config.dimContent -> TangemTheme.colors.text.tertiary + else -> TangemTheme.colors.text.primary1 + } Text( text = config.text.resolveReference(), color = textColor, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt index 78c7a724b7..f07edb1802 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt @@ -46,7 +46,7 @@ inline fun RowContentContainer( } @Composable -internal fun RowText( +fun RowText( mainText: String, secondText: String, accentMainText: Boolean, diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index 2745c0d132..51477ea895 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.onramp) + implementation(projects.domain.legacy) // region DI diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index e6d6c8e177..aa7e56fae3 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -1,5 +1,7 @@ package com.tangem.data.onramp +import com.squareup.moshi.Moshi +import com.tangem.blockchain.extensions.toBigDecimalOrDefault import com.tangem.data.common.api.safeApiCall import com.tangem.data.onramp.converters.CountryConverter import com.tangem.data.onramp.converters.CurrencyConverter @@ -12,13 +14,16 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.TangemExpressValues import com.tangem.datasource.api.express.models.response.ExchangeProvider +import com.tangem.datasource.api.express.models.response.ExchangeProviderType import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest +import com.tangem.datasource.api.onramp.models.response.OnrampDataJson import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO +import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore @@ -28,20 +33,26 @@ import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.onramp.model.* +import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.withContext +import org.joda.time.DateTime import timber.log.Timber import java.math.BigDecimal +import java.util.UUID -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass", "TooManyFunctions") internal class DefaultOnrampRepository( private val onrampApi: OnrampApi, private val expressApi: TangemExpressApi, @@ -51,12 +62,16 @@ internal class DefaultOnrampRepository( private val pairsStore: OnrampPairsStore, private val quotesStore: OnrampQuotesStore, private val quotesErrorConverter: OnrampQuotesErrorConverter, + private val walletManagersFacade: WalletManagersFacade, + private val dataSignatureVerifier: DataSignatureVerifier, + moshi: Moshi, ) : OnrampRepository { private val currencyConverter = CurrencyConverter() private val countryConverter = CountryConverter(currencyConverter) private val statusConverter = StatusConverter() private val paymentMethodsConverter = PaymentMethodConverter() + private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java) override suspend fun getCurrencies(): List = withContext(dispatchers.io) { onrampApi.getCurrencies() @@ -194,14 +209,19 @@ internal class DefaultOnrampRepository( providerId = provider.id, ).bind() OnrampQuote.Data( - fromAmount = convertToBigDecimal(response.fromAmount), - toAmount = convertToBigDecimal(response.toAmount), - minFromAmount = convertToBigDecimal(response.minFromAmount), - maxFromAmount = convertToBigDecimal(response.maxFromAmount), + fromAmount = OnrampAmount( + value = response.fromAmount + .toBigDecimalOrDefault() + .movePointLeft(currency.precision), + decimals = currency.precision, + symbol = amount.currencySymbol, + ), + toAmount = convertToAmount(response.toAmount, cryptoCurrency), + minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency), + maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency), paymentMethod = paymentMethod, provider = provider, ) - null }, onError = { error -> if (error is ApiResponseError.HttpException) { @@ -230,6 +250,69 @@ internal class DefaultOnrampRepository( return quotesStore.get(QUOTES_KEY) } + override suspend fun getQuotesSync(): List? { + return quotesStore.getSyncOrNull(QUOTES_KEY) + } + + override suspend fun getOnrampData( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + quote: OnrampProviderWithQuote.Data, + ): OnrampTransaction = withContext(dispatchers.io) { + try { + val address = requireNotNull( + value = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network), + lazyMessage = { "Address must not be null" }, + ) + val fromAmountString = quote.fromAmount.value.movePointRight(quote.fromAmount.decimals).toString() + val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" } + val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" } + val data = safeApiCall( + call = { + onrampApi.getData( + fromCurrencyCode = currency.code, + toContractAddress = cryptoCurrency.getContractAddress(), + toNetwork = cryptoCurrency.network.backendId, + paymentMethod = quote.paymentMethod.id, + countryCode = country.code, + fromAmount = fromAmountString, + toDecimals = cryptoCurrency.decimals, + providerId = quote.provider.id, + toAddress = address, + redirectUrl = "tangem://redirect?action=dismissBrowser", + language = null, + theme = null, + requestId = UUID.randomUUID().toString(), + ).bind() + }, + onError = { e -> + Timber.e(e) + throw e + }, + ) + if (dataSignatureVerifier.verifySignature(data.signature, data.dataJson)) { + val dataJson = requireNotNull(onrampDataAdapter.fromJson(data.dataJson)) { + "Can not parse dataJson. ${data.dataJson}" + } + createOnrampTransaction( + txId = data.txId, + onrampDataJson = dataJson, + userWalletId = userWalletId, + currency = currency, + provider = quote.provider, + cryptoCurrency = cryptoCurrency, + fromAmount = quote.fromAmount.value, + toAmount = quote.toAmount.value, + ) + } else { + error("Express signature invalid") + } + } catch (e: Exception) { + Timber.e(e) + throw e + } + } + override suspend fun getPaymentMethods(): List { val paymentMethods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) { "Onramp payment methods is absent in storage" @@ -237,6 +320,17 @@ internal class DefaultOnrampRepository( return paymentMethodsConverter.convertList(paymentMethods) } + override suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod) { + val dto = paymentMethodsConverter.convertBack(paymentMethod) + paymentMethodsStore.store(SELECTED_PAYMENT_METHOD_KEY, listOf(dto)) + } + + override fun getSelectedPaymentMethod(): Flow { + return paymentMethodsStore + .get(SELECTED_PAYMENT_METHOD_KEY) + .mapNotNull { paymentMethods -> paymentMethods.firstOrNull()?.let(paymentMethodsConverter::convert) } + } + override suspend fun clearCache() = withContext(NonCancellable) { paymentMethodsStore.clear() pairsStore.clear() @@ -264,18 +358,53 @@ internal class DefaultOnrampRepository( pairsStore.store(PAIRS_KEY, onrampPairs) } + private fun createOnrampTransaction( + txId: String, + onrampDataJson: OnrampDataJson, + userWalletId: UserWalletId, + currency: OnrampCurrency, + provider: OnrampProvider, + cryptoCurrency: CryptoCurrency, + fromAmount: BigDecimal, + toAmount: BigDecimal, + ): OnrampTransaction { + return OnrampTransaction( + txId = txId, + userWalletId = userWalletId, + fromAmount = fromAmount, + fromCurrency = currency, + toAmount = toAmount, + toCurrencyId = cryptoCurrency.id.value, + status = OnrampStatus.Status.Expired, + externalTxUrl = onrampDataJson.externalTxUrl, + externalTxId = onrampDataJson.externalTxId, + timestamp = DateTime.now().millis, + providerName = provider.info.name, + providerImageUrl = provider.info.imageLarge, + providerType = ExchangeProviderType.ONRAMP.name, + redirectUrl = onrampDataJson.widgetUrl, + ) + } + private fun CryptoCurrency.getContractAddress(): String = when (this) { is CryptoCurrency.Coin -> TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE is CryptoCurrency.Token -> this.contractAddress } - private fun convertToBigDecimal(value: String): BigDecimal = - requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" } + private fun convertToAmount(value: String, cryptoCurrency: CryptoCurrency): OnrampAmount { + val cryptoValue = requireNotNull(value.toBigDecimalOrNull()) { "Can not parse $value to BigDecimal" } + return OnrampAmount( + value = cryptoValue.movePointLeft(cryptoCurrency.decimals), + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ) + } private fun List.removeApplePay(): List = filterNot { it.id == "apple-pay" } private companion object { const val PAYMENT_METHODS_KEY = "onramp_payment_methods" + const val SELECTED_PAYMENT_METHOD_KEY = "onramp_selected_payment_method" const val PAIRS_KEY = "onramp_pairs" const val QUOTES_KEY = "onramp_quotes" } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt index b45273c12e..f29bfe88ae 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/PaymentMethodConverter.kt @@ -2,12 +2,18 @@ package com.tangem.data.onramp.converters import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO import com.tangem.domain.onramp.model.OnrampPaymentMethod -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter -internal class PaymentMethodConverter : Converter { +internal class PaymentMethodConverter : TwoWayConverter { override fun convert(value: PaymentMethodDTO): OnrampPaymentMethod = OnrampPaymentMethod( id = value.id, name = value.name, imageUrl = value.image, ) + + override fun convertBack(value: OnrampPaymentMethod): PaymentMethodDTO = PaymentMethodDTO( + id = value.id, + name = value.name, + image = value.imageUrl, + ) } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt index 5373cba864..9b29c49779 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/TransactionConverter.kt @@ -25,7 +25,7 @@ internal class TransactionConverter : TwoWayConverter = mapOf(), - val primaryCard: PrimaryCard? = null, + @Json(name = "card") val card: CardDTO, + @Json(name = "productType") val productType: ProductType, + @Json(name = "walletData") val walletData: WalletData?, + @Json(name = "secondTwinPublicKey") val secondTwinPublicKey: String? = null, + @Json(name = "derivedKeys") val derivedKeys: Map = mapOf(), + @Json(name = "primaryCard") val primaryCard: PrimaryCard? = null, ) typealias KeyWalletPublicKey = ByteArrayKey diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ByteArrayKeyAdapter.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ByteArrayKeyAdapter.kt similarity index 86% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ByteArrayKeyAdapter.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ByteArrayKeyAdapter.kt index cae2c47666..459cadb6fa 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ByteArrayKeyAdapter.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ByteArrayKeyAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.userWalletList.utils.json +package com.tangem.domain.models.scan.serialization import com.squareup.moshi.FromJson import com.squareup.moshi.JsonAdapter @@ -7,7 +7,7 @@ import com.squareup.moshi.JsonWriter import com.squareup.moshi.ToJson import com.tangem.common.extensions.ByteArrayKey -internal class ByteArrayKeyAdapter { +class ByteArrayKeyAdapter { @ToJson fun toJson(writer: JsonWriter, src: ByteArrayKey, byteArrayAdapter: JsonAdapter) { byteArrayAdapter.toJson(writer, src.bytes) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/CardBackupStatusAdapter.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/CardBackupStatusAdapter.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/CardBackupStatusAdapter.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/CardBackupStatusAdapter.kt index dc40917fb8..620a432457 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/CardBackupStatusAdapter.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/CardBackupStatusAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.userWalletList.utils.json +package com.tangem.domain.models.scan.serialization import com.squareup.moshi.FromJson import com.squareup.moshi.JsonAdapter @@ -7,7 +7,7 @@ import com.squareup.moshi.JsonWriter import com.squareup.moshi.ToJson import com.tangem.domain.models.scan.CardDTO -internal class CardBackupStatusAdapter { +class CardBackupStatusAdapter { @ToJson fun toJson(writer: JsonWriter, src: CardDTO.BackupStatus?, mapAdapter: JsonAdapter>) { val jsonMap = mutableMapOf() diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/DerivationPathAdapterWithMigration.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/DerivationPathAdapterWithMigration.kt similarity index 87% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/DerivationPathAdapterWithMigration.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/DerivationPathAdapterWithMigration.kt index 4719b96b49..3765ca477a 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/DerivationPathAdapterWithMigration.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/DerivationPathAdapterWithMigration.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.userWalletList.utils.json +package com.tangem.domain.models.scan.serialization import com.squareup.moshi.FromJson import com.squareup.moshi.ToJson @@ -6,7 +6,6 @@ import com.tangem.common.extensions.guard import com.tangem.common.json.MoshiJsonConverter import com.tangem.crypto.hdWallet.DerivationNode import com.tangem.crypto.hdWallet.DerivationPath -import timber.log.Timber class DerivationPathAdapterWithMigration { @ToJson @@ -26,7 +25,6 @@ class DerivationPathAdapterWithMigration { private fun fromLegacyScheme(jsonMap: Map): DerivationPath { val rawPath = jsonMap["rawPath"] as String val nodeIndexes = (jsonMap["nodes"] as? List).guard { - Timber.e("Unable to convert derivation path nodes from JSON") return DerivationPath(rawPath) } val nodes = nodeIndexes.map { DerivationNode.fromIndex(it.toLong()) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ExtendedPublicKeysMapAdapter.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ExtendedPublicKeysMapAdapter.kt similarity index 95% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ExtendedPublicKeysMapAdapter.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ExtendedPublicKeysMapAdapter.kt index 4fa0999074..95e1e829ff 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ExtendedPublicKeysMapAdapter.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ExtendedPublicKeysMapAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.userWalletList.utils.json +package com.tangem.domain.models.scan.serialization import com.squareup.moshi.FromJson import com.squareup.moshi.JsonAdapter @@ -11,7 +11,7 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.operations.derivation.ExtendedPublicKeysMap -internal class ExtendedPublicKeysMapAdapter { +class ExtendedPublicKeysMapAdapter { @ToJson fun toJson( writer: JsonWriter, diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt new file mode 100644 index 0000000000..b3cb3d847e --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.models.scan.serialization + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.json.TangemSdkAdapter +import com.tangem.domain.models.scan.ScanResponse +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +internal object ScanResponseAsStringSerializer : KSerializer { + private val moshi = Moshi.Builder() + .add(WalletDerivedKeysMapAdapter()) + .add(ScanResponseDerivedKeysMapAdapter()) + .add(ByteArrayKeyAdapter()) + .add(ExtendedPublicKeysMapAdapter()) + .add(CardBackupStatusAdapter()) + .add(DerivationPathAdapterWithMigration()) + .add(TangemSdkAdapter.DateAdapter()) + .add(TangemSdkAdapter.DerivationNodeAdapter()) + .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model + .add(KotlinJsonAdapterFactory()) + .build() + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ScanResponse", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): ScanResponse { + return moshi.adapter(ScanResponse::class.java).fromJson(decoder.decodeString())!! + } + + override fun serialize(encoder: Encoder, value: ScanResponse) { + encoder.encodeString(moshi.adapter(ScanResponse::class.java).toJson(value)!!) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ScanResponseDerivedKeysMapAdapter.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseDerivedKeysMapAdapter.kt similarity index 94% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ScanResponseDerivedKeysMapAdapter.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseDerivedKeysMapAdapter.kt index 0aada43b6a..a190126b64 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/ScanResponseDerivedKeysMapAdapter.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseDerivedKeysMapAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.userWalletList.utils.json +package com.tangem.domain.models.scan.serialization import com.squareup.moshi.FromJson import com.squareup.moshi.JsonAdapter @@ -8,7 +8,7 @@ import com.squareup.moshi.ToJson import com.tangem.common.extensions.ByteArrayKey import com.tangem.operations.derivation.ExtendedPublicKeysMap -internal class ScanResponseDerivedKeysMapAdapter { +class ScanResponseDerivedKeysMapAdapter { @ToJson fun toJson( writer: JsonWriter, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/WalletDerivedKeysMapAdapter.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/WalletDerivedKeysMapAdapter.kt similarity index 95% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/WalletDerivedKeysMapAdapter.kt rename to domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/WalletDerivedKeysMapAdapter.kt index d374f5013d..a51bcca50f 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/WalletDerivedKeysMapAdapter.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/WalletDerivedKeysMapAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.userWalletList.utils.json +package com.tangem.domain.models.scan.serialization import com.squareup.moshi.FromJson import com.squareup.moshi.JsonAdapter @@ -10,7 +10,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey -internal class WalletDerivedKeysMapAdapter { +class WalletDerivedKeysMapAdapter { @ToJson fun toJson( writer: JsonWriter, diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt new file mode 100644 index 0000000000..a6ffb9df16 --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampAmount.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.onramp.model + +import com.tangem.domain.core.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +@Serializable +data class OnrampAmount( + val symbol: String, + val value: SerializedBigDecimal, + val decimals: Int, +) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProvider.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProvider.kt index 53e6d8848b..35c4934530 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProvider.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProvider.kt @@ -1,9 +1,13 @@ package com.tangem.domain.onramp.model +import kotlinx.serialization.Serializable + +@Serializable data class OnrampProvider( val id: String, val info: OnrampProviderInfo, val paymentMethods: List, ) +@Serializable data class OnrampProviderInfo(val name: String, val imageLarge: String) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt new file mode 100644 index 0000000000..4499df29bd --- /dev/null +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.onramp.model + +import kotlinx.serialization.Serializable + +sealed interface OnrampProviderWithQuote { + + val provider: OnrampProvider + + @Serializable + data class Data( + override val provider: OnrampProvider, + val paymentMethod: OnrampPaymentMethod, + val toAmount: OnrampAmount, + val fromAmount: OnrampAmount, + ) : OnrampProviderWithQuote + + sealed interface Unavailable : OnrampProviderWithQuote { + + data class NotSupportedPaymentMethod( + override val provider: OnrampProvider, + val availablePaymentMethods: List, + ) : Unavailable + + data class AvailableFrom(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable + data class AvailableUpTo(override val provider: OnrampProvider, val amount: OnrampAmount) : Unavailable + } +} \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt index 565aab17c5..9c2ab7307f 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt @@ -1,8 +1,5 @@ package com.tangem.domain.onramp.model -import com.tangem.domain.tokens.model.Amount -import java.math.BigDecimal - sealed class OnrampQuote { abstract val paymentMethod: OnrampPaymentMethod @@ -11,10 +8,10 @@ sealed class OnrampQuote { data class Data( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val fromAmount: BigDecimal, - val toAmount: BigDecimal, - val minFromAmount: BigDecimal, - val maxFromAmount: BigDecimal, + val fromAmount: OnrampAmount, + val toAmount: OnrampAmount, + val minFromAmount: OnrampAmount, + val maxFromAmount: OnrampAmount, ) : OnrampQuote() sealed class Error : OnrampQuote() { @@ -22,13 +19,13 @@ sealed class OnrampQuote { data class AmountTooSmallError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val amount: Amount, + val amount: OnrampAmount, ) : Error() data class AmountTooBigError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val amount: Amount, + val amount: OnrampAmount, ) : Error() } } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt index d2403d003e..acf88799f9 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/cache/OnrampTransaction.kt @@ -36,4 +36,5 @@ data class OnrampTransaction( val providerName: String, // todo onramp fix after SwapProvider moved to own module val providerImageUrl: String, // todo onramp fix after SwapProvider moved to own module val providerType: String, // todo onramp fix after SwapProvider moved to own module + val redirectUrl: String, ) \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt new file mode 100644 index 0000000000..e1de3cdca5 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt @@ -0,0 +1,68 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.OnrampProvider +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.onramp.model.OnrampProviderWithQuote.Unavailable +import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.domain.onramp.repositories.OnrampRepository + +class GetOnrampProviderWithQuoteUseCase(private val repository: OnrampRepository) { + + suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either> { + return Either.catch { + val quotes: List = requireNotNull(repository.getQuotesSync()) { "Quotes must not be null" } + quotes + .groupBy { it.provider } + .mapNotNull { (provider, quotes) -> + quotes.quoteWithProvider(provider = provider, selectedPaymentMethod = paymentMethod) + } + } + } + + private fun List.quoteWithProvider( + provider: OnrampProvider, + selectedPaymentMethod: OnrampPaymentMethod, + ): OnrampProviderWithQuote? { + val quoteData = this.filterIsInstance() + val matchedPaymentMethodQuote = quoteData.firstOrNull { it.paymentMethod == selectedPaymentMethod } + + if (matchedPaymentMethodQuote != null) { + return OnrampProviderWithQuote.Data( + provider = matchedPaymentMethodQuote.provider, + paymentMethod = matchedPaymentMethodQuote.paymentMethod, + toAmount = matchedPaymentMethodQuote.toAmount, + fromAmount = matchedPaymentMethodQuote.fromAmount, + ) + } + + val paymentMethodNotSupported = quoteData.filterNot { it.paymentMethod == selectedPaymentMethod } + if (paymentMethodNotSupported.isNotEmpty()) { + return Unavailable.NotSupportedPaymentMethod( + provider = provider, + availablePaymentMethods = paymentMethodNotSupported.map(OnrampQuote.Data::paymentMethod), + ) + } + + val amountError = this + .filterIsInstance() + .firstOrNull { it.paymentMethod == selectedPaymentMethod } + + return when { + amountError != null -> { + when (amountError) { + is OnrampQuote.Error.AmountTooBigError -> Unavailable.AvailableUpTo( + provider = amountError.provider, + amount = amountError.amount, + ) + is OnrampQuote.Error.AmountTooSmallError -> Unavailable.AvailableFrom( + provider = amountError.provider, + amount = amountError.amount, + ) + } + } + else -> null + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt new file mode 100644 index 0000000000..3c7762b989 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.onramp.repositories.OnrampTransactionRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +class GetOnrampRedirectUrlUseCase( + private val repository: OnrampRepository, + private val transactionRepository: OnrampTransactionRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + quote: OnrampProviderWithQuote.Data, + cryptoCurrency: CryptoCurrency, + ): Either { + return Either.catch { + val transaction = repository.getOnrampData( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + quote = quote, + ) + transactionRepository.storeTransaction(transaction) + transaction.redirectUrl + } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampSelectedPaymentMethodUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampSelectedPaymentMethodUseCase.kt new file mode 100644 index 0000000000..23daf16d80 --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampSelectedPaymentMethodUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.repositories.OnrampRepository +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetOnrampSelectedPaymentMethodUseCase(private val repository: OnrampRepository) { + + operator fun invoke(): EitherFlow { + return repository.getSelectedPaymentMethod() + .map> { it.right() } + .catch { emit(it.left()) } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveSelectedPaymentMethod.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveSelectedPaymentMethod.kt new file mode 100644 index 0000000000..69f40194af --- /dev/null +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveSelectedPaymentMethod.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.onramp + +import arrow.core.Either +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.repositories.OnrampRepository + +class OnrampSaveSelectedPaymentMethod(private val repository: OnrampRepository) { + + suspend operator fun invoke(paymentMethod: OnrampPaymentMethod): Either { + return Either.catch { repository.saveSelectedPaymentMethod(paymentMethod) } + } +} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt index 7ffb44d177..313e735b18 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt @@ -1,10 +1,13 @@ package com.tangem.domain.onramp.repositories import com.tangem.domain.onramp.model.* +import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +@Suppress("TooManyFunctions") interface OnrampRepository { // api suspend fun getCurrencies(): List @@ -14,6 +17,11 @@ interface OnrampRepository { suspend fun fetchPaymentMethodsIfAbsent() suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency) suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount) + suspend fun getOnrampData( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + quote: OnrampProviderWithQuote.Data, + ): OnrampTransaction // cache suspend fun saveDefaultCurrency(currency: OnrampCurrency) @@ -23,6 +31,9 @@ interface OnrampRepository { suspend fun getDefaultCountrySync(): OnrampCountry? fun getDefaultCountry(): Flow suspend fun getPaymentMethods(): List + suspend fun saveSelectedPaymentMethod(paymentMethod: OnrampPaymentMethod) + fun getSelectedPaymentMethod(): Flow fun getQuotes(): Flow> + suspend fun getQuotesSync(): List? suspend fun clearCache() } \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 9a16bc4892..b3981ae397 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -21,7 +21,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index d60dcd1885..c7d9e6d9fd 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { /** Core modules */ implementation(projects.core.ui) implementation(projects.core.utils) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.decompose) implementation(projects.common.routing) diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 93e730eb6f..1d92f0d67a 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.ui) implementation(projects.common.routing) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.analytics) /* Project - Domain */ diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index 8f3c97fef8..cf0f8554a8 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf internal data class CustomTokenFormUM( val networkName: ClickableFieldUM, @@ -29,6 +30,11 @@ internal data class CustomTokenFormUM( SYMBOL, DECIMALS, } + + companion object { + + val Empty = TokenFormUM(fields = persistentMapOf()) + } } data class NotificationUM( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index b0b7f781d9..b5f4d3bfc1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -65,7 +65,7 @@ internal class CustomTokenFormModel @Inject constructor( observeTokenFormUpdates() } - createCoin() + validatePrefilledForm() } private fun getInitialState(): CustomTokenFormUM { @@ -148,11 +148,12 @@ internal class CustomTokenFormModel @Inject constructor( } } - private fun createCoin() = modelScope.launch { - customCurrencyValidator.createCoin( + private fun validatePrefilledForm() = modelScope.launch { + customCurrencyValidator.validateForm( userWalletId = params.userWalletId, networkId = params.network.id, derivationPath = getDerivationPath(), + formValues = state.value.tokenForm.mapToDomainModel(), ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt index d5ba7409da..77a41933ea 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -79,16 +79,12 @@ internal class CustomCurrencyValidator @Inject constructor( findToken(userWalletId, networkId, derivationPath, validatedForm) } is AddCustomTokenForm.Validated.Empty -> { - createCoin(userWalletId, networkId, derivationPath) + createCurrency(userWalletId, networkId, derivationPath, validatedForm = null) } } }.saveInAndJoin(validateFormJobHolder) } - suspend fun createCoin(userWalletId: UserWalletId, networkId: Network.ID, derivationPath: Network.DerivationPath) { - createCurrency(userWalletId, networkId, derivationPath, validatedForm = null) - } - private suspend fun findOrCreateCurrency( userWalletId: UserWalletId, networkId: Network.ID, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt index 6e55316dfb..8a9876d746 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/mapper/TokenFormMapper.kt @@ -3,7 +3,11 @@ package com.tangem.features.managetokens.utils.mapper import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormUM -internal fun CustomTokenFormUM.TokenFormUM.mapToDomainModel(): AddCustomTokenForm.Raw { +internal fun CustomTokenFormUM.TokenFormUM?.mapToDomainModel(): AddCustomTokenForm.Raw { + if (this == null) { + return AddCustomTokenForm.Raw() + } + return AddCustomTokenForm.Raw( contractAddress = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.CONTRACT_ADDRESS).value, symbol = fields.getValue(CustomTokenFormUM.TokenFormUM.Field.SYMBOL).value, diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 94012bfcb8..6b12108830 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -63,7 +63,7 @@ dependencies { /* Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.analytics) implementation(projects.core.analytics.models) diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 64a2e7565d..c603952480 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { implementation(projects.features.manageTokens.api) /** Core modules */ - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.utils) 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 fe67d84f57..529bebe5d7 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 @@ -1,6 +1,6 @@ package com.tangem.features.onboarding.v2 -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import javax.inject.Inject internal class DefaultOnboardingV2FeatureToggles @Inject constructor( 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 ecc501a4c0..d0c206de50 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 @@ -6,9 +6,9 @@ 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.legacy.UserWalletsListManager 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.child.MultiWalletChildParams import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableSharedFlow @@ -17,12 +17,12 @@ import kotlinx.coroutines.launch import javax.inject.Inject @ComponentScoped -class Wallet1ChooseOptionModel @Inject constructor( +internal class Wallet1ChooseOptionModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val generateWalletNameUseCase: GenerateWalletNameUseCase, - private val userWalletsListManager: UserWalletsListManager, private val cardRepository: CardRepository, + private val saveWalletUseCase: SaveWalletUseCase, ) : Model() { private val params = paramsContainer.require() @@ -39,19 +39,17 @@ class Wallet1ChooseOptionModel @Inject constructor( val scanResponse = params.multiWalletState.value.currentScanResponse val userWallet = createUserWallet(scanResponse) - userWalletsListManager.save( - userWallet = userWallet, - canOverride = true, - ) + saveWalletUseCase(userWallet, canOverride = true) + .onRight { + cardRepository.finishCardActivation(scanResponse.card.cardId) - cardRepository.finishCardActivation(scanResponse.card.cardId) + // save user wallet for manage tokens screen + params.multiWalletState.update { + it.copy(resultUserWallet = userWallet) + } - // save user wallet for manage tokens screen - params.multiWalletState.update { - it.copy(resultUserWallet = userWallet) - } - - returnToParentFlow.emit(Unit) + returnToParentFlow.emit(Unit) + } } } diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 9f081a463f..fb353a3220 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { /** Core modules */ implementation(project(":common")) implementation(project(":domain:models")) - implementation(project(":core:featuretoggles")) + implementation(project(":core:config-toggles")) implementation(project(":core:datasource")) implementation(project(":core:analytics")) implementation(projects.core.analytics.models) diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt index 52be1340e5..ac7c7c7800 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt @@ -3,10 +3,11 @@ package com.tangem.features.onramp.component 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 OnrampComponent : ComposableContentComponent { - data class Params(val cryptoCurrency: CryptoCurrency) + data class Params(val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index c97691d146..25efafca87 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -19,10 +19,11 @@ dependencies { implementation(projects.features.swap.domain.models) /** Project - Core */ + implementation(projects.core.analytics) + implementation(projects.core.configToggles) implementation(projects.core.decompose) - implementation(projects.core.ui) - implementation(projects.core.featuretoggles) implementation(projects.core.navigation) + implementation(projects.core.ui) /** Project - Common */ implementation(projects.common.routing) 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 15d7dd6713..337eb159c5 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 @@ -1,6 +1,6 @@ package com.tangem.features.onramp -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager internal class DefaultOnrampFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureModule.kt index 6e668e4f69..c10c24183d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureModule.kt @@ -1,6 +1,6 @@ package com.tangem.features.onramp -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt index cb95070456..f56dec4454 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ui/ConfirmResidencyBottomSheet.kt @@ -115,18 +115,14 @@ private fun CountryContent(name: String, flagUrl: String, isCountrySupported: Bo maxLines = 1, ) - Text( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), - text = stringResource( - id = if (isCountrySupported) { - R.string.onramp_residency_bottomsheet_country_subtitle - } else { - R.string.onramp_residency_bottomsheet_country_not_supported - }, - ), - color = if (isCountrySupported) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.warning, - style = TangemTheme.typography.body2, - ) + if (!isCountrySupported) { + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), + text = stringResource(id = R.string.onramp_residency_bottomsheet_country_not_supported), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.body2, + ) + } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt index 8e4f11228c..a5c15531e0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -16,6 +16,7 @@ import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.model.OnrampMainComponentModel import com.tangem.features.onramp.main.ui.OnrampMainComponentContent +import com.tangem.features.onramp.providers.SelectProviderComponent import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,6 +27,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( @Assisted params: OnrampMainComponent.Params, private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, + private val selectProviderComponentFactory: SelectProviderComponent.Factory, ) : OnrampMainComponent, AppComponentContext by appComponentContext { private val model: OnrampMainComponentModel = getOrCreateModel(params) @@ -60,6 +62,14 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( context = childByContext(componentContext), params = SelectCurrencyComponent.Params(model.bottomSheetNavigation::dismiss), ) + is OnrampMainBottomSheetConfig.ProvidersList -> selectProviderComponentFactory.create( + context = childByContext(componentContext), + params = SelectProviderComponent.Params( + onProviderClick = model::onProviderSelected, + onDismiss = model.bottomSheetNavigation::dismiss, + selectedPaymentMethod = config.selectedPaymentMethod, + ), + ) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt index de585fef2d..b8998352d9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -2,11 +2,16 @@ package com.tangem.features.onramp.main import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.tokens.model.CryptoCurrency internal interface OnrampMainComponent : ComposableContentComponent { - data class Params(val cryptoCurrency: CryptoCurrency, val openSettings: () -> Unit) + data class Params( + val cryptoCurrency: CryptoCurrency, + val openSettings: () -> Unit, + val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt index bc8946cbb5..ef55baf857 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -1,9 +1,11 @@ package com.tangem.features.onramp.main.entity +import com.tangem.domain.onramp.model.OnrampProviderWithQuote + interface OnrampIntents { fun onAmountValueChanged(value: String) fun openSettings() fun openCurrenciesList() - fun onBuyClick() + fun onBuyClick(quote: OnrampProviderWithQuote.Data) fun openProviders() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt index 397816b229..0a6f2b8fa1 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.main.entity import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.domain.onramp.model.OnrampPaymentMethod import kotlinx.serialization.Serializable @Serializable @@ -10,4 +11,7 @@ internal sealed interface OnrampMainBottomSheetConfig { @Serializable data object CurrenciesList : OnrampMainBottomSheetConfig + + @Serializable + data class ProvidersList(val selectedPaymentMethod: OnrampPaymentMethod) : OnrampMainBottomSheetConfig } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index c390cb9a7e..afd1b4e88e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -18,7 +18,6 @@ internal sealed interface OnrampMainComponentUM { val currency: String, val onClose: () -> Unit, val openSettings: () -> Unit, - val onBuyClick: () -> Unit, ) : OnrampMainComponentUM { override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM( title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), @@ -36,7 +35,7 @@ internal sealed interface OnrampMainComponentUM { override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig( text = resourceReference(R.string.common_buy), - onClick = onBuyClick, + onClick = {}, enabled = false, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt index 997292dfb8..5cd08fe2fa 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt @@ -3,7 +3,7 @@ package com.tangem.features.onramp.main.entity import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference -data class OnrampMainTopBarUM( +internal data class OnrampMainTopBarUM( val title: TextReference, val startButtonUM: TopAppBarButtonUM, val endButtonUM: TopAppBarButtonUM, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt index 2d34e8e66d..10469ddea3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt @@ -1,11 +1,12 @@ package com.tangem.features.onramp.main.entity +import com.tangem.domain.onramp.model.OnrampPaymentMethod + sealed class OnrampProviderBlockUM { data object Empty : OnrampProviderBlockUM() data object Loading : OnrampProviderBlockUM() data class Content( - val paymentMethodIconUrl: String, - val paymentMethodName: String, + val paymentMethod: OnrampPaymentMethod, val providerName: String, val isBestRate: Boolean, val onClick: () -> Unit, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index b138b4f5a9..054e19aa36 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -30,7 +30,6 @@ internal class OnrampStateFactory( currency = currency, onClose = onClose, openSettings = onrampIntents::openSettings, - onBuyClick = onrampIntents::onBuyClick, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index d1c630786f..ff6681a318 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.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.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.tokens.model.AmountType import com.tangem.features.onramp.impl.R @@ -55,6 +56,7 @@ internal class OnrampAmountStateFactory( return currentState.copy( amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading), + buyButtonConfig = currentState.buyButtonConfig.copy(enabled = false), ) } @@ -68,13 +70,52 @@ internal class OnrampAmountStateFactory( secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState), ), providerBlockState = quote.toProviderBlockState(), + buyButtonConfig = currentState.buyButtonConfig.copy( + enabled = quote is OnrampQuote.Data, + onClick = { + if (quote is OnrampQuote.Data) { + onrampIntents.onBuyClick( + OnrampProviderWithQuote.Data( + provider = quote.provider, + paymentMethod = quote.paymentMethod, + toAmount = quote.toAmount, + fromAmount = quote.fromAmount, + ), + ) + } + }, + ), + ) + } + + fun getAmountSecondaryUpdatedState(quoteWithProvider: OnrampProviderWithQuote.Data): OnrampMainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampMainComponentUM.Content) return currentState + + val amountState = currentState.amountBlockState + val amount = quoteWithProvider.toAmount.value.format { + crypto(symbol = quoteWithProvider.toAmount.symbol, decimals = quoteWithProvider.toAmount.decimals) + } + return currentState.copy( + amountBlockState = amountState.copy( + secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(stringReference(amount)), + ), + providerBlockState = OnrampProviderBlockUM.Content( + paymentMethod = quoteWithProvider.paymentMethod, + providerName = quoteWithProvider.provider.info.name, + isBestRate = true, + onClick = onrampIntents::openProviders, + ), + buyButtonConfig = currentState.buyButtonConfig.copy( + enabled = true, + onClick = { onrampIntents.onBuyClick(quoteWithProvider) }, + ), ) } private fun OnrampQuote.toProviderBlockState(): OnrampProviderBlockUM { return OnrampProviderBlockUM.Content( - paymentMethodIconUrl = paymentMethod.imageUrl, - paymentMethodName = paymentMethod.name, + paymentMethod = paymentMethod, providerName = provider.info.name, isBestRate = true, onClick = onrampIntents::openProviders, @@ -84,11 +125,8 @@ internal class OnrampAmountStateFactory( private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM { return when (this) { is OnrampQuote.Data -> { - val amount = toAmount.format { - crypto( - symbol = amountState.amountFieldModel.cryptoAmount.currencySymbol, - decimals = amountState.amountFieldModel.cryptoAmount.decimals, - ) + val amount = toAmount.value.format { + crypto(symbol = toAmount.symbol, decimals = toAmount.decimals) } OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index f8d3afd97d..14ffd88063 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -8,10 +8,12 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.domain.onramp.* import com.tangem.domain.onramp.model.OnrampAvailability import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.OnrampIntents import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory import com.tangem.features.onramp.utils.InputManager @@ -60,6 +62,10 @@ internal class OnrampMainComponentModel @Inject constructor( subscribeToAmountChanges() } + fun onProviderSelected(providerWithQuote: OnrampProviderWithQuote.Data) { + _state.update { amountStateFactory.getAmountSecondaryUpdatedState(providerWithQuote) } + } + private fun checkResidenceCountry() { modelScope.launch { checkOnrampAvailabilityUseCase.invoke(params.cryptoCurrency) @@ -125,8 +131,8 @@ internal class OnrampMainComponentModel @Inject constructor( params.openSettings() } - override fun onBuyClick() { - TODO("Not yet implemented") + override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { + params.openRedirectPage(quote) } override fun openCurrenciesList() { @@ -134,6 +140,13 @@ internal class OnrampMainComponentModel @Inject constructor( } override fun openProviders() { + val providerState = (state.value as? OnrampMainComponentUM.Content)?.providerBlockState ?: return + val providerContentState = providerState as? OnrampProviderBlockUM.Content ?: return + bottomSheetNavigation.activate( + OnrampMainBottomSheetConfig.ProvidersList( + selectedPaymentMethod = providerContentState.paymentMethod, + ), + ) } override fun onDestroy() { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt index 0ebdc14d95..cc1fe8f7be 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt @@ -49,7 +49,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: .size(size = TangemTheme.dimens.size40) .clip(TangemTheme.shapes.roundedCorners8), model = ImageRequest.Builder(context = LocalContext.current) - .data(state.paymentMethodIconUrl) + .data(state.paymentMethod.imageUrl) .crossfade(enable = true) .allowHardware(false) .build(), @@ -67,7 +67,7 @@ private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: color = TangemTheme.colors.text.primary1, ), ) { - append(state.paymentMethodName) + append(state.paymentMethod.name) } }, color = TangemTheme.colors.text.tertiary, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt index 08214001de..24c79bdaa6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/paymentmethod/entity/PaymentMethodUM.kt @@ -1,6 +1,9 @@ package com.tangem.features.onramp.paymentmethod.entity -data class PaymentMethodUM( +import kotlinx.serialization.Serializable + +@Serializable +internal data class PaymentMethodUM( val id: String, val imageUrl: String, val name: String, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt new file mode 100644 index 0000000000..e59e4722e9 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/DefaultSelectProviderComponent.kt @@ -0,0 +1,95 @@ +package com.tangem.features.onramp.providers + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +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.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.paymentmethod.SelectPaymentMethodComponent +import com.tangem.features.onramp.providers.entity.ProviderListBottomSheetConfig +import com.tangem.features.onramp.providers.model.SelectProviderModel +import com.tangem.features.onramp.providers.ui.SelectProviderBottomSheet +import com.tangem.features.onramp.providers.ui.SelectProviderBottomSheetContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultSelectProviderComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: SelectProviderComponent.Params, + private val selectPaymentMethodComponentFactory: SelectPaymentMethodComponent.Factory, +) : SelectProviderComponent, AppComponentContext by context { + + private val model: SelectProviderModel = getOrCreateModel(params) + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + + val bottomSheetConfig = remember(key1 = this) { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + SelectProviderBottomSheet( + config = bottomSheetConfig, + content = { + SelectProviderBottomSheetContent( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = TangemTheme.dimens.spacing16), + state = state, + ) + }, + ) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: ProviderListBottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is ProviderListBottomSheetConfig.PaymentMethods -> selectPaymentMethodComponentFactory.create( + context = childByContext(componentContext), + params = SelectPaymentMethodComponent.Params( + selectedMethodId = config.selectedMethodId, + paymentMethods = config.paymentMethodsUM, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } + + @AssistedFactory + interface Factory : SelectProviderComponent.Factory { + override fun create( + context: AppComponentContext, + params: SelectProviderComponent.Params, + ): DefaultSelectProviderComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/SelectProviderComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/SelectProviderComponent.kt new file mode 100644 index 0000000000..1e12daef48 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/SelectProviderComponent.kt @@ -0,0 +1,17 @@ +package com.tangem.features.onramp.providers + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.OnrampProviderWithQuote + +internal interface SelectProviderComponent : ComposableBottomSheetComponent { + + data class Params( + val onProviderClick: (OnrampProviderWithQuote.Data) -> Unit, + val onDismiss: () -> Unit, + val selectedPaymentMethod: OnrampPaymentMethod, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderComponentModule.kt new file mode 100644 index 0000000000..ac7e9c9db1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.providers.di + +import com.tangem.features.onramp.providers.DefaultSelectProviderComponent +import com.tangem.features.onramp.providers.SelectProviderComponent +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 SelectProviderComponentModule { + + @Binds + @Singleton + fun bindSelectProviderComponentFactory( + factory: DefaultSelectProviderComponent.Factory, + ): SelectProviderComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderModelModule.kt new file mode 100644 index 0000000000..812d6bd45d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/di/SelectProviderModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.providers.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.providers.model.SelectProviderModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface SelectProviderModelModule { + + @Binds + @IntoMap + @ClassKey(SelectProviderModel::class) + fun bindSelectProviderModel(model: SelectProviderModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListBottomSheetConfig.kt new file mode 100644 index 0000000000..966e361c38 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListBottomSheetConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.providers.entity + +import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface ProviderListBottomSheetConfig { + + @Serializable + data class PaymentMethods( + val selectedMethodId: String, + val paymentMethodsUM: List, + ) : ProviderListBottomSheetConfig +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt new file mode 100644 index 0000000000..9a1105f0f9 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt @@ -0,0 +1,24 @@ +package com.tangem.features.onramp.providers.entity + +import com.tangem.core.ui.extensions.TextReference + +internal sealed interface ProviderListItemUM { + + val providerId: String + + data class Available( + override val providerId: String, + val imageUrl: String, + val name: String, + val rate: String, + val isSelected: Boolean, + val onClick: () -> Unit, + ) : ProviderListItemUM + + data class Unavailable( + override val providerId: String, + val imageUrl: String, + val name: String, + val subtitle: TextReference, + ) : ProviderListItemUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListPaymentMethodUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListPaymentMethodUM.kt new file mode 100644 index 0000000000..ab8e11e89c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListPaymentMethodUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.providers.entity + +internal data class ProviderListPaymentMethodUM( + val id: String, + val name: String, + val imageUrl: String, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt new file mode 100644 index 0000000000..6b8b8764c2 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.providers.entity + +import kotlinx.collections.immutable.ImmutableList + +internal data class ProviderListUM( + val paymentMethod: ProviderListPaymentMethodUM, + val providers: ImmutableList, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt new file mode 100644 index 0000000000..cbab9bf39e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -0,0 +1,170 @@ +package com.tangem.features.onramp.providers.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +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.domain.onramp.GetOnrampPaymentMethodsUseCase +import com.tangem.domain.onramp.GetOnrampProviderWithQuoteUseCase +import com.tangem.domain.onramp.GetOnrampSelectedPaymentMethodUseCase +import com.tangem.domain.onramp.OnrampSaveSelectedPaymentMethod +import com.tangem.domain.onramp.model.OnrampPaymentMethod +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.paymentmethod.entity.PaymentMethodUM +import com.tangem.features.onramp.providers.SelectProviderComponent +import com.tangem.features.onramp.providers.entity.ProviderListBottomSheetConfig +import com.tangem.features.onramp.providers.entity.ProviderListItemUM +import com.tangem.features.onramp.providers.entity.ProviderListPaymentMethodUM +import com.tangem.features.onramp.providers.entity.ProviderListUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@ComponentScoped +internal class SelectProviderModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val getSelectedPaymentMethodsUseCase: GetOnrampPaymentMethodsUseCase, + private val getOnrampSelectedPaymentMethodUseCase: GetOnrampSelectedPaymentMethodUseCase, + private val getOnrampProviderWithQuoteUseCase: GetOnrampProviderWithQuoteUseCase, + private val saveSelectedPaymentMethod: OnrampSaveSelectedPaymentMethod, + paramsContainer: ParamsContainer, +) : Model() { + + val state: StateFlow get() = _state.asStateFlow() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + private val params: SelectProviderComponent.Params = paramsContainer.require() + private val _state = MutableStateFlow(getInitialState()) + + init { + getProviders(params.selectedPaymentMethod) + subscribeToPaymentMethodUpdates() + } + + private fun subscribeToPaymentMethodUpdates() { + getOnrampSelectedPaymentMethodUseCase.invoke() + .onEach { it.getOrNull()?.let(::getProviders) } + .launchIn(modelScope) + } + + private fun getProviders(paymentMethod: OnrampPaymentMethod) { + modelScope.launch { + getOnrampProviderWithQuoteUseCase.invoke(paymentMethod) + .onRight { quotes -> + _state.update { state -> state.copy(providers = quotes.toProvidersListItems()) } + } + .onLeft { Timber.e(it) } + } + } + + private fun getInitialState(): ProviderListUM { + return ProviderListUM( + paymentMethod = ProviderListPaymentMethodUM( + id = params.selectedPaymentMethod.id, + name = params.selectedPaymentMethod.name, + imageUrl = params.selectedPaymentMethod.imageUrl, + onClick = ::openPaymentMethods, + ), + providers = emptyList().toImmutableList(), + ) + } + + private fun openPaymentMethods() { + modelScope.launch { + val methods = getSelectedPaymentMethodsUseCase.invoke().getOrNull().orEmpty() + bottomSheetNavigation.activate( + ProviderListBottomSheetConfig.PaymentMethods( + selectedMethodId = _state.value.paymentMethod.id, + paymentMethodsUM = methods.toPaymentUMList(), + ), + ) + } + } + + private fun List.toPaymentUMList(): List = map { method -> + PaymentMethodUM( + id = method.id, + imageUrl = method.imageUrl, + name = method.name, + onSelect = { onPaymentMethodSelected(method) }, + ) + } + + private fun onPaymentMethodSelected(paymentMethod: OnrampPaymentMethod) { + modelScope.launch { + saveSelectedPaymentMethod.invoke(paymentMethod) + _state.update { state -> + val paymentMethodUM = state.paymentMethod.copy( + id = paymentMethod.id, + name = paymentMethod.name, + imageUrl = paymentMethod.imageUrl, + ) + state.copy(paymentMethod = paymentMethodUM) + } + bottomSheetNavigation.dismiss() + } + } + + private fun List.toProvidersListItems() = mapIndexed { index, quote -> + when (quote) { + is OnrampProviderWithQuote.Data -> { + val rate = quote.toAmount.value.format { + crypto(symbol = quote.toAmount.symbol, decimals = quote.toAmount.decimals) + } + ProviderListItemUM.Available( + providerId = quote.provider.id, + imageUrl = quote.provider.info.imageLarge, + name = quote.provider.info.name, + rate = rate, + isSelected = index == 0, + onClick = { + params.onProviderClick(quote) + params.onDismiss() + }, + ) + } + is OnrampProviderWithQuote.Unavailable.AvailableFrom -> { + val amount = quote.amount.value.format { + crypto(symbol = quote.amount.symbol, decimals = quote.amount.decimals) + } + ProviderListItemUM.Unavailable( + providerId = quote.provider.id, + imageUrl = quote.provider.info.imageLarge, + name = quote.provider.info.name, + subtitle = resourceReference(R.string.express_provider_min_amount, wrappedList(amount)), + ) + } + is OnrampProviderWithQuote.Unavailable.AvailableUpTo -> { + val amount = quote.amount.value.format { + crypto(symbol = quote.amount.symbol, decimals = quote.amount.decimals) + } + ProviderListItemUM.Unavailable( + providerId = quote.provider.id, + imageUrl = quote.provider.info.imageLarge, + name = quote.provider.info.name, + subtitle = resourceReference(R.string.express_provider_max_amount, wrappedList(amount)), + ) + } + is OnrampProviderWithQuote.Unavailable.NotSupportedPaymentMethod -> { + ProviderListItemUM.Unavailable( + providerId = quote.provider.id, + imageUrl = quote.provider.info.imageLarge, + name = quote.provider.info.name, + subtitle = stringReference( + "Available with ${quote.availablePaymentMethods.joinToString { it.name }}", + ), + ) + } + } + }.toImmutableList() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt new file mode 100644 index 0000000000..6c79dfaa11 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -0,0 +1,199 @@ +package com.tangem.features.onramp.providers.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.util.fastForEach +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.providers.entity.ProviderListItemUM +import com.tangem.features.onramp.providers.entity.ProviderListPaymentMethodUM +import com.tangem.features.onramp.providers.entity.ProviderListUM + +@Composable +internal fun SelectProviderBottomSheet(config: TangemBottomSheetConfig, content: @Composable () -> Unit) { + TangemBottomSheet( + config = config, + titleText = resourceReference(R.string.express_choose_providers_title), + content = { content() }, + ) +} + +@Composable +internal fun SelectProviderBottomSheetContent(state: ProviderListUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier.verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(id = R.string.onramp_choose_provider_title_hint), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + PaymentMethodBlock( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing20, bottom = TangemTheme.dimens.spacing16), + state = state.paymentMethod, + ) + state.providers.fastForEach { provider -> + key(provider.providerId) { ProviderItem(state = provider) } + } + } +} + +@Composable +private fun PaymentMethodBlock(state: ProviderListPaymentMethodUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .border( + width = TangemTheme.dimens.size1, + color = TangemTheme.colors.stroke.secondary, + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + ) + .clickable(onClick = state.onClick) + .padding(all = TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.imageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + Column(modifier = Modifier.weight(1F)) { + Text( + text = stringResource(id = R.string.onramp_pay_with), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + text = state.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.primary1, + ) + } + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_chevron_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +@Composable +private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifier) { + when (state) { + is ProviderListItemUM.Available -> AvailableProviderItem( + modifier = modifier + .clickable(onClick = state.onClick) + .padding(all = TangemTheme.dimens.spacing12), + state = state, + ) + is ProviderListItemUM.Unavailable -> UnavailableProviderItem( + modifier = modifier.padding(all = TangemTheme.dimens.spacing12), + state = state, + ) + } +} + +@Composable +private fun AvailableProviderItem(state: ProviderListItemUM.Available, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.imageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + Text( + modifier = Modifier.weight(1F), + text = state.name, + style = TangemTheme.typography.subtitle2, + color = if (state.isSelected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + ) + Text( + text = state.rate, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } +} + +private const val UNAVAILABLE_ALPHA = 0.4F + +@Composable +private fun UnavailableProviderItem(state: ProviderListItemUM.Unavailable, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size40) + .clip(TangemTheme.shapes.roundedCorners8) + .alpha(UNAVAILABLE_ALPHA), + model = ImageRequest.Builder(context = LocalContext.current) + .data(state.imageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + Column(modifier = Modifier.weight(1F)) { + Text( + text = state.name, + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.secondary, + ) + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt new file mode 100644 index 0000000000..71cedbb3a5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onramp.redirect + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.redirect.model.OnrampRedirectModel +import com.tangem.features.onramp.redirect.ui.OnrampRedirectContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampRedirectComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: OnrampRedirectComponent.Params, +) : OnrampRedirectComponent, AppComponentContext by appComponentContext { + + private val model: OnrampRedirectModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + BackHandler(onBack = params.onBack) + OnrampRedirectContent(modifier = modifier, state = model.state) + } + + @AssistedFactory + interface Factory : OnrampRedirectComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampRedirectComponent.Params, + ): DefaultOnrampRedirectComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/OnrampRedirectComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/OnrampRedirectComponent.kt new file mode 100644 index 0000000000..9a7dcceb3b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/OnrampRedirectComponent.kt @@ -0,0 +1,19 @@ +package com.tangem.features.onramp.redirect + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +internal interface OnrampRedirectComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + val onBack: () -> Unit, + val cryptoCurrency: CryptoCurrency, + val onrampProviderWithQuote: OnrampProviderWithQuote.Data, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectComponentModule.kt new file mode 100644 index 0000000000..4a3a0d5c78 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.redirect.di + +import com.tangem.features.onramp.redirect.DefaultOnrampRedirectComponent +import com.tangem.features.onramp.redirect.OnrampRedirectComponent +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 OnrampRedirectComponentModule { + + @Binds + @Singleton + fun bindOnrampRedirectComponentFactory( + factory: DefaultOnrampRedirectComponent.Factory, + ): OnrampRedirectComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectModelModule.kt new file mode 100644 index 0000000000..21547db511 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/di/OnrampRedirectModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.redirect.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.redirect.model.OnrampRedirectModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampRedirectModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampRedirectModel::class) + fun bindOnrampRedirectModel(model: OnrampRedirectModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectTopBarUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectTopBarUM.kt new file mode 100644 index 0000000000..b7761dec03 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectTopBarUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.onramp.redirect.entity + +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.TextReference + +data class OnrampRedirectTopBarUM(val title: TextReference, val startButtonUM: TopAppBarButtonUM) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectUM.kt new file mode 100644 index 0000000000..22db516242 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/entity/OnrampRedirectUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.redirect.entity + +import com.tangem.core.ui.extensions.TextReference + +internal data class OnrampRedirectUM( + val topBarConfig: OnrampRedirectTopBarUM, + val providerImageUrl: String, + val title: TextReference, + val subtitle: TextReference, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt new file mode 100644 index 0000000000..8d7b3dc545 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -0,0 +1,69 @@ +package com.tangem.features.onramp.redirect.model + +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.navigation.url.UrlOpener +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.redirect.OnrampRedirectComponent +import com.tangem.features.onramp.redirect.entity.OnrampRedirectTopBarUM +import com.tangem.features.onramp.redirect.entity.OnrampRedirectUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +internal class OnrampRedirectModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val urlOpener: UrlOpener, + private val getOnrampRedirectUrlUseCase: GetOnrampRedirectUrlUseCase, + router: Router, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: OnrampRedirectComponent.Params = paramsContainer.require() + val state = OnrampRedirectUM( + topBarConfig = OnrampRedirectTopBarUM( + title = combinedReference( + resourceReference(R.string.common_buy), + stringReference(" ${params.cryptoCurrency.name}"), + ), + startButtonUM = TopAppBarButtonUM( + iconRes = R.drawable.ic_close_24, + onIconClicked = router::pop, + enabled = true, + ), + ), + providerImageUrl = params.onrampProviderWithQuote.provider.info.imageLarge, + title = resourceReference( + R.string.onramp_redirecting_to_provider_title, + wrappedList(params.onrampProviderWithQuote.provider.info.name), + ), + subtitle = resourceReference( + R.string.onramp_redirecting_to_provider_subtitle, + wrappedList(params.onrampProviderWithQuote.provider.info.name), + ), + ) + + init { + getRedirectUrl() + } + + private fun getRedirectUrl() { + modelScope.launch { + getOnrampRedirectUrlUseCase.invoke( + userWalletId = params.userWalletId, + quote = params.onrampProviderWithQuote, + cryptoCurrency = params.cryptoCurrency, + ) + .onLeft { Timber.e(it) } + .onRight { urlOpener.openUrl(it) } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/ui/OnrampRedirectContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/ui/OnrampRedirectContent.kt new file mode 100644 index 0000000000..384f8f83e7 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/ui/OnrampRedirectContent.kt @@ -0,0 +1,133 @@ +package com.tangem.features.onramp.redirect.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.redirect.entity.OnrampRedirectUM + +@Composable +internal fun OnrampRedirectContent(state: OnrampRedirectUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier.imePadding(), + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.secondary, + topBar = { + TangemTopAppBar( + modifier = Modifier.statusBarsPadding(), + startButton = state.topBarConfig.startButtonUM, + title = state.topBarConfig.title.resolveReference(), + ) + }, + content = { innerPadding -> + Content( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize() + .padding(horizontal = TangemTheme.dimens.spacing16), + state = state, + ) + }, + ) +} + +@Composable +private fun Content(state: OnrampRedirectUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + LogoBlock(providerImageUrl = state.providerImageUrl) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), + text = state.title.resolveReference(), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun LogoBlock(providerImageUrl: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + modifier = Modifier + .size(TangemTheme.dimens.size64) + .clip(TangemTheme.shapes.roundedCorners8) + .background(TangemTheme.colors.background.primary) + .padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing12), + painter = painterResource(id = R.drawable.ic_tangem_24), + contentDescription = null, + ) + LoadingDots() + SubcomposeAsyncImage( + modifier = Modifier + .size(size = TangemTheme.dimens.size64) + .clip(TangemTheme.shapes.roundedCorners8), + model = ImageRequest.Builder(context = LocalContext.current) + .data(providerImageUrl) + .crossfade(enable = true) + .allowHardware(false) + .build(), + loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, + contentDescription = null, + ) + } +} + +@Composable +private fun LoadingDots(modifier: Modifier = Modifier) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size6) + .clip(CircleShape) + .background(color = TangemTheme.colors.icon.accent.copy(alpha = 0.75F)), + ) + Box( + modifier = Modifier + .size(TangemTheme.dimens.size6) + .clip(CircleShape) + .background(color = TangemTheme.colors.icon.accent.copy(alpha = 0.45F)), + ) + Box( + modifier = Modifier + .size(TangemTheme.dimens.size8) + .clip(CircleShape) + .background(color = TangemTheme.colors.icon.accent), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt index 36fd583a12..0c6a0c9b77 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -17,6 +17,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.redirect.OnrampRedirectComponent import com.tangem.features.onramp.root.entity.OnrampChild import com.tangem.features.onramp.settings.OnrampSettingsComponent import dagger.assisted.Assisted @@ -29,6 +30,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor( @Assisted private val params: OnrampComponent.Params, private val settingsComponentFactory: OnrampSettingsComponent.Factory, private val onrampMainComponentFactory: OnrampMainComponent.Factory, + private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory, ) : OnrampComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -65,6 +67,23 @@ internal class DefaultOnrampComponent @AssistedInject constructor( params = OnrampMainComponent.Params( cryptoCurrency = params.cryptoCurrency, openSettings = { navigation.push(OnrampChild.Settings) }, + openRedirectPage = { + navigation.push( + OnrampChild.RedirectPage( + quote = it, + cryptoCurrency = params.cryptoCurrency, + ), + ) + }, + ), + ) + is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create( + context = childByContext(componentContext), + params = OnrampRedirectComponent.Params( + userWalletId = params.userWalletId, + onBack = navigation::pop, + cryptoCurrency = config.cryptoCurrency, + onrampProviderWithQuote = config.quote, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt index 994a986713..73dc3b48db 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/entity/OnrampChild.kt @@ -1,5 +1,7 @@ package com.tangem.features.onramp.root.entity +import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.tokens.model.CryptoCurrency import kotlinx.serialization.Serializable @Serializable @@ -9,4 +11,7 @@ internal sealed class OnrampChild { @Serializable data object Settings : OnrampChild() + + @Serializable + data class RedirectPage(val quote: OnrampProviderWithQuote.Data, val cryptoCurrency: CryptoCurrency) : OnrampChild() } \ 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 6947d107cd..5829d1f651 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 @@ -3,6 +3,9 @@ package com.tangem.features.onramp.selecttoken import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -20,6 +23,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +// TODO: Add OnrampOperationModel +@Suppress("LongParameterList") internal class DefaultOnrampOperationComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, onrampTokenListComponentFactory: OnrampTokenListComponent.Factory, @@ -27,6 +32,7 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( private val reduxStateHolder: ReduxStateHolder, private val getWalletsUseCase: GetWalletsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : AppComponentContext by appComponentContext, OnrampOperationComponent { private val onrampTokenListComponent: OnrampTokenListComponent = onrampTokenListComponentFactory.create( @@ -42,6 +48,15 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( ), ) + init { + analyticsEventHandler.send( + event = when (params) { + is OnrampOperationComponent.Params.Buy -> MainScreenAnalyticsEvent.BuyScreenOpened + is OnrampOperationComponent.Params.Sell -> MainScreenAnalyticsEvent.SellScreenOpened + }, + ) + } + @Composable override fun Content(modifier: Modifier) { OnrampSelectToken( @@ -49,13 +64,25 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( is OnrampOperationComponent.Params.Buy -> R.string.common_buy is OnrampOperationComponent.Params.Sell -> R.string.common_sell }, - onBackClick = router::pop, + onBackClick = ::onBackClick, onrampTokenListComponent = onrampTokenListComponent, modifier = modifier, ) } private fun onTokenClick(status: CryptoCurrencyStatus) { + val currencySymbol = status.currency.symbol + analyticsEventHandler.send( + event = when (params) { + is OnrampOperationComponent.Params.Buy -> { + MainScreenAnalyticsEvent.BuyTokenClicked(currencySymbol = currencySymbol) + } + is OnrampOperationComponent.Params.Sell -> { + MainScreenAnalyticsEvent.SellTokenClicked(currencySymbol = currencySymbol) + } + }, + ) + componentScope.launch { val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }.code @@ -76,6 +103,19 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( ) } + private fun onBackClick() { + analyticsEventHandler.send( + event = MainScreenAnalyticsEvent.ButtonClose( + source = when (params) { + is OnrampOperationComponent.Params.Buy -> AnalyticsParam.ScreensSources.Buy + is OnrampOperationComponent.Params.Sell -> AnalyticsParam.ScreensSources.Sell + }, + ), + ) + + router.pop() + } + @AssistedFactory interface Factory : OnrampOperationComponent.Factory { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt index 457590ea6b..7c48a4eb47 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel @@ -21,6 +23,7 @@ import dagger.assisted.AssistedInject internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( tokenListComponentFactory: OnrampTokenListComponent.Factory, availableSwapPairsComponentFactory: AvailableSwapPairsComponent.Factory, + analyticsEventHandler: AnalyticsEventHandler, @Assisted private val appComponentContext: AppComponentContext, @Assisted private val params: SwapSelectTokensComponent.Params, ) : AppComponentContext by appComponentContext, SwapSelectTokensComponent { @@ -37,15 +40,18 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( ), ) - private val selectToTokenListComponent: AvailableSwapPairsComponent = - availableSwapPairsComponentFactory.create( - context = child(key = "select_to_token_list"), - params = AvailableSwapPairsComponent.Params( - userWalletId = params.userWalletId, - selectedStatus = model.fromCurrencyStatus, - onTokenClick = model::selectToToken, - ), - ) + private val selectToTokenListComponent: AvailableSwapPairsComponent = availableSwapPairsComponentFactory.create( + context = child(key = "select_to_token_list"), + params = AvailableSwapPairsComponent.Params( + userWalletId = params.userWalletId, + selectedStatus = model.fromCurrencyStatus, + onTokenClick = model::selectToToken, + ), + ) + + init { + analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened) + } @Composable override fun Content(modifier: Modifier) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 1c9f0ecae6..4d639a5c46 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -81,6 +81,7 @@ internal class AvailableSwapPairsModel @Inject constructor( val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding val filterTokenList = currencies + .filter { it.currency != selectedStatus?.currency } .filterByQuery(query = query) .filterByAvailability(availablePairs = availablePairs) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt index f6cd655930..4e76010bdb 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt @@ -1,6 +1,5 @@ package com.tangem.features.onramp.swap.entity -import com.tangem.core.decompose.navigation.Router import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeFrom import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo import kotlinx.coroutines.flow.MutableStateFlow @@ -12,19 +11,15 @@ import javax.inject.Inject /** * [SwapSelectTokensUM] controller * - * @property router router - * [REDACTED_AUTHOR] */ -internal class SwapSelectTokensController @Inject constructor( - private val router: Router, -) { +internal class SwapSelectTokensController @Inject constructor() { val state: StateFlow get() = _state private val _state: MutableStateFlow = MutableStateFlow( value = SwapSelectTokensUM( - onBackClick = router::pop, + onBackClick = {}, exchangeFrom = createEmptyExchangeFrom(), exchangeTo = createEmptyExchangeTo(), ), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedToTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedToTokenTransformer.kt new file mode 100644 index 0000000000..971bf36fd8 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/RemoveSelectedToTokenTransformer.kt @@ -0,0 +1,29 @@ +package com.tangem.features.onramp.swap.entity.transformer + +import com.tangem.features.onramp.swap.entity.ExchangeCardUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM +import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer +import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo + +/** + * Transformer for removing selected "to" token + * +[REDACTED_AUTHOR] + */ +internal class RemoveSelectedToTokenTransformer( + private val onRemoveFromTokenClick: () -> Unit, +) : SwapSelectTokensUMTransformer { + + override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { + return prevState.copy( + exchangeFrom = prevState.exchangeFrom.showRemoveButton(onClick = onRemoveFromTokenClick), + exchangeTo = createEmptyExchangeTo(), + ) + } + + private fun ExchangeCardUM.showRemoveButton(onClick: () -> Unit): ExchangeCardUM { + return (this as? ExchangeCardUM.Filled) + ?.copy(removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onClick)) + ?: this + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt index 40a3b14a91..6944ed517f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt @@ -1,6 +1,9 @@ package com.tangem.features.onramp.swap.model import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router @@ -10,11 +13,15 @@ import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.onramp.swap.entity.SwapSelectTokensController import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedFromTokenTransformer +import com.tangem.features.onramp.swap.entity.transformer.RemoveSelectedToTokenTransformer import com.tangem.features.onramp.swap.entity.transformer.SelectFromTokenTransformer import com.tangem.features.onramp.swap.entity.transformer.SelectToTokenTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import javax.inject.Inject @Suppress("LongParameterList") @@ -23,6 +30,7 @@ internal class SwapSelectTokensModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val controller: SwapSelectTokensController, private val router: Router, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { val state: StateFlow = controller.state @@ -34,6 +42,12 @@ internal class SwapSelectTokensModel @Inject constructor( private val params = paramsContainer.require() + init { + controller.update { + it.copy(onBackClick = ::onBackClick) + } + } + /** * Select "from" token * @@ -41,12 +55,16 @@ internal class SwapSelectTokensModel @Inject constructor( * @param status crypto currency status */ fun selectFromToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) { + analyticsEventHandler.send( + event = MainScreenAnalyticsEvent.SwapTokenClicked(currencySymbol = status.currency.symbol), + ) + _fromCurrencyStatus.value = status controller.update( transformer = SelectFromTokenTransformer( selectedTokenItemState = selectedTokenItemState, - onRemoveClick = ::onRemoveClick, + onRemoveClick = ::onRemoveFromTokenClick, ), ) } @@ -58,22 +76,67 @@ internal class SwapSelectTokensModel @Inject constructor( * @param status crypto currency status */ fun selectToToken(selectedTokenItemState: TokenItemState, status: CryptoCurrencyStatus) { - _toCurrencyStatus.value = status - - controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState)) - - router.push( - route = AppRoute.Swap( - currencyFrom = requireNotNull(fromCurrencyStatus.value).currency, - currencyTo = status.currency, - userWalletId = params.userWalletId, - ), + analyticsEventHandler.send( + event = MainScreenAnalyticsEvent.ReceiveTokenClicked(currencySymbol = status.currency.symbol), ) + + modelScope.launch { + _toCurrencyStatus.value = status + + controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState)) + + // require some delay to show state with selected "from" and "to" tokens + delay(timeMillis = 500) + + router.push( + route = AppRoute.Swap( + currencyFrom = requireNotNull(fromCurrencyStatus.value).currency, + currencyTo = status.currency, + userWalletId = params.userWalletId, + ), + onComplete = { + modelScope.launch { + withTimeout(timeMillis = 500) { + // Return a state with selected only "from" token + removeSelectedToToken() + } + } + }, + ) + } } - private fun onRemoveClick() { + private fun onBackClick() { + analyticsEventHandler.send( + event = MainScreenAnalyticsEvent.ButtonClose(source = AnalyticsParam.ScreensSources.Swap), + ) + + router.pop() + } + + private fun onRemoveFromTokenClick() { + val currencySymbol = requireNotNull(_fromCurrencyStatus.value?.currency?.symbol) { + "Token was not selected" + } + + analyticsEventHandler.send( + event = MainScreenAnalyticsEvent.RemoveTokenClicked(currencySymbol = currencySymbol), + ) + + removeSelectedFromToken() + } + + private fun removeSelectedFromToken() { _fromCurrencyStatus.value = null controller.update(transformer = RemoveSelectedFromTokenTransformer) } + + private fun removeSelectedToToken() { + _toCurrencyStatus.value = null + + controller.update( + transformer = RemoveSelectedToTokenTransformer(onRemoveFromTokenClick = ::removeSelectedFromToken), + ) + } } \ No newline at end of file diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts index a9965b5681..de3b57819b 100644 --- a/features/push-notifications/impl/build.gradle.kts +++ b/features/push-notifications/impl/build.gradle.kts @@ -27,7 +27,7 @@ dependencies { /** Core modules */ implementation(projects.core.ui) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 14e47173a5..176864f05a 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -43,7 +43,7 @@ dependencies { implementation(deps.tangem.blockchain) /** Core modules */ - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.ui) implementation(projects.core.utils) implementation(projects.core.navigation) diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 808810f2d7..6c77d65471 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -40,7 +40,7 @@ dependencies { implementation(deps.tangem.blockchain) /** Core modules */ - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.ui) implementation(projects.core.utils) implementation(projects.core.navigation) diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 84cbe75297..ddbb0c1e9a 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { /** Core modules */ implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.utils) implementation(projects.core.ui) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 620ca41a28..b5e80ff692 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1721,7 +1721,8 @@ internal class StateBuilder( } private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal { - return this.divide(to, min(decimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP) + val rateDecimals = if (decimals == 0) IF_ZERO_DECIMALS_TO_SHOW else decimals + return this.divide(to, min(rateDecimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP) } private fun toBigDecimalOrNull(text: String): BigDecimal? { @@ -1748,6 +1749,7 @@ internal class StateBuilder( const val ADDRESS_SECOND_PART_LENGTH = 4 private const val PRICE_IMPACT_THRESHOLD = 0.1 private const val MAX_DECIMALS_TO_SHOW = 8 + private const val IF_ZERO_DECIMALS_TO_SHOW = 2 private const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" private const val FEE_READ_MORE_URL_SECOND_PART = "/blog/post/what-is-a-transaction-fee-and-why-do-we-need-it/" } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index b3360ff214..431017f917 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -51,7 +51,6 @@ import com.tangem.feature.swap.router.SwapNavScreen 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.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero @@ -87,7 +86,6 @@ internal class SwapViewModel @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, - private val walletFeatureToggles: WalletFeatureToggles, swapInteractorFactory: SwapInteractor.Factory, private val savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -99,13 +97,8 @@ internal class SwapViewModel @Inject constructor( ?: error("no expected parameter CryptoCurrency (from) found") } - private val initialCurrencyTo: CryptoCurrency? - get() = if (walletFeatureToggles.isMainActionButtonsEnabled) { - savedStateHandle.get(AppRoute.Swap.CURRENCY_TO_KEY) - ?.unbundle(CryptoCurrency.serializer()) - } else { - null - } + private val initialCurrencyTo: CryptoCurrency? = savedStateHandle.get(AppRoute.Swap.CURRENCY_TO_KEY) + ?.unbundle(CryptoCurrency.serializer()) private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Swap.USER_WALLET_ID_KEY) ?.unbundle(UserWalletId.serializer()) diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 0e94a8abeb..6897aff661 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { /** Core modules */ implementation(projects.core.datasource) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.ui) implementation(projects.core.utils) implementation(projects.core.navigation) @@ -47,6 +47,9 @@ dependencies { /** Feature Apis */ implementation(projects.features.tester.api) + /* SDK */ + implementation(deps.tangem.blockchain) + /** Other modules */ implementation(projects.common.routing) implementation(projects.libs.crypto) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 10d761ffb9..4ae38f7c83 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -2,8 +2,10 @@ package com.tangem.feature.tester.presentation import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -17,6 +19,8 @@ import com.tangem.feature.tester.presentation.actions.TesterActionsScreen import com.tangem.feature.tester.presentation.actions.TesterActionsViewModel import com.tangem.feature.tester.presentation.environments.ui.EnvironmentTogglesScreen import com.tangem.feature.tester.presentation.environments.viewmodels.EnvironmentsTogglesViewModel +import com.tangem.feature.tester.presentation.excludedblockchains.ExcludedBlockchainsScreen +import com.tangem.feature.tester.presentation.excludedblockchains.ExcludedBlockchainsViewModel import com.tangem.feature.tester.presentation.featuretoggles.ui.FeatureTogglesScreen import com.tangem.feature.tester.presentation.featuretoggles.viewmodels.FeatureTogglesViewModel import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState @@ -70,6 +74,9 @@ internal class TesterActivity : ComposeActivity() { onBackClick = { appRouter.pop { finish() } }, onFeatureTogglesClick = { innerTesterRouter.open(TesterScreen.FEATURE_TOGGLES) }, onEnvironmentTogglesClick = { innerTesterRouter.open(TesterScreen.ENVIRONMENTS_TOGGLES) }, + onExcludedBlockchainsClick = { + innerTesterRouter.open(TesterScreen.EXCLUDED_BLOCKCHAINS) + }, onTesterActionsClick = { innerTesterRouter.open(TesterScreen.TESTER_ACTIONS) }, ), ) @@ -100,6 +107,15 @@ internal class TesterActivity : ComposeActivity() { TesterActionsScreen(state = viewModel.uiState) } + + composable(route = TesterScreen.EXCLUDED_BLOCKCHAINS.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter, appFinisher) + } + + val state by viewModel.state.collectAsStateWithLifecycle() + ExcludedBlockchainsScreen(state = state) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt new file mode 100644 index 0000000000..2909504146 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsScreen.kt @@ -0,0 +1,190 @@ +package com.tangem.feature.tester.presentation.excludedblockchains + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomFade +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.components.rows.RowContentContainer +import com.tangem.core.ui.components.rows.RowText +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.excludedblockchains.state.BlockchainUM +import com.tangem.feature.tester.presentation.excludedblockchains.state.ExcludedBlockchainsScreenUM +import com.tangem.feature.tester.presentation.excludedblockchains.state.mapper.toUiModels +import kotlinx.collections.immutable.toPersistentList + +@Composable +internal fun ExcludedBlockchainsScreen(state: ExcludedBlockchainsScreenUM, modifier: Modifier = Modifier) { + Scaffold( + modifier = modifier, + containerColor = TangemTheme.colors.background.primary, + topBar = { + TangemTopAppBar( + title = resourceReference(R.string.excluded_blockchains), + startButton = TopAppBarButtonUM.Back(onBackClicked = state.popBack), + endButton = TopAppBarButtonUM( + iconRes = R.drawable.ic_refresh_24, + onIconClicked = state.onRecoverClick, + ), + ) + }, + floatingActionButtonPosition = FabPosition.Center, + floatingActionButton = { + PrimaryButton( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = "Restart App", + onClick = state.onRestartClick, + ) + }, + content = { paddingValues -> + LazyColumn( + modifier = Modifier + .bottomFade() + .padding(paddingValues) + .fillMaxSize(), + contentPadding = PaddingValues( + top = 8.dp, + bottom = 96.dp, + start = 16.dp, + end = 16.dp, + ), + ) { + item { + SearchBar( + modifier = Modifier.padding(bottom = 12.dp), + state = state.search, + ) + } + + item { + TogglesNotification( + modifier = Modifier.padding(vertical = 12.dp), + appVersion = state.appVersion, + isVisible = state.showRecoverWarning, + ) + } + + items( + items = state.blockchains, + key = BlockchainUM::id, + ) { item -> + BlockchainItem( + model = item, + ) + } + } + }, + ) +} + +@Composable +private fun TogglesNotification(appVersion: String, isVisible: Boolean, modifier: Modifier = Modifier) { + if (isVisible) { + Notification( + modifier = modifier, + config = NotificationConfig( + subtitle = resourceReference( + id = R.string.feature_toggles_custom_setup_warning_description, + wrappedList(appVersion), + ), + iconResId = R.drawable.ic_alert_triangle_20, + title = resourceReference(id = R.string.feature_toggles_custom_setup_warning_title), + ), + ) + } +} + +@Composable +private fun BlockchainItem(model: BlockchainUM, modifier: Modifier = Modifier) { + RowContentContainer( + modifier = modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size52), + icon = { + Image( + modifier = Modifier + .align(Alignment.Center) + .size(TangemTheme.dimens.size22), + painter = painterResource(id = model.iconResId), + contentDescription = null, + ) + }, + text = { + RowText( + mainText = model.name, + secondText = model.symbol, + accentMainText = false, + accentSecondText = false, + ) + }, + action = { + TangemSwitch( + checked = model.isExcluded, + onCheckedChange = model.onExcludedStateChange, + ) + }, + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_ExcludedBlockchainsScreen( + @PreviewParameter(ExcludedBlockchainsScreenPreviewProvider::class) params: ExcludedBlockchainsScreenUM, +) { + TangemThemePreview { + ExcludedBlockchainsScreen(params) + } +} + +private class ExcludedBlockchainsScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + ExcludedBlockchainsScreenUM( + blockchains = Blockchain.entries.toUiModels( + excludedBlockchainsIds = setOf(Blockchain.Ethereum.id, Blockchain.Solana.id), + onExcludedStateChange = { _, _ -> }, + ).toPersistentList(), + showRecoverWarning = true, + appVersion = "1.0.0", + search = SearchBarUM( + placeholderText = resourceReference(R.string.excluded_blockchains_search_placeholder), + query = "", + isActive = false, + onQueryChange = {}, + onActiveChange = {}, + ), + popBack = {}, + onRecoverClick = {}, + onRestartClick = {}, + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsViewModel.kt new file mode 100644 index 0000000000..03fa351f78 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/ExcludedBlockchainsViewModel.kt @@ -0,0 +1,150 @@ +package com.tangem.feature.tester.presentation.excludedblockchains + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.excludedblockchains.state.BlockchainUM +import com.tangem.feature.tester.presentation.excludedblockchains.state.ExcludedBlockchainsScreenUM +import com.tangem.feature.tester.presentation.excludedblockchains.state.mapper.toUiModels +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.utils.version.AppVersionProvider +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.mutate +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class ExcludedBlockchainsViewModel @Inject constructor( + private val appVersionProvider: AppVersionProvider, + excludedBlockchainsManager: MutableExcludedBlockchainsManager?, +) : ViewModel() { + + private val excludedBlockchainsManager: MutableExcludedBlockchainsManager = + requireNotNull(excludedBlockchainsManager) { + "Mutable excluded blockchains manager can't be null when tester actions is available" + } + + val state: MutableStateFlow = MutableStateFlow( + value = getInitialState(), + ) + + init { + observeQueryUpdates() + } + + fun setupNavigation(router: InnerTesterRouter, appFinisher: AppFinisher) { + state.update { state -> + state.copy( + popBack = router::back, + onRestartClick = appFinisher::finish, + ) + } + } + + private fun getInitialState(): ExcludedBlockchainsScreenUM = ExcludedBlockchainsScreenUM( + popBack = {}, + search = getInitialSearchBar(), + blockchains = getBlockchains(), + showRecoverWarning = !excludedBlockchainsManager.isMatchLocalConfig(), + appVersion = appVersionProvider.versionName, + onRestartClick = {}, + onRecoverClick = ::recoverLocalConfig, + ) + + @OptIn(FlowPreview::class) + private fun observeQueryUpdates() { + state + .map { it.search.query } + .distinctUntilChanged() + .drop(count = 1) // Skip initial value + .sample(periodMillis = 1_000) + .onEach(::filterBlockchains) + .launchIn(viewModelScope) + } + + private fun recoverLocalConfig() = viewModelScope.launch { + excludedBlockchainsManager.recoverLocalConfig() + + state.update { state -> + state.copy( + search = getInitialSearchBar(), + blockchains = getBlockchains(), + ) + } + } + + private fun getBlockchains(): PersistentList { + val excludedBlockchains = excludedBlockchainsManager.excludedBlockchainsIds + + return Blockchain.entries.toUiModels( + excludedBlockchainsIds = excludedBlockchains, + onExcludedStateChange = ::excludeBlockchain, + ).toPersistentList() + } + + private fun getInitialSearchBar(): SearchBarUM = SearchBarUM( + placeholderText = resourceReference(R.string.excluded_blockchains_search_placeholder), + query = "", + isActive = false, + onQueryChange = ::updateQuery, + onActiveChange = ::updateSearchBarState, + ) + + private fun excludeBlockchain(blockchain: Blockchain, isExcluded: Boolean) = viewModelScope.launch { + excludedBlockchainsManager.excludeBlockchain(blockchain.id, isExcluded) + + state.update { state -> + state.copy( + blockchains = state.blockchains.mutate { mutableBlockchains -> + val index = mutableBlockchains.indexOfFirst { it.id == blockchain.id } + if (index == -1) return@mutate + + mutableBlockchains[index] = mutableBlockchains[index].copy(isExcluded = isExcluded) + }, + showRecoverWarning = !excludedBlockchainsManager.isMatchLocalConfig(), + ) + } + } + + private fun filterBlockchains(query: String) { + val filteredBlockchains = if (query.isBlank()) { + getBlockchains() + } else { + getBlockchains().filter { blockchain -> + blockchain.name.contains(query, ignoreCase = true) || + blockchain.symbol.contains(query, ignoreCase = true) + }.toPersistentList() + } + + state.update { state -> + state.copy( + blockchains = filteredBlockchains, + ) + } + } + + private fun updateQuery(query: String) { + state.update { state -> + state.copy( + search = state.search.copy(query = query), + ) + } + } + + private fun updateSearchBarState(isActive: Boolean) { + state.update { state -> + state.copy( + search = state.search.copy(isActive = isActive), + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/BlockchainUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/BlockchainUM.kt new file mode 100644 index 0000000000..bf406c0243 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/BlockchainUM.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.tester.presentation.excludedblockchains.state + +internal data class BlockchainUM( + val id: String, + val name: String, + val symbol: String, + val iconResId: Int, + val isExcluded: Boolean, + val onExcludedStateChange: (isExcluded: Boolean) -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/ExcludedBlockchainsScreenUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/ExcludedBlockchainsScreenUM.kt new file mode 100644 index 0000000000..0513f4b482 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/ExcludedBlockchainsScreenUM.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.tester.presentation.excludedblockchains.state + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import kotlinx.collections.immutable.PersistentList + +internal data class ExcludedBlockchainsScreenUM( + val popBack: () -> Unit, + val search: SearchBarUM, + val blockchains: PersistentList, + val showRecoverWarning: Boolean, + val appVersion: String, + val onRecoverClick: () -> Unit, + val onRestartClick: () -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/mapper/BlockchainsMappings.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/mapper/BlockchainsMappings.kt new file mode 100644 index 0000000000..d27b8b8c7c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/excludedblockchains/state/mapper/BlockchainsMappings.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.tester.presentation.excludedblockchains.state.mapper + +import com.tangem.blockchain.common.Blockchain +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.feature.tester.presentation.excludedblockchains.state.BlockchainUM + +internal fun List.toUiModels( + excludedBlockchainsIds: Set, + onExcludedStateChange: (Blockchain, Boolean) -> Unit, +): List = mapNotNull { blockchain -> + blockchain + .takeUnless { it == Blockchain.Unknown || it.isTestnet() } + ?.toUiModel( + isExcluded = blockchain.id in excludedBlockchainsIds, + onExcludedStateChange = { isExcluded -> + onExcludedStateChange(blockchain, isExcluded) + }, + ) +} + .sortedBy(BlockchainUM::name) + .sortedByDescending(BlockchainUM::isExcluded) + +private fun Blockchain.toUiModel(isExcluded: Boolean, onExcludedStateChange: (Boolean) -> Unit): BlockchainUM = + BlockchainUM( + id = id, + name = name, + symbol = currency, + iconResId = getActiveIconRes(id), + isExcluded = isExcluded, + onExcludedStateChange = onExcludedStateChange, + ) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index 2af75d3b80..0da5a80bad 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -5,8 +5,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.core.featuretoggle.manager.MutableFeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt index a074e734b9..2dd6061286 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuContentState.kt @@ -12,5 +12,6 @@ data class TesterMenuContentState( val onBackClick: () -> Unit, val onFeatureTogglesClick: () -> Unit, val onEnvironmentTogglesClick: () -> Unit, + val onExcludedBlockchainsClick: () -> Unit, val onTesterActionsClick: () -> Unit, ) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt index 105ea332fd..dd31fe2f08 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/ui/TesterMenuScreen.kt @@ -51,6 +51,12 @@ internal fun TesterMenuScreen(state: TesterMenuContentState) { modifier = Modifier.fillMaxWidth(), ) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.excluded_blockchains), + onClick = state.onExcludedBlockchainsClick, + ) + PrimaryButton( text = stringResource(R.string.environment_toggles), onClick = state.onEnvironmentTogglesClick, @@ -76,6 +82,7 @@ private fun PreviewTesterMenuScreen() { onBackClick = {}, onFeatureTogglesClick = {}, onEnvironmentTogglesClick = {}, + onExcludedBlockchainsClick = {}, onTesterActionsClick = {}, ), ) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index 2d7622855b..5c501ce531 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -6,5 +6,9 @@ package com.tangem.feature.tester.presentation.navigation [REDACTED_AUTHOR] */ internal enum class TesterScreen { - MENU, FEATURE_TOGGLES, ENVIRONMENTS_TOGGLES, TESTER_ACTIONS + MENU, + FEATURE_TOGGLES, + ENVIRONMENTS_TOGGLES, + TESTER_ACTIONS, + EXCLUDED_BLOCKCHAINS, } \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 969d34bce4..95ad57f1ad 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -3,10 +3,12 @@ Tester menu Feature toggles Custom setup - Feature toggles differs from v%s config + Toggles differs from v%s config Restart app Environment toggles Tester actions Hide all currencies Toggle app theme - %s + Excluded blockchains + Filter by name or symbol diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index ac41a99f20..15e9420092 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -54,7 +54,7 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.decompose) implementation(projects.common.ui) @@ -93,5 +93,6 @@ dependencies { implementation(projects.features.send.api) implementation(projects.features.staking.api) implementation(projects.features.markets.api) + implementation(projects.features.onramp.api) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 30a598c8df..ef43fe1b48 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -48,4 +48,8 @@ internal class DefaultTokenDetailsRouter( ), ) } + + override fun openOnrampSuccess(txId: String) { + router.push(AppRoute.OnrampSuccess(txId)) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt index b7c55e065b..93c8591b7b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -19,4 +19,6 @@ internal interface InnerTokenDetailsRouter : TokenDetailsRouter { fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yield: Yield) + + fun openOnrampSuccess(txId: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 782f95ba25..bef30a208e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -66,6 +66,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.E import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -111,6 +112,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, expressStatusFactory: ExpressStatusFactory.Factory, + private val onrampFeatureToggles: OnrampFeatureToggles, getUserWalletUseCase: GetUserWalletUseCase, getStakingIntegrationIdUseCase: GetStakingIntegrationIdUseCase, deepLinksRegistry: DeepLinksRegistry, @@ -193,9 +195,13 @@ internal class TokenDetailsViewModel @Inject constructor( userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: error("UserWallet not found") } - private fun onBuyCurrencyDeepLink() { - val currency = cryptoCurrencyStatus?.currency ?: return - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol)) + private fun onBuyCurrencyDeepLink(txId: String) { + if (onrampFeatureToggles.isFeatureEnabled) { + router.openOnrampSuccess(txId) + } else { + val currency = cryptoCurrencyStatus?.currency ?: return + analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol)) + } } override fun onCreate(owner: LifecycleOwner) { diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index e472817d13..9bad883a97 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 6164b3a3cc..be6834bd6b 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -46,7 +46,7 @@ dependencies { kapt(deps.hilt.kapt) /** Core modules */ - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) @@ -94,6 +94,7 @@ dependencies { implementation(projects.features.details.api) implementation(projects.features.pushNotifications.api) implementation(projects.features.markets.api) + implementation(projects.features.onramp.api) /** Common modules */ implementation(projects.common.ui) 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 cda1776fc1..c3aba027cc 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,6 +1,6 @@ package com.tangem.feature.wallet.featuretoggles -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import javax.inject.Inject diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index 99a762cea6..129a6ff281 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -35,6 +35,7 @@ internal object WalletPreviewData { onRenameClick = { _ -> }, onDeleteClick = {}, cardCount = 1, + isZeroBalance = false, ) } 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 8082bd05e2..fcff8bb806 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 @@ -110,6 +110,7 @@ internal object WalletScreenPreviewData { balance = DASH_SIGN, onRenameClick = { _ -> }, onDeleteClick = {}, + isZeroBalance = false, ) } private val multiWalletState by lazy { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt index d34c68bd50..8a8186fde0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt @@ -16,6 +16,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.features.onramp.OnrampFeatureToggles import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch @@ -31,6 +33,8 @@ internal class WalletDeepLinksHandler @Inject constructor( private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val reduxStateHolder: ReduxStateHolder, + private val clickIntents: WalletClickIntents, + private val onrampFeatureToggles: OnrampFeatureToggles, ) { private var deepLinksMap = mutableMapOf>() @@ -58,11 +62,11 @@ internal class WalletDeepLinksHandler @Inject constructor( return buildList { add(sellCurrencyDeepLink) - if (!userWallet.isMultiCurrency) { + if (onrampFeatureToggles.isFeatureEnabled || !userWallet.isMultiCurrency) { add( BuyCurrencyDeepLink( - onReceive = { - scope.launch { onBuyCurrencyDeepLink(userWallet) } + onReceive = { txId -> + scope.launch { onBuyCurrencyDeepLink(txId, userWallet) } }, ), ) @@ -102,10 +106,13 @@ internal class WalletDeepLinksHandler @Inject constructor( } } - private suspend fun onBuyCurrencyDeepLink(userWallet: UserWallet) { - val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrNull() ?: return - - analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol)) + private suspend fun onBuyCurrencyDeepLink(txId: String, userWallet: UserWallet) { + if (onrampFeatureToggles.isFeatureEnabled) { + clickIntents.onOnrampSuccessClick(txId) + } else { + val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId).getOrNull() ?: return + analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol)) + } } private suspend fun findCryptoCurrencyStatus( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 15a4f2250e..1c79fc38cd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -246,6 +246,7 @@ private fun TopBar( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), ) { RoundedActionButton( + modifier = Modifier.weight(1f), config = ActionButtonConfig( text = TextReference.Res(id = R.string.organize_tokens_sort_by_balance), iconResId = R.drawable.ic_sort_24, @@ -253,10 +254,10 @@ private fun TopBar( onClick = config.onSortClick, dimContent = config.isSortedByBalance, ), - modifier = Modifier.weight(1f), color = TangemTheme.colors.background.primary, ) RoundedActionButton( + modifier = Modifier.weight(1f), config = ActionButtonConfig( text = TextReference.Res( id = if (config.isGrouped) { @@ -265,11 +266,10 @@ private fun TopBar( R.string.organize_tokens_group }, ), - enabled = config.isEnabled, iconResId = R.drawable.ic_group_24, + enabled = config.isEnabled, onClick = config.onGroupClick, ), - modifier = Modifier.weight(1f), color = TangemTheme.colors.background.primary, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt index 93c0aab777..a27f15bdab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensViewModel.kt @@ -23,11 +23,9 @@ import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDr import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.router.WalletRoute import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject @Suppress("LongParameterList") @@ -40,7 +38,6 @@ internal class OrganizeTokensViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val analyticsEventsHandler: AnalyticsEventHandler, - private val dispatchers: CoroutineDispatcherProvider, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, OrganizeTokensIntents { @@ -95,7 +92,7 @@ internal class OrganizeTokensViewModel @Inject constructor( analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance) - viewModelScope.launch(dispatchers.default) { + viewModelScope.launch { toggleTokenListSortingUseCase(list).fold( ifLeft = stateHolder::updateStateWithError, ifRight = { @@ -111,7 +108,7 @@ internal class OrganizeTokensViewModel @Inject constructor( analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group) - viewModelScope.launch(dispatchers.default) { + viewModelScope.launch { toggleTokenListGroupingUseCase(list).fold( ifLeft = stateHolder::updateStateWithError, ifRight = { @@ -123,7 +120,7 @@ internal class OrganizeTokensViewModel @Inject constructor( } override fun onApplyClick() { - viewModelScope.launch(dispatchers.default) { + viewModelScope.launch { stateHolder.updateStateToDisplayProgress() val listState = uiState.value.itemsState @@ -147,8 +144,8 @@ internal class OrganizeTokensViewModel @Inject constructor( result.fold( ifLeft = stateHolder::updateStateWithError, ifRight = { + router.popBackStack() stateHolder.updateStateToHideProgress() - withContext(dispatchers.main) { router.popBackStack() } }, ) } @@ -161,7 +158,7 @@ internal class OrganizeTokensViewModel @Inject constructor( } private fun bootstrapTokenList() { - viewModelScope.launch(dispatchers.default) { + viewModelScope.launch { val tokenList = getTokenList() ?: return@launch stateHolder.updateStateWithTokenList(tokenList) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt index 2bc9fe9dd9..111a8b5211 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt @@ -16,15 +16,7 @@ internal sealed class OrganizeTokensListState { override val items: PersistentList, ) : OrganizeTokensListState() - object Empty : OrganizeTokensListState() { + data object Empty : OrganizeTokensListState() { override val items: PersistentList = persistentListOf() } - - fun copySealed(items: PersistentList = this.items): OrganizeTokensListState { - return when (this) { - is GroupedByNetwork -> copy(items = items) - is Ungrouped -> copy(items = items) - is Empty -> Empty - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt index c567a63a5d..39d8295da6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt @@ -167,9 +167,9 @@ internal class DragAndDropAdapter( sealed class Type { - object Start : Type() + data object Start : Type() - object Dragged : Type() + data object Dragged : Type() data class End(val isItemsOrderChanged: Boolean) : Type() } 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 a2e325f621..7c1a971dd1 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 @@ -112,6 +112,10 @@ internal class DefaultWalletRouter( ) } + override fun openOnrampSuccessScreen(txId: String) { + router.push(AppRoute.OnrampSuccess(txId)) + } + override fun openUrl(url: String) { urlOpener.openUrl(url) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 344887162c..4532ced599 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -38,6 +38,9 @@ internal interface InnerWalletRouter : WalletRouter { /** Open onboarding screen */ fun openOnboardingScreen() + /** Open onramp success screen for [txId] */ + fun openOnrampSuccessScreen(txId: String) + /** Open transaction history website by [url] */ fun openUrl(url: String) 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 9a11997960..6b1652a33a 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 @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet @@ -11,12 +10,10 @@ 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.feature.wallet.presentation.wallet.subscribers.MultiWalletActionsSubscriber 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.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles @Suppress("LongParameterList") internal class MultiWalletContentLoader( @@ -31,8 +28,6 @@ internal class MultiWalletContentLoader( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, - private val rampStateManager: RampStateManager, - private val walletFeatureToggles: WalletFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -55,16 +50,6 @@ internal class MultiWalletContentLoader( getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, ), - createMultiWalletButtonsSubscriber(), ) } - - private fun createMultiWalletButtonsSubscriber(): MultiWalletActionsSubscriber? { - return MultiWalletActionsSubscriber( - userWallet = userWallet, - rampStateManager = rampStateManager, - stateController = stateHolder, - ) - .takeIf { walletFeatureToggles.isMainActionButtonsEnabled } - } } \ No newline at end of file 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 5bc11a8b00..7a70b865b2 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 @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet @@ -12,7 +11,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.viewmodels.intents.WalletClickIntents -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import dagger.hilt.android.scopes.ViewModelScoped import javax.inject.Inject @@ -28,8 +26,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, - private val rampStateManager: RampStateManager, - private val walletFeatureToggles: WalletFeatureToggles, ) { fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { @@ -45,8 +41,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, - rampStateManager = rampStateManager, - walletFeatureToggles = walletFeatureToggles, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index 03fb7f7902..f43173acd0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -83,4 +83,32 @@ internal sealed interface WalletAlertState { ) override val onConfirmClick: (() -> Unit)? = null } + + data object ProvidersStillLoading : Basic() { + override val title: TextReference = stringReference(value = "Providers are still loading") + override val message: TextReference = resourceReference(R.string.wallet_connect_toast_awaiting_session_proposal) + override val onConfirmClick: (() -> Unit)? = null + } + + data object UnavailableOperation : Basic() { + override val title: TextReference = stringReference(value = "This operation is currently unavailable") + override val message: TextReference = resourceReference(R.string.warning_some_networks_unreachable_message) + override val onConfirmClick: (() -> Unit)? = null + } + + data object SellingRegionalRestriction : Basic() { + override val title: TextReference = resourceReference(id = R.string.selling_regional_restriction_alert_title) + override val message: TextReference = + resourceReference(id = R.string.selling_regional_restriction_alert_message) + + override val onConfirmClick: (() -> Unit)? = null + } + + data object InsufficientBalanceForSelling : Basic() { + override val title: TextReference = resourceReference(id = R.string.selling_insufficient_balance_alert_title) + override val message: TextReference = + resourceReference(id = R.string.selling_insufficient_balance_alert_message) + + override val onConfirmClick: (() -> Unit)? = null + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt index 69b9b48fb1..76fa712a77 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt @@ -50,6 +50,7 @@ internal sealed interface WalletCardState { override val onDeleteClick: (UserWalletId) -> Unit, val cardCount: Int?, val balance: String, + val isZeroBalance: Boolean?, ) : WalletCardState /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DisableActionTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DisableActionTransformer.kt new file mode 100644 index 0000000000..9c1a7178bc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DisableActionTransformer.kt @@ -0,0 +1,57 @@ +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.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import timber.log.Timber +import kotlin.reflect.KClass + +/** + * Transformer for disabling action of multi-currency wallet + * + * @param userWalletId user wallet id + * @property actionClass action class that must be disabled + */ +internal class DisableActionTransformer( + userWalletId: UserWalletId, + private val actionClass: KClass, +) : WalletStateTransformer(userWalletId = userWalletId) { + + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.MultiCurrency.Content -> { + prevState.copy(buttons = prevState.buttons.updateButtons()) + } + is WalletState.MultiCurrency.Locked -> { + Timber.w("Impossible to disable action for locked wallet") + prevState + } + is WalletState.SingleCurrency -> { + Timber.w("Impossible to disable action for single-currency wallet") + prevState + } + is WalletState.Visa -> { + Timber.w("Impossible to disable action for VISA wallet") + prevState + } + } + } + + private fun PersistentList.updateButtons(): PersistentList { + return map { action -> + if (action::class == actionClass) { + when (action) { + is WalletManageButton.Buy -> action.copy(enabled = false) + is WalletManageButton.Swap -> action.copy(enabled = false) + is WalletManageButton.Sell -> action.copy(enabled = false) + else -> action + } + } else { + action + } + } + .toPersistentList() + } +} \ No newline at end of file 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 0c85aed9cd..7ff1da092b 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 @@ -58,7 +58,7 @@ internal class InitializeWalletsTransformer( multiCurrencyCreator = { WalletState.MultiCurrency.Locked( walletCardState = userWallet.toLockedWalletCardState(), - buttons = createMultiWalletDisabledButtons(), + buttons = createMultiWalletEnabledButtons(), bottomSheetConfig = null, onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, ) @@ -67,7 +67,7 @@ internal class InitializeWalletsTransformer( WalletState.SingleCurrency.Locked( walletCardState = userWallet.toLockedWalletCardState(), bottomSheetConfig = null, - buttons = createDisabledButtons(), + buttons = createSingleWalletDisabledButtons(), onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, onExploreClick = clickIntents::onExploreClick, ) @@ -75,7 +75,7 @@ internal class InitializeWalletsTransformer( visaWalletCreator = { WalletState.Visa.Locked( walletCardState = userWallet.toLockedWalletCardState(), - buttons = createMultiWalletDisabledButtons(), + buttons = createMultiWalletEnabledButtons(), bottomSheetConfig = null, onUnlockNotificationClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, onExploreClick = clickIntents::onExploreClick, @@ -95,18 +95,17 @@ internal class InitializeWalletsTransformer( ) } - private fun createMultiWalletDisabledButtons(): PersistentList { + private fun createMultiWalletEnabledButtons(): PersistentList { if (!walletFeatureToggles.isMainActionButtonsEnabled) return persistentListOf() return persistentListOf( - WalletManageButton.Receive(enabled = false, dimContent = false, onClick = {}, onLongClick = null), - WalletManageButton.Send(enabled = false, dimContent = false, onClick = {}), WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {}), + WalletManageButton.Swap(enabled = false, dimContent = false, onClick = {}), WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}), ) } - private fun createDisabledButtons(): PersistentList { + private fun createSingleWalletDisabledButtons(): PersistentList { return persistentListOf( WalletManageButton.Receive(enabled = false, dimContent = false, onClick = {}, onLongClick = null), WalletManageButton.Send(enabled = false, dimContent = false, onClick = {}), 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/SetBalancesAndLimitsTransformer.kt index af365a24d7..705ffefdc5 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/SetBalancesAndLimitsTransformer.kt @@ -14,6 +14,7 @@ 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.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.utils.extensions.isZero import org.joda.time.DateTime import org.joda.time.Days @@ -76,6 +77,7 @@ internal class SetBalancesAndLimitsTransformer( crypto(visaCurrency.symbol, visaCurrency.decimals) }, cardCount = userWallet.getCardsCount(), + isZeroBalance = visaCurrency.balances.available.isZero(), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index a13bd46297..39b11b41e6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -9,6 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo 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.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons import timber.log.Timber import java.math.BigDecimal @@ -26,6 +27,11 @@ internal class SetTokenListErrorTransformer( prevState.copy( walletCardState = prevState.walletCardState.toLoadedState(), tokensListState = WalletTokensListState.Empty, + buttons = if (error == TokenListError.EmptyTokens) { + prevState.disableButtons() + } else { + prevState.buttons + }, ) } is WalletState.MultiCurrency.Locked -> { @@ -62,6 +68,7 @@ internal class SetTokenListErrorTransformer( fiatCurrencySymbol = appCurrency.symbol, ), cardCount = selectedWallet.getCardsCount(), + isZeroBalance = true, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 44698f71e3..86d40d6d13 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -8,6 +8,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import timber.log.Timber @@ -24,6 +25,7 @@ internal class SetTokenListTransformer( prevState.copy( walletCardState = prevState.walletCardState.toLoadedState(), tokensListState = prevState.tokensListState.toLoadedState(), + buttons = prevState.enableButtons(), ) } is WalletState.MultiCurrency.Locked -> { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt index 0ff9942daf..d37d638432 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt @@ -8,6 +8,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isZero internal class MultiWalletCardStateConverter( private val fiatBalance: TotalFiatBalance, @@ -58,6 +59,7 @@ internal class MultiWalletCardStateConverter( fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ), + isZeroBalance = fiatBalance.amount.isZero(), cardCount = selectedWallet.getCardsCount(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt index 8ea62cf3fe..c33ff28872 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt @@ -8,6 +8,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isZero internal class SingleWalletCardStateConverter( private val status: CryptoCurrencyStatus.Value, @@ -63,6 +64,7 @@ internal class SingleWalletCardStateConverter( onDeleteClick = onDeleteClick, balance = formatFiatAmount(status = status, appCurrency = appCurrency), cardCount = selectedWallet.getCardsCount(), + isZeroBalance = status.fiatAmount?.isZero(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt new file mode 100644 index 0000000000..e109933f15 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.wallet.presentation.wallet.state.utils + +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal fun WalletState.MultiCurrency.Content.enableButtons(): PersistentList { + return changeAvailability(enabled = true) +} + +internal fun WalletState.MultiCurrency.Content.disableButtons(): PersistentList { + return changeAvailability(enabled = false) +} + +private fun WalletState.MultiCurrency.Content.changeAvailability(enabled: Boolean): PersistentList { + return buttons + .map { action -> + when (action) { + is WalletManageButton.Buy -> action.copy(enabled = enabled) + is WalletManageButton.Sell -> action.copy(enabled = enabled) + is WalletManageButton.Swap -> action.copy(enabled = enabled) + else -> action + } + } + .toPersistentList() +} \ No newline at end of file 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 6b9dda9803..b9aa9f82fd 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 @@ -98,18 +98,18 @@ internal class WalletLoadingStateFactory( return persistentListOf( WalletManageButton.Buy( - enabled = false, - dimContent = true, + enabled = true, + dimContent = false, onClick = { clickIntents.onMultiWalletBuyClick(userWalletId = userWallet.walletId) }, ), WalletManageButton.Swap( - enabled = false, - dimContent = true, + enabled = true, + dimContent = false, onClick = { clickIntents.onMultiWalletSwapClick(userWalletId = userWallet.walletId) }, ), WalletManageButton.Sell( - enabled = false, - dimContent = true, + enabled = true, + dimContent = false, onClick = { clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) }, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 22c9441399..fb40dbfcf1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -34,6 +34,8 @@ internal interface WalletContentClickIntents { fun onOrganizeTokensClick() + fun onOnrampSuccessClick(txId: String) + fun onDismissMarketsOnboarding() fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus) @@ -97,6 +99,10 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } + override fun onOnrampSuccessClick(txId: String) { + router.openOnrampSuccessScreen(txId = txId) + } + override fun onDismissMarketsOnboarding() { stateHolder.update { it.copy(showMarketsOnboarding = false) } viewModelScope.launch { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 23d45eab8b..2eac6d5e37 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -5,6 +5,8 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig @@ -19,9 +21,14 @@ import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.utils.lceError import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.usercountry.GetUserCountryUseCase +import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction @@ -40,17 +47,23 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.DisableActionTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import java.math.BigDecimal import javax.inject.Inject +import kotlin.reflect.KClass interface WalletCurrencyActionsClickIntents { @@ -110,6 +123,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, private val appRouter: AppRouter, + private val rampStateManager: RampStateManager, + private val getUserCountryUseCase: GetUserCountryUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -439,18 +454,59 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onMultiWalletSellClick(userWalletId: UserWalletId) { - // TODO [REDACTED_JIRA] - appRouter.push(route = AppRoute.SellCrypto(userWalletId = userWalletId)) + viewModelScope.launch { + val userCountry = getUserCountryUseCase().getOrNull() + if (userCountry is UserCountry.Russia) { + handleError( + userWalletId = userWalletId, + actionKClass = WalletManageButton.Sell::class, + alertState = WalletAlertState.SellingRegionalRestriction, + eventCreator = MainScreenAnalyticsEvent::ButtonSell, + ) + + return@launch + } + + val selectedWallet = stateHolder.getSelectedWallet().walletCardState as? WalletCardState.Content + if (selectedWallet?.isZeroBalance == true) { + handleError( + userWalletId = userWalletId, + actionKClass = WalletManageButton.Sell::class, + alertState = WalletAlertState.InsufficientBalanceForSelling, + eventCreator = MainScreenAnalyticsEvent::ButtonSell, + ) + + return@launch + } + + onMultiWalletActionClick( + userWalletId = userWalletId, + statusFlow = rampStateManager.getSellInitializationStatus(), + route = AppRoute.SellCrypto(userWalletId = userWalletId), + actionKClass = WalletManageButton.Sell::class, + eventCreator = MainScreenAnalyticsEvent::ButtonSell, + ) + } } override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { - // TODO [REDACTED_JIRA] - appRouter.push(route = AppRoute.SwapCrypto(userWalletId = userWalletId)) + onMultiWalletActionClick( + userWalletId = userWalletId, + statusFlow = rampStateManager.getSwapInitializationStatus(userWalletId), + route = AppRoute.SwapCrypto(userWalletId = userWalletId), + actionKClass = WalletManageButton.Swap::class, + eventCreator = MainScreenAnalyticsEvent::ButtonSwap, + ) } override fun onMultiWalletBuyClick(userWalletId: UserWalletId) { - // TODO [REDACTED_JIRA] - appRouter.push(route = AppRoute.BuyCrypto(userWalletId = userWalletId)) + onMultiWalletActionClick( + userWalletId = userWalletId, + statusFlow = rampStateManager.getBuyInitializationStatus(), + route = AppRoute.BuyCrypto(userWalletId = userWalletId), + actionKClass = WalletManageButton.Buy::class, + eventCreator = MainScreenAnalyticsEvent::ButtonBuy, + ) } private fun openExplorer() { @@ -550,4 +606,69 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( return true } + + private fun onMultiWalletActionClick( + userWalletId: UserWalletId, + statusFlow: Flow>, + route: AppRoute, + actionKClass: KClass, + eventCreator: (AnalyticsParam.Status) -> MainScreenAnalyticsEvent, + ) { + viewModelScope.launch { + statusFlow.foldStatus( + onContent = { handleContent(route, eventCreator) }, + onError = { + handleError( + userWalletId = userWalletId, + actionKClass = actionKClass, + eventCreator = eventCreator, + ) + }, + onLoading = { handleLoading(eventCreator) }, + ) + } + } + + private suspend fun Flow>.foldStatus( + onContent: () -> Unit, + onError: () -> Unit, + onLoading: () -> Unit, + ) { + val status = firstOrNull() ?: IllegalStateException("Status is null").lceError() + + when (status) { + is Lce.Content -> onContent() + is Lce.Error -> onError() + is Lce.Loading -> onLoading() + } + } + + private fun handleContent(route: AppRoute, eventCreator: (AnalyticsParam.Status) -> MainScreenAnalyticsEvent) { + analyticsEventHandler.send(event = eventCreator(AnalyticsParam.Status.Success)) + + appRouter.push(route = route) + } + + private fun handleError( + userWalletId: UserWalletId, + actionKClass: KClass, + alertState: WalletAlertState = WalletAlertState.UnavailableOperation, + eventCreator: (AnalyticsParam.Status) -> MainScreenAnalyticsEvent, + ) { + analyticsEventHandler.send(event = eventCreator(AnalyticsParam.Status.Error)) + + stateHolder.update( + transformer = DisableActionTransformer(userWalletId = userWalletId, actionClass = actionKClass), + ) + + walletEventSender.send(event = WalletEvent.ShowAlert(state = alertState)) + } + + private fun handleLoading(eventCreator: (AnalyticsParam.Status) -> MainScreenAnalyticsEvent) { + analyticsEventHandler.send(event = eventCreator(AnalyticsParam.Status.Pending)) + + walletEventSender.send( + event = WalletEvent.ShowAlert(state = WalletAlertState.ProvidersStillLoading), + ) + } } \ No newline at end of file diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts index 47b65e7313..96dcda0905 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/build.gradle.kts @@ -13,7 +13,7 @@ dependencies { // region Core modules implementation(projects.core.datasource) - implementation(projects.core.featuretoggles) + implementation(projects.core.configToggles) implementation(projects.core.utils) // endregion 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 de38a2241f..e0ac497835 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 @@ -9,7 +9,7 @@ import com.tangem.blockchainsdk.datastorage.DefaultBlockchainDataStorage import com.tangem.blockchainsdk.featuretoggles.DefaultBlockchainSDKFeatureToggles import com.tangem.blockchainsdk.loader.BlockchainProvidersResponseLoader import com.tangem.blockchainsdk.store.DefaultRuntimeStore -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore 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 29df94634c..2242f2675f 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,6 +1,6 @@ package com.tangem.blockchainsdk.featuretoggles -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.configtoggle.feature.FeatureTogglesManager internal class DefaultBlockchainSDKFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, diff --git a/settings.gradle.kts b/settings.gradle.kts index f915fb1945..d8139ac1f0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -127,7 +127,7 @@ include(":common:routing") include(":core:analytics") include(":core:analytics:models") include(":core:datasource") -include(":core:featuretoggles") +include(":core:config-toggles") include(":core:navigation") include(":core:res") include(":core:ui")