diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d8615f2502..c6f66b3c04 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,6 +14,11 @@ android { testOptions { animationsDisabled = true } + packaging { + jniLibs { + useLegacyPackaging = true + } + } } configurations.all { @@ -71,6 +76,7 @@ dependencies { implementation(projects.domain.walletConnect) implementation(projects.common) + implementation(projects.common.routing) implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) @@ -130,6 +136,12 @@ dependencies { implementation(projects.features.staking.impl) implementation(projects.features.details.api) implementation(projects.features.details.impl) + implementation(projects.features.disclaimer.api) + implementation(projects.features.disclaimer.impl) + implementation(projects.features.pushNotifications.api) + implementation(projects.features.pushNotifications.impl) + implementation(projects.features.walletSettings.api) + implementation(projects.features.walletSettings.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) @@ -166,6 +178,7 @@ dependencies { implementation(platform(deps.firebase.bom)) implementation(deps.firebase.analytics) implementation(deps.firebase.crashlytics) + implementation(deps.firebase.messaging) /** Tangem libraries */ implementation(deps.tangem.blockchain) { diff --git a/app/src/debug/res/values/values.xml b/app/src/debug/res/values/values.xml new file mode 100644 index 0000000000..8cc3a85358 --- /dev/null +++ b/app/src/debug/res/values/values.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/external/res/values/values.xml b/app/src/external/res/values/values.xml new file mode 100644 index 0000000000..8cc3a85358 --- /dev/null +++ b/app/src/external/res/values/values.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/internal/res/values/values.xml b/app/src/internal/res/values/values.xml new file mode 100644 index 0000000000..8cc3a85358 --- /dev/null +++ b/app/src/internal/res/values/values.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 536e770195..02e790d2c8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,11 +40,11 @@ android:fullBackupContent="false" android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" + android:resizeableActivity="@bool/resizeable_activity" android:label="@string/tangem_app_name" android:largeHeap="@bool/largeHeap" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher" - android:extractNativeLibs="true" android:supportsRtl="true" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:allowBackup, android:fullBackupContent, android:label"> @@ -146,5 +146,12 @@ android:resource="@xml/provider_paths" /> + + + + + diff --git a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt index 16a05117ac..3ef77ac909 100644 --- a/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt +++ b/app/src/main/java/com/tangem/tap/ActivityResultCaller.kt @@ -1,8 +1,32 @@ package com.tangem.tap import android.content.Intent +import android.hardware.biometrics.BiometricManager +import android.os.Build +import android.provider.Settings import androidx.activity.result.ActivityResultLauncher interface ActivityResultCaller { val activityResultLauncher: ActivityResultLauncher? +} + +internal fun ActivityResultCaller.openSystemBiometrySettings() { + val settingsAction = when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { + Settings.ACTION_BIOMETRIC_ENROLL + } + else -> { + Settings.ACTION_SECURITY_SETTINGS + } + } + val intent = Intent(settingsAction).apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + putExtra( + Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, + BiometricManager.Authenticators.BIOMETRIC_STRONG, + ) + } + } + + activityResultLauncher?.launch(intent) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index c86e0f9392..967026bf4c 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -2,8 +2,11 @@ package com.tangem.tap import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.connection.NetworkConnectionManager @@ -15,6 +18,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -26,9 +30,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.details.DetailsFeatureToggles -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles @@ -59,8 +62,6 @@ interface ApplicationEntryPoint { fun getWalletConnectSessionsRepository(): WalletConnectSessionsRepository - fun getManageTokensFeatureToggles(): ManageTokensFeatureToggles - fun getScanCardProcessor(): ScanCardProcessor fun getAppCurrencyRepository(): AppCurrencyRepository @@ -109,5 +110,13 @@ interface ApplicationEntryPoint { fun getDetailsFeatureToggles(): DetailsFeatureToggles - fun getDetailsEntryPoint(): DetailsEntryPoint + fun getGetCardInfoUseCase(): GetCardInfoUseCase + + fun getUrlOpener(): UrlOpener + + fun getShareManager(): ShareManager + + fun getAppRouter(): AppRouter + + fun getPushNotificationsFeatureToggles(): PushNotificationsFeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt new file mode 100644 index 0000000000..296704a5e1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt @@ -0,0 +1,71 @@ +package com.tangem.tap + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.message.EventMessageEffect +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.utils.Provider +import dagger.hilt.android.AndroidEntryPoint +import java.util.WeakHashMap +import javax.inject.Inject + +@AndroidEntryPoint +internal class DecomposeFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + private val component by lazy(mode = LazyThreadSafetyMode.NONE) { + val tag = requireArguments().getString(TAG_KEY) + val builder = componentsBuilders[tag] + + requireNotNull(builder?.build()) { + "Component builder is not set, call newInstance() for DecomposeFragment creation first." + } + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + component.Content(modifier) + + EventMessageEffect( + messageHandler = uiDependencies.eventMessageHandler, + snackbarHostState = uiDependencies.globalSnackbarHostState, + ) + } + + private class ComponentBuilder>( + private val contextProvider: Provider, + private val params: P, + private val componentFactory: F, + ) { + + fun build(): C = componentFactory.create(contextProvider(), params) + } + + companion object { + + private const val TAG_KEY = "tag" + + private val componentsBuilders = WeakHashMap>() + + fun > newInstance( + tag: String, + contextProvider: Provider, + params: P, + componentFactory: F, + ): Fragment { + this@Companion.componentsBuilders[tag] = ComponentBuilder(contextProvider, params, componentFactory) + + return DecomposeFragment().apply { + this.arguments = bundleOf(TAG_KEY to tag) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index bb6e616898..8a80aff9f7 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -3,13 +3,12 @@ package com.tangem.tap import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import kotlinx.coroutines.* import timber.log.Timber import kotlin.time.Duration @@ -50,7 +49,7 @@ internal class LockUserWalletsTimer( start() if (shouldOpenWelcomeScreenOnResume) { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome)) + store.dispatchNavigationAction { popTo() } settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false) } } @@ -128,7 +127,7 @@ internal class LockUserWalletsTimer( if (wasApplicationStopped) { settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) } else { - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Welcome)) + store.dispatchNavigationAction { popTo() } } } } diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index ea644f792a..edd4f38350 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -1,40 +1,41 @@ package com.tangem.tap -import android.Manifest import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Intent import android.content.IntentFilter import android.content.pm.ActivityInfo -import android.content.pm.PackageManager import android.content.res.Configuration import android.nfc.NfcAdapter -import android.os.Build import android.os.Bundle import android.view.View +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.coordinatorlayout.widget.CoordinatorLayout -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import androidx.core.os.bundleOf import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.WindowCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding +import com.arkivanov.decompose.value.observe +import com.arkivanov.essenty.lifecycle.asEssentyLifecycle import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DeepLinksRegistry -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference @@ -43,6 +44,7 @@ import com.tangem.core.ui.res.TangemColorPalette import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase @@ -50,8 +52,9 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent -import com.tangem.features.managetokens.navigation.ManageTokensUi +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -62,6 +65,9 @@ import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -71,17 +77,17 @@ import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHan import com.tangem.tap.features.main.MainViewModel import com.tangem.tap.features.main.model.Toast import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.features.welcome.ui.WelcomeFragment import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction +import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.routing.RoutingComponent +import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import com.tangem.wallet.BuildConfig import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import java.lang.ref.WeakReference import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -128,9 +134,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter - @Inject - lateinit var manageTokensUi: ManageTokensUi - @Inject lateinit var walletConnectInteractor: WalletConnectInteractor @@ -161,6 +164,28 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var emailSender: EmailSender + @Inject + lateinit var stakingRouter: StakingRouter + + @Inject + @RootAppComponentContext + internal lateinit var rootComponentContext: AppComponentContext + + @Inject + internal lateinit var appRouterConfig: AppRouterConfig + + @Inject + internal lateinit var routingComponentFactory: RoutingComponent.Factory + + @Inject + internal lateinit var appRouter: AppRouter + + @Inject + lateinit var pushNotificationsRouter: PushNotificationsRouter + + @Inject + lateinit var cardRepository: CardRepository + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -180,6 +205,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac val splashScreen = installSplashScreen() + enableEdgeToEdge( + navigationBarStyle = SystemBarStyle.auto( + Color.Transparent.toArgb(), + Color.Transparent.toArgb(), + ), + ) + super.onCreate(savedInstanceState) splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } @@ -188,9 +220,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac observeAppThemeModeUpdates() setContentView(R.layout.activity_main) + installRouting() initContent() - checkForNotificationPermission() observeStateUpdates() observePolkadotAccountHealthCheck() @@ -199,6 +231,31 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } + private fun installRouting() { + val routingComponent = routingComponentFactory.create( + context = rootComponentContext, + ) + + appRouterConfig.routerScope = lifecycleScope + appRouterConfig.componentRouter = routingComponent.router + + routingComponent.stack.observe(lifecycle.asEssentyLifecycle()) { childStack -> + appRouterConfig.stack = childStack.backStack + .plus(childStack.active) + .map { it.configuration } + + when (val child = childStack.active.instance) { + is RoutingComponent.Child.Initial -> Unit + is RoutingComponent.Child.LegacyFragment -> { + supportFragmentManager.showFragmentAllowingStateLoss(child.name, child.fragmentProvider) + } + is RoutingComponent.Child.LegacyIntent -> { + startActivity(child.intent) + } + } + } + } + private fun observeStateUpdates() { viewModel.state .flowWithLifecycle(lifecycle) @@ -220,8 +277,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun installActivityDependencies() { - store.dispatch(NavigationAction.ActivityCreated(WeakReference(this))) - cardSdkOwner.register(activity = this) tangemSdkManager = injectedTangemSdkManager appStateHolder.tangemSdkManager = tangemSdkManager @@ -241,11 +296,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac walletRouter = walletRouter, walletConnectInteractor = walletConnectInteractor, tokenDetailsRouter = tokenDetailsRouter, - manageTokensUi = manageTokensUi, cardSdkConfigRepository = cardSdkConfigRepository, sendRouter = sendRouter, qrScanningRouter = qrScanningRouter, emailSender = emailSender, + stakingRouter = stakingRouter, + pushNotificationsRouter = pushNotificationsRouter, ), ) } @@ -266,8 +322,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @SuppressLint("SourceLockedOrientationActivity") private fun initContent() { - WindowCompat.setDecorFitsSystemWindows(window, false) - supportFragmentManager.registerFragmentLifecycleCallbacks( NavBarInsetsFragmentLifecycleCallback(), true, @@ -298,19 +352,21 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onResume() { super.onResume() - val nfcAdapter = NfcAdapter.getDefaultAdapter(this) - val pendingIntent = PendingIntent.getActivity( - this, - 0, - Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, - ) - val intentFilters = arrayOf( - IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), - IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), - IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), - ) - nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) + val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) + if (nfcAdapter?.isEnabled == true) { + val pendingIntent = PendingIntent.getActivity( + this, + 0, + Intent(this, javaClass).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + ) + val intentFilters = arrayOf( + IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TAG_DISCOVERED), + IntentFilter(NfcAdapter.ACTION_TECH_DISCOVERED), + ) + nfcAdapter.enableForegroundDispatch(this, pendingIntent, intentFilters, null) + } // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop notificationsHandler = NotificationsHandler(binding.fragmentContainer) @@ -319,8 +375,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onPause() { super.onPause() - val nfcAdapter = NfcAdapter.getDefaultAdapter(this) - nfcAdapter.disableForegroundDispatch(this) + val nfcAdapter: NfcAdapter? = NfcAdapter.getDefaultAdapter(this) + + if (nfcAdapter?.isEnabled == true) { + nfcAdapter.disableForegroundDispatch(this) + } } override fun onStop() { @@ -330,7 +389,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } override fun onDestroy() { - store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this))) intentProcessor.removeAll() super.onDestroy() } @@ -470,8 +528,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { - val backStack = store.state.navigationState.backStack - val isOnInitialScreen = backStack.all { it == AppScreen.Welcome || it == AppScreen.Home } + val backStack = appRouter.stack + val isOnInitialScreen = backStack.all { it is AppRoute.Welcome || it is AppRoute.Home } val isNotScannedBefore = store.state.globalState.scanResponse == null val isOnboardingServiceNotActive = !store.state.globalState.onboardingState.onboardingStarted @@ -488,17 +546,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) { if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) { - store.dispatch( - NavigationAction.NavigateTo( - screen = AppScreen.Welcome, - bundle = intentWhichStartedActivity?.let { - bundleOf(WelcomeFragment.INITIAL_INTENT_KEY to it) - }, - ), - ) + store.dispatchNavigationAction { + replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent))) + } } else { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) lifecycleScope.launch { + val toggles = store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles) + val isEnabled = toggles.isPushNotificationsEnabled && !cardRepository.isTangemTOSAccepted() + val route = if (isEnabled) AppRoute.Disclaimer(isTosAccepted = false) else AppRoute.Home + + store.dispatchNavigationAction { replaceAll(route) } intentProcessor.handleIntent(intentWhichStartedActivity, false) } } @@ -506,16 +563,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch(BackupAction.CheckForUnfinishedBackup) } - private fun checkForNotificationPermission() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - BuildConfig.LOG_ENABLED && - ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != - PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0) - } - } - private fun observePolkadotAccountHealthCheck() { lifecycleScope.launch { getPolkadotCheckHasResetUseCase() diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index d9b90a7bb9..9a6577a66f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -13,6 +13,7 @@ import com.tangem.LogFormat import com.tangem.TangemSdkLogger import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager @@ -32,6 +33,7 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.LogConfig import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase @@ -43,9 +45,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.details.DetailsFeatureToggles -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder @@ -53,7 +54,7 @@ import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHand import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.feedback.AdditionalFeedbackInfo -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.common.log.TimberFormatStrategy @@ -110,9 +111,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val walletConnectSessionsRepository: WalletConnectSessionsRepository get() = entryPoint.getWalletConnectSessionsRepository() - private val manageTokensFeatureToggles: ManageTokensFeatureToggles - get() = entryPoint.getManageTokensFeatureToggles() - private val scanCardProcessor: ScanCardProcessor get() = entryPoint.getScanCardProcessor() @@ -181,13 +179,25 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase get() = entryPoint.getSaveBlockchainErrorUseCase() - // endregion + + private val getCardInfoUseCase: GetCardInfoUseCase + get() = entryPoint.getGetCardInfoUseCase() private val detailsFeatureToggles: DetailsFeatureToggles get() = entryPoint.getDetailsFeatureToggles() - private val detailsEntryPoint: DetailsEntryPoint - get() = entryPoint.getDetailsEntryPoint() + private val urlOpener + get() = entryPoint.getUrlOpener() + + private val shareManager + get() = entryPoint.getShareManager() + + private val appRouter: AppRouter + get() = entryPoint.getAppRouter() + + private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles + get() = entryPoint.getPushNotificationsFeatureToggles() + // endregion override fun onCreate() { super.onCreate() @@ -253,7 +263,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { customTokenFeatureToggles = customTokenFeatureToggles, walletConnectRepository = walletConnect2Repository, walletConnectSessionsRepository = walletConnectSessionsRepository, - manageTokensFeatureToggles = manageTokensFeatureToggles, scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, @@ -274,9 +283,14 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { settingsRepository = settingsRepository, blockchainSDKFactory = blockchainSDKFactory, saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, - detailsFeatureToggles = detailsFeatureToggles, - detailsEntryPoint = detailsEntryPoint, + getFeedbackEmailUseCase = getFeedbackEmailUseCase, + getCardInfoUseCase = getCardInfoUseCase, assetLoader = assetLoader, + detailsFeatureToggles = detailsFeatureToggles, + urlOpener = urlOpener, + shareManager = shareManager, + appRouter = appRouter, + pushNotificationsFeatureToggles = pushNotificationsFeatureToggles, ), ), ) @@ -365,7 +379,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { logger = if (feedbackManagerFeatureToggles.isLocalLogsEnabled) tangemSdkLogger else tangemLogCollector, ) - val feedbackManager = FeedbackManager( + val feedbackManager = LegacyFeedbackManager( infoHolder = additionalFeedbackInfo, logCollector = tangemLogCollector, chatManager = ChatManager(foregroundActivityObserver), diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 6b870386be..936230d205 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common import android.app.Dialog import android.content.Context -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.ui.* @@ -46,7 +46,11 @@ class DialogManager : StoreSubscriber { dialog = when (state.dialog) { is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context) - is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context, state.dialog.source) + is StateDialog.ScanFailsDialog -> ScanFailsDialog.create( + context = context, + source = state.dialog.source, + onTryAgain = state.dialog.onTryAgain, + ) is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context) is AppDialog.RussianCardholdersWarningDialog -> RussianCardholdersWarningBottomSheetDialog( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt new file mode 100644 index 0000000000..392f340405 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.analytics.events + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam + +class ScanFailsDialogAnalytics(button: Buttons, source: AnalyticsParam.ScreensSources) : AnalyticsEvent( + category = "Cant Scan The Card", + event = button.event, + params = mapOf( + AnalyticsParam.SOURCE to source.value, + ), +) { + enum class Buttons(val event: String) { + TRY_AGAIN("Try again button"), + HOW_TO_SCAN("Button blog"), + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 4b0a740760..251edacdf4 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -1,171 +1,51 @@ package com.tangem.tap.common.extensions -import android.os.Bundle -import androidx.fragment.app.* -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.FragmentShareTransition -import com.tangem.feature.referral.ReferralFragment -import com.tangem.feature.swap.presentation.SwapFragment -import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment -import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment -import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment -import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment -import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment -import com.tangem.tap.features.details.ui.details.DetailsFragment -import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment -import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment -import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment -import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment -import com.tangem.tap.features.home.HomeFragment -import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment -import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment -import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment -import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment -import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment -import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment -import com.tangem.tap.features.send.ui.SendFragment -import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment -import com.tangem.tap.features.welcome.ui.WelcomeFragment -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import com.tangem.utils.Provider import com.tangem.wallet.R import timber.log.Timber -fun FragmentActivity.openFragment( - screen: AppScreen, - addToBackstack: Boolean, - bundle: Bundle? = null, - fgShareTransition: FragmentShareTransition? = null, -) { - val transaction = supportFragmentManager.beginTransaction().apply { - setReorderingAllowed(true) - } +fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider) { + Timber.d("Showing $name route") - val fragment = fragmentFactory(screen).apply { - arguments = bundle + val isPoppedBack = popBackStackImmediate(name, 0) - if (fgShareTransition != null) { - sharedElementEnterTransition = fgShareTransition.enterTransitionSet - sharedElementReturnTransition = fgShareTransition.exitTransitionSet - fgShareTransition.shareElements.forEach { shareElement -> - shareElement.wView.get()?.let { view -> - transaction.addSharedElement(view, shareElement.elementName) - } - } + if (!isPoppedBack) { + val fragment = fragmentProvider() + + if (fragment is DialogFragment) { + fragment.showDialog(fragmentManager = this, name) + } else { + fragment.showFragment(fragmentManager = this, name) } - } - if (screen.isDialogFragment && fragment is DialogFragment) { - fragment.showAllowingStateLoss( - fragmentManager = supportFragmentManager, - baseTransaction = transaction, - tag = screen.name, - addToBackstack = addToBackstack, - ) + Timber.d("Route $name is shown") } else { - transaction.replace(R.id.fragment_container, fragment, screen.name) - if (addToBackstack) { - transaction.addToBackStack(screen.name) - } + Timber.d("Route $name is found in backstack and shown") + } +} + +private fun DialogFragment.showDialog(fragmentManager: FragmentManager, name: String) { + val transaction = fragmentManager.beginTransaction() + + try { + transaction.addToBackStack(name) + show(transaction, name) + } catch (e: IllegalStateException) { + transaction.add(this, name) + transaction.addToBackStack(name) + transaction.commitAllowingStateLoss() } } -private fun DialogFragment.showAllowingStateLoss( - fragmentManager: FragmentManager, - baseTransaction: FragmentTransaction, - tag: String, - addToBackstack: Boolean, -) { - runCatching { - if (addToBackstack) baseTransaction.addToBackStack(tag) - show(baseTransaction, tag) - } - .onFailure { throwable -> - if (throwable is IllegalStateException) { - val transaction = fragmentManager.beginTransaction() - transaction.add(this, tag) - if (addToBackstack) transaction.addToBackStack(tag) +private fun Fragment.showFragment(fragmentManager: FragmentManager, name: String) { + val transaction = fragmentManager.beginTransaction() - transaction.commitAllowingStateLoss() - } else { - Timber.e(throwable) - } - } -} + transaction.replace(R.id.fragment_container, this, name) + transaction.addToBackStack(name) -fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) { - val inclusiveFlag = if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0 - try { - this.supportFragmentManager.popBackStack(screen?.name, inclusiveFlag) - } catch (e: IllegalStateException) { - Timber.e(e) - } -} - -fun FragmentActivity.getPreviousScreen(): AppScreen? { - val indexOfLastFragment = if (this.supportFragmentManager.backStackEntryCount > 0) { - this.supportFragmentManager.backStackEntryCount - 1 - } else { - 0 - } - val tag = if (indexOfLastFragment < this.supportFragmentManager.backStackEntryCount) { - this.supportFragmentManager.getBackStackEntryAt(indexOfLastFragment).name - } else { - null - } - return tag?.let { AppScreen.valueOf(tag) } -} - -@Suppress("ComplexMethod", "LongMethod") -private fun fragmentFactory(screen: AppScreen): Fragment { - return when (screen) { - AppScreen.Home -> HomeFragment() - AppScreen.OnboardingNote -> OnboardingNoteFragment() - AppScreen.OnboardingWallet -> OnboardingWalletFragment() - AppScreen.OnboardingTwins -> OnboardingTwinsFragment() - AppScreen.OnboardingOther -> OnboardingOtherCardsFragment() - AppScreen.Wallet -> { - store.inject(getDependency = DaggerGraphState::walletRouter).getEntryFragment() - } - AppScreen.Send -> { - val featureToggles = store.inject(getDependency = DaggerGraphState::sendFeatureToggles) - - if (featureToggles.isRedesignedSendEnabled) { - store.inject(getDependency = DaggerGraphState::sendRouter).getEntryFragment() - } else { - SendFragment() - } - } - AppScreen.Details -> { - val featureToggles = store.inject(getDependency = DaggerGraphState::detailsFeatureToggles) - - if (featureToggles.isRedesignEnabled) { - store.inject(DaggerGraphState::detailsEntryPoint).entryFragment() - } else { - DetailsFragment() - } - } - AppScreen.DetailsSecurity -> SecurityModeFragment() - AppScreen.CardSettings -> CardSettingsFragment() - AppScreen.AppSettings -> AppSettingsFragment() - AppScreen.ResetToFactory -> ResetCardFragment() - AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment() - AppScreen.Disclaimer -> DisclaimerFragment() - AppScreen.ManageTokens -> TokensListFragment() - AppScreen.AddCustomToken -> AddCustomTokenFragment() - AppScreen.WalletDetails -> { - store.inject(getDependency = DaggerGraphState::tokenDetailsRouter).getEntryFragment() - } - AppScreen.WalletConnectSessions -> WalletConnectFragment() - AppScreen.QrScanning -> { - store.inject(getDependency = DaggerGraphState::qrScanningRouter).getEntryFragment() - } - AppScreen.ReferralProgram -> ReferralFragment() - AppScreen.Swap -> SwapFragment() - AppScreen.Welcome -> WelcomeFragment() - AppScreen.SaveWallet -> SaveWalletBottomSheetFragment() - AppScreen.AppCurrencySelector -> AppCurrencySelectorFragment() - AppScreen.ModalNotification -> ModalNotificationBottomSheetFragment() - } + transaction.commitAllowingStateLoss() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index d16d3488e2..27fc3c3bc3 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -1,8 +1,8 @@ package com.tangem.tap.common.extensions -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.StateDialog +import com.tangem.common.routing.AppRouter import com.tangem.domain.common.extensions.withMainContext +import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction @@ -82,12 +82,16 @@ suspend fun dispatchOnMain(vararg actions: Action) { withMainContext { actions.forEach { store.dispatch(it) } } } -fun Store<*>.dispatchOpenUrl(url: String) { - dispatch(NavigationAction.OpenUrl(url)) +fun Store.dispatchOpenUrl(url: String) { + inject(DaggerGraphState::urlOpener).openUrl(url) } -fun Store<*>.dispatchShare(url: String) { - dispatch(NavigationAction.Share(url)) +fun Store.dispatchShare(url: String) { + inject(DaggerGraphState::shareManager).shareText(url) +} + +fun Store.dispatchNavigationAction(action: AppRouter.() -> Unit) { + inject(DaggerGraphState::appRouter).action() } inline fun Store.inject(getDependency: DaggerGraphState.() -> T?): T { diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index 524edaded8..b14e9e4090 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -3,7 +3,10 @@ package com.tangem.tap.common.extensions import android.app.Activity -import android.content.* +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.ContextWrapper import android.graphics.drawable.Drawable import android.view.View import androidx.annotation.* @@ -78,16 +81,6 @@ fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? { return clipData.getItemAt(0).text } -fun Context.shareText(text: String) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - startActivity(shareIntent) -} - fun View.getString(resId: Int, vararg formatArgs: Any?): String { return context.getString(resId, *formatArgs) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt similarity index 77% rename from app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt rename to app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt index 98aa013e84..9f2f6bb335 100644 --- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt +++ b/app/src/main/java/com/tangem/tap/common/feedback/LegacyFeedbackManager.kt @@ -7,13 +7,14 @@ import com.tangem.domain.common.TapWorkarounds import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.chat.ChatManager import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.sendEmail import com.tangem.tap.common.log.TangemLogCollector import com.tangem.tap.foregroundActivityObserver -import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.withForegroundActivity import kotlinx.coroutines.launch @@ -25,7 +26,7 @@ import java.io.StringWriter /** [REDACTED_AUTHOR] */ -class FeedbackManager( +class LegacyFeedbackManager( val infoHolder: AdditionalFeedbackInfo, private val logCollector: TangemLogCollector, private val chatManager: ChatManager, @@ -36,16 +37,24 @@ class FeedbackManager( private var sessionFeedbackFile: File? = null private var sessionLogsFile: File? = null - fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) { + fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) { if (feedbackManagerFeatureToggles.isLocalLogsEnabled) { - mainScope.launch { + scope.launch { + val getCardInfo = suspend { + scanResponse ?: error("ScanResponse must be not null") + store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull() + ?: error("CardInfo must be not null") + } + val email = getFeedbackEmailUseCase( - when (feedbackData) { - is FeedbackEmail -> FeedbackEmailType.DirectUserRequest - is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter + type = when (feedbackData) { + is FeedbackEmail -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo()) + is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter(cardInfo = getCardInfo()) is ScanFailsEmail -> FeedbackEmailType.ScanningProblem - is SendTransactionFailedEmail -> FeedbackEmailType.TransactionSendingProblem - else -> FeedbackEmailType.DirectUserRequest + is SendTransactionFailedEmail -> { + FeedbackEmailType.TransactionSendingProblem(cardInfo = getCardInfo()) + } + else -> FeedbackEmailType.DirectUserRequest(cardInfo = getCardInfo()) }, ) @@ -66,12 +75,28 @@ class FeedbackManager( subject = activity.getString(feedbackData.subjectResId), message = feedbackData.joinTogether(activity, infoHolder), file = getLogFile(activity), - onFail = onFail, ) } } } + fun sendEmail(type: FeedbackEmailType) { + if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) error("LOCAL_LOGS feature toggle must be enabled") + + scope.launch { + val email = getFeedbackEmailUseCase(type = type) + + store.inject(DaggerGraphState::emailSender).send( + email = EmailSender.Email( + address = email.address, + subject = email.subject, + message = email.message, + attachment = email.file, + ), + ) + } + } + fun openChat(config: ChatConfig, feedbackData: FeedbackData) { chatManager.open( config = config, @@ -144,7 +169,7 @@ class FeedbackManager( } } - companion object { + private companion object { const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com" const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com" const val FEEDBACK_FILE = "feedback.txt" diff --git a/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt new file mode 100644 index 0000000000..d013937813 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/ProxyFeedbackManager.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.common.feedback + +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.tap.store +import timber.log.Timber + +internal class ProxyFeedbackManager : FeedbackManager { + + override fun sendEmail(type: FeedbackEmailType) { + val manager = store.state.globalState.feedbackManager + + if (manager == null) { + Timber.e("Feedback manager is not initialized") + return + } + + manager.sendEmail(type) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt new file mode 100644 index 0000000000..dc15eb3e95 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.common.finisher + +import android.content.Context +import android.content.Intent +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.tap.MainActivity +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity + +internal class AndroidAppFinisher( + private val appContext: Context, +) : AppFinisher { + + override fun finish() { + foregroundActivityObserver.withForegroundActivity { activity -> + activity.finish() + } + } + + override fun restart() { + val intent = Intent(appContext, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + appContext.startActivity(intent) + } +} \ 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 new file mode 100644 index 0000000000..4a207104ca --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -0,0 +1,103 @@ +package com.tangem.tap.common.pushes + +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import coil.executeBlocking +import coil.request.ImageRequest +import com.google.firebase.messaging.FirebaseMessagingService +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.wallet.R +import timber.log.Timber + +@SuppressLint("MissingFirebaseInstanceTokenRefresh") +internal class TangemPushNotificationService : FirebaseMessagingService() { + + override fun onNewToken(token: String) { + super.onNewToken(token) + Timber.d("New FCM token received: $token") + } + + override fun onMessageReceived(message: RemoteMessage) { + super.onMessageReceived(message) + + val notification = message.notification ?: return + val channelId = notification.channelId ?: TANGEM_CHANNEL_ID + + // TODO refactoring: [REDACTED_JIRA] + val intent = Intent(applicationContext, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + val pendingIntent = PendingIntent.getActivity( + /* context = */ this, + /* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE, + /* intent = */ intent, + /* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE, + ) + + val notificationBuilder = + NotificationCompat.Builder(applicationContext, channelId) + .setSmallIcon(R.drawable.ic_tangem_24) + .setContentTitle(notification.title) + .setContentText(notification.body) + .setPriority(message.priority) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setVibrate(notification.vibrateTimings) + .apply { + notification.imageUrl?.let { uri -> + val bitmap = getBitmapImageFromUrl(uri) + setStyle( + NotificationCompat + .BigPictureStyle() + .bigPicture(bitmap), + ).setLargeIcon(bitmap) + } + } + + val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val notificationChannel = NotificationChannel( + channelId, + ContextCompat.getString(applicationContext, R.string.tangem_app_name), + NotificationManager.IMPORTANCE_HIGH, + ) + notificationManager.createNotificationChannel(notificationChannel) + } + + // Generating unique notification id + val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt() + + notificationManager.notify( + /* id = */ uniqueId, + /* notification = */ notificationBuilder.build(), + ) + } + + private fun getBitmapImageFromUrl(url: Uri): Bitmap? { + return createCoilImageLoader( + applicationContext, + logEnabled = LogConfig.imageLoader, + ).executeBlocking( + ImageRequest.Builder(applicationContext) + .data(url) + .build(), + ).drawable?.toBitmap() + } + + private companion object { + const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications + const val PUSH_NOTIFICATION_REQUEST_CODE = 123 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt index fc362509c3..b896303e37 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.redux import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.TestAction import com.tangem.tap.domain.model.Currency import com.tangem.tap.domain.model.WalletAddressData diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 7adedbed8f..d047620b5e 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -1,7 +1,6 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.globalReducer -import com.tangem.tap.common.redux.navigation.NavigationReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer @@ -23,7 +22,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder) if (action is AppAction.RestoreState) return action.state return AppState( - navigationState = NavigationReducer.reduce(action, state), globalState = globalReducer(action, state, appStateHolder), homeState = HomeReducer.reduce(action, state), onboardingNoteState = OnboardingNoteReducer.reduce(action, state), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 59b899ee8b..2a83cbf4df 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -1,13 +1,11 @@ package com.tangem.tap.common.redux -import com.tangem.core.navigation.NavigationState import com.tangem.domain.redux.DomainState import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.global.NetworkServices import com.tangem.tap.common.redux.global.GlobalMiddleware import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.legacy.LegacyMiddleware -import com.tangem.tap.common.redux.navigation.navigationMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware @@ -40,7 +38,6 @@ import org.rekotlin.Middleware import org.rekotlin.StateType data class AppState( - val navigationState: NavigationState = NavigationState(), val globalState: GlobalState = GlobalState(), val homeState: HomeState = HomeState(), val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(), @@ -73,7 +70,6 @@ data class AppState( fun getMiddleware(): List> { return listOf( logMiddleware, - navigationMiddleware, notificationsMiddleware, GlobalMiddleware.handler, HomeMiddleware.handler, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 8439279a0a..ba7a0ac095 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -2,14 +2,14 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.models.ChatConfig import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.feedback.FeedbackData -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.common.redux.DebugErrorAction import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction @@ -79,9 +79,9 @@ sealed class GlobalAction : Action { data class SetConfigManager(val configManager: ConfigManager) : GlobalAction() data class SetWarningManager(val warningManager: WarningMessagesManager) : GlobalAction() - data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction() + data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction() - data class SendEmail(val feedbackData: FeedbackData) : GlobalAction() + data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction() data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction() object ExchangeManager : GlobalAction() { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index fc584f6b8e..957b756803 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -4,12 +4,12 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.models.Config import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.LogConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.send.redux.SendAction @@ -69,7 +69,10 @@ private fun handleAction(action: Action, appState: () -> AppState?) { } } is GlobalAction.SendEmail -> { - store.state.globalState.feedbackManager?.sendEmail(action.feedbackData) + store.state.globalState.feedbackManager?.sendEmail( + feedbackData = action.feedbackData, + scanResponse = action.scanResponse, + ) } is GlobalAction.OpenChat -> { val globalState = store.state.globalState diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 7a75d16d87..50f8f85f47 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,11 +1,11 @@ package com.tangem.tap.common.redux.global -import com.tangem.core.navigation.StateDialog import com.tangem.datasource.config.ConfigManager import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.feedback.FeedbackManager +import com.tangem.domain.redux.StateDialog +import com.tangem.tap.common.feedback.LegacyFeedbackManager import com.tangem.tap.domain.TapWalletManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.onboarding.OnboardingManager @@ -20,7 +20,7 @@ data class GlobalState( val tapWalletManager: TapWalletManager = TapWalletManager(), val configManager: ConfigManager? = null, val warningManager: WarningMessagesManager? = null, - val feedbackManager: FeedbackManager? = null, + val feedbackManager: LegacyFeedbackManager? = null, val appCurrency: AppCurrency = AppCurrency.Default, val scanCardFailsCounter: Int = 0, val dialog: StateDialog? = null, diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 7cf0e08bf1..0b0af1a01b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -1,8 +1,11 @@ package com.tangem.tap.common.redux.legacy +import com.tangem.blockchain.common.AmountType +import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.redux.LegacyAction import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.feedback.RateCanBeBetterEmail import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.AppState @@ -19,7 +22,10 @@ internal object LegacyMiddleware { { action -> when (action) { is LegacyAction.SendEmailRateCanBeBetter -> { - store.state.globalState.feedbackManager?.sendEmail(RateCanBeBetterEmail()) + store.state.globalState.feedbackManager?.sendEmail( + feedbackData = RateCanBeBetterEmail(), + scanResponse = action.scanResponse, + ) } is LegacyAction.StartOnboardingProcess -> { store.dispatch( @@ -28,8 +34,28 @@ internal object LegacyMiddleware { } is LegacyAction.SendEmailTransactionFailed -> { if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) { + + val amount = action.amount?.convertToAmount(action.cryptoCurrency) + store.inject(DaggerGraphState::saveBlockchainErrorUseCase).invoke( + error = BlockchainErrorInfo( + errorMessage = action.errorMessage, + blockchainId = action.cryptoCurrency.network.id.value, + derivationPath = action.cryptoCurrency.network.derivationPath.value, + destinationAddress = action.destinationAddress.orEmpty(), + tokenSymbol = if (amount?.type is AmountType.Token) { + amount.currencySymbol + } else { + "" + }, + amount = amount?.value?.stripZeroPlainString() ?: "unknown", + fee = action.fee?.convertToAmount(action.cryptoCurrency) + ?.value?.stripZeroPlainString() ?: "unknown", + ), + ) + store.state.globalState.feedbackManager?.sendEmail( - SendTransactionFailedEmail(action.errorMessage), + feedbackData = SendTransactionFailedEmail(action.errorMessage), + scanResponse = action.scanResponse, ) } else { scope.launch { @@ -46,7 +72,8 @@ internal object LegacyMiddleware { ) } store.state.globalState.feedbackManager?.sendEmail( - SendTransactionFailedEmail(action.errorMessage), + feedbackData = SendTransactionFailedEmail(action.errorMessage), + scanResponse = null, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt deleted file mode 100644 index ad2b8f3178..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ /dev/null @@ -1,91 +0,0 @@ -package com.tangem.tap.common.redux.navigation - -import android.content.Intent -import android.hardware.biometrics.BiometricManager -import android.os.Build -import android.provider.Settings -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.tap.activityResultCaller -import com.tangem.tap.common.CustomTabsManager -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.store -import org.rekotlin.Middleware - -val navigationMiddleware: Middleware = { _, state -> - { next -> - { action -> - if (action is NavigationAction) { - val navState = state()?.navigationState - when (action) { - is NavigationAction.NavigateTo -> { - navState?.activity?.get()?.openFragment( - screen = action.screen, - addToBackstack = action.addToBackstack, - fgShareTransition = action.fragmentShareTransition, - bundle = action.bundle, - ) - } - is NavigationAction.PopBackTo -> { - if (navState?.backStack?.lastOrNull() != action.screen) { - when (val screen = action.screen) { - AppScreen.Home, - AppScreen.Welcome, - -> { - if (navState?.backStack?.contains(screen) == false) { - // Pop back to activity - navState.activity?.get()?.popBackTo(screen = null, inclusive = true) - store.dispatchOnMain(NavigationAction.NavigateTo(screen)) - } else { - navState?.activity?.get()?.popBackTo(screen, action.inclusive) - } - } - else -> { - navState?.activity?.get()?.popBackTo(screen, action.inclusive) - } - } - } - } - is NavigationAction.OpenUrl -> { - navState?.activity?.get()?.let { - CustomTabsManager().openUrl(action.url, it) - } - } - is NavigationAction.OpenDocument -> { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = action.url - navState?.activity?.get()?.startActivity(intent) - } - is NavigationAction.OpenDialog -> store.dispatchDialogShow(action.stateDialog) - is NavigationAction.OpenBiometricsSettings -> { - val settingsAction = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> { - Settings.ACTION_BIOMETRIC_ENROLL - } - else -> { - Settings.ACTION_SECURITY_SETTINGS - } - } - val intent = Intent(settingsAction).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - putExtra( - Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, - BiometricManager.Authenticators.BIOMETRIC_STRONG, - ) - } - } - activityResultCaller.activityResultLauncher?.launch(intent) - } - is NavigationAction.Share -> { - navState?.activity?.get()?.shareText(action.data) - } - is NavigationAction.ActivityCreated, - is NavigationAction.ActivityDestroyed, - -> Unit - } - } - next(action) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt deleted file mode 100644 index ff53145651..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationReducer.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.common.redux.navigation - -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.NavigationState -import com.tangem.tap.common.extensions.getPreviousScreen -import com.tangem.tap.common.redux.AppState -import org.rekotlin.Action - -object NavigationReducer { - fun reduce(action: Action, state: AppState): NavigationState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, state: AppState): NavigationState { - val navigationAction = action as? NavigationAction ?: return state.navigationState - val navState = state.navigationState - - return when (navigationAction) { - is NavigationAction.NavigateTo -> { - navState.copy(backStack = navState.backStack + navigationAction.screen) - } - is NavigationAction.PopBackTo -> { - if (navState.backStack.lastOrNull() == navigationAction.screen) return navState - - val screen = navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen() - val index = navState.backStack.lastIndexOf(screen) + 1 - state.navigationState.copy(backStack = navState.backStack.subList(0, index)) - } - is NavigationAction.ActivityCreated -> navState.copy(activity = navigationAction.activity) - is NavigationAction.ActivityDestroyed -> { - when { - // Destroy the activity if it invoked for the same activity. Prevents overwriting to null if there is a - // new scan from the background [REDACTED_TASK_KEY] - navState.activity?.get() == navigationAction.activity.get() -> navState.copy(activity = null) - else -> navState - } - } - else -> navState - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt new file mode 100644 index 0000000000..09305fd59e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.settings + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import com.tangem.core.navigation.settings.SettingsManager + +internal class IntentSettingsManager(val context: Context) : SettingsManager { + override fun openSettings() { + val openSettingsIntent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null), + ) + context.startActivity(openSettingsIntent) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt b/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt new file mode 100644 index 0000000000..51b829de5f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/share/IntentShareManager.kt @@ -0,0 +1,22 @@ +package com.tangem.tap.common.share + +import android.content.Intent +import com.tangem.core.navigation.share.ShareManager +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity + +internal class IntentShareManager : ShareManager { + + override fun shareText(text: String) { + foregroundActivityObserver.withForegroundActivity { activity -> + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + + activity.startActivity(shareIntent) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt index 64763a6216..5b0ac2e000 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/RussianCardholdersWarningBottomSheetDialog.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.LayoutInflater import com.google.android.material.bottomsheet.BottomSheetDialog import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.NavigationAction import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.dispatchDialogHide import com.tangem.tap.common.extensions.dispatchOpenUrl @@ -44,7 +43,7 @@ class RussianCardholdersWarningBottomSheetDialog( dismiss() } binding?.btnNo?.setOnClickListener { - store.dispatch(NavigationAction.OpenUrl(INSTRUCTION_URL)) + store.dispatchOpenUrl(INSTRUCTION_URL) dismiss() } } diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt index d59f668687..410f81e1c6 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt @@ -1,37 +1,76 @@ package com.tangem.tap.common.ui import android.content.Context +import android.view.View +import android.widget.TextView import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import androidx.core.view.isVisible import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog +import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics import com.tangem.tap.common.extensions.dispatchDialogHide +import com.tangem.tap.common.extensions.dispatchOpenUrl import com.tangem.tap.common.feedback.ScanFailsEmail import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.home.LocaleRegionProvider +import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.store import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -object ScanFailsDialog { - fun create(context: Context, source: StateDialog.ScanFailsSource): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.common_warning)) - setMessage(R.string.alert_troubleshooting_scan_card_title) - setPositiveButton(R.string.alert_button_request_support) { _, _ -> - val sourceAnalytics = when (source) { - StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main - StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn - StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings - StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro - } - Analytics.send(Basic.ButtonSupport(sourceAnalytics)) - store.dispatch(GlobalAction.SendEmail(ScanFailsEmail())) +internal object ScanFailsDialog { + + private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/" + private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/" + + fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog { + return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply { + val customView = View.inflate(context, R.layout.dialog_scan_fails, null) + val sourceAnalytics = when (source) { + StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main + StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn + StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings + StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro } - setNeutralButton(R.string.common_cancel) { _, _ -> } + val tryAgainBtn: TextView? = customView.findViewById(R.id.try_again_button) + if (onTryAgain != null) { + tryAgainBtn?.isVisible = true + tryAgainBtn?.setOnClickListener { + store.dispatchDialogHide() + Analytics.send( + ScanFailsDialogAnalytics( + button = ScanFailsDialogAnalytics.Buttons.TRY_AGAIN, + source = sourceAnalytics, + ), + ) + onTryAgain() + } + } else { + tryAgainBtn?.isVisible = false + } + customView.findViewById(R.id.how_to_scan_button)?.setOnClickListener { + Analytics.send( + ScanFailsDialogAnalytics( + button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN, + source = sourceAnalytics, + ), + ) + val locale = LocaleRegionProvider().getRegion() + val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK + store.dispatchOpenUrl(link) + } + customView.findViewById(R.id.request_support_button)?.setOnClickListener { + Analytics.send(Basic.ButtonSupport(sourceAnalytics)) + store.dispatch(GlobalAction.SendEmail(feedbackData = ScanFailsEmail(), scanResponse = null)) + } + customView.findViewById(R.id.cancel_button)?.setOnClickListener { + store.dispatchDialogHide() + } + setView(customView) setOnDismissListener { store.dispatchDialogHide() } }.create() } diff --git a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt similarity index 74% rename from app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt rename to app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index c56c238e57..549f514b01 100644 --- a/app/src/main/java/com/tangem/tap/common/CustomTabsManager.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.common +package com.tangem.tap.common.url import android.content.Context import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -8,12 +8,22 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT +import com.tangem.core.navigation.url.UrlOpener import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.getColorCompat +import com.tangem.tap.foregroundActivityObserver +import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R -class CustomTabsManager { - fun openUrl(url: String, context: Context) { +internal class CustomTabsUrlOpener : UrlOpener { + + override fun openUrl(url: String) { + foregroundActivityObserver.withForegroundActivity { + openUrl(url, context = it) + } + } + + private fun openUrl(url: String, context: Context) { if (url.isEmpty()) return val customTabsIntent = CustomTabsIntent.Builder() .setDefaultColorSchemeParams( diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index bd50b28faf..58d13616e4 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull // FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented @@ -15,6 +16,9 @@ internal class RuntimeUserWalletsStore( override val selectedUserWalletOrNull: UserWallet? get() = userWalletsListManager.selectedUserWalletSync + override val userWallets: Flow> + get() = userWalletsListManager.userWallets + override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? { return userWalletsListManager .userWallets diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index cbf8eebb27..62f28c043e 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -6,10 +6,8 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.tap.MainActivity -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository +import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.DefaultRampManager import com.tangem.tap.proxy.AppStateHolder import dagger.Module @@ -67,10 +65,4 @@ internal object ActivityModule { ): GetPolkadotCheckHasImmortalUseCase { return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository) } - - @Provides - @Singleton - fun provideActivityClassWrapper(): ActivityClassWrapper { - return ActivityClassWrapper(MainActivity::class.java) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt index c6e0267c6b..267fa733f1 100644 --- a/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt +++ b/app/src/main/java/com/tangem/tap/di/AppStateHolderModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.di -import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.redux.ReduxStateHolder import com.tangem.tap.proxy.AppStateHolder import dagger.Binds @@ -13,10 +12,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal interface AppStateHolderModule { - @Binds - @Singleton - fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController - @Binds @Singleton fun bindsReduxStateHolder(appStateHolder: AppStateHolder): ReduxStateHolder diff --git a/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt index b23a41b8a8..1128b32708 100644 --- a/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt +++ b/app/src/main/java/com/tangem/tap/di/RootAppComponentContextModule.kt @@ -7,8 +7,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.DefaultAppComponentContext import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.di.RootAppComponentContext -import com.tangem.core.decompose.ui.UiMessage -import com.tangem.core.decompose.ui.UiMessageHandler +import com.tangem.core.ui.UiDependencies import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -16,7 +15,6 @@ import dagger.hilt.InstallIn import dagger.hilt.android.components.ActivityComponent import dagger.hilt.android.qualifiers.ActivityContext import dagger.hilt.android.scopes.ActivityScoped -import timber.log.Timber @Module @InstallIn(ActivityComponent::class) @@ -29,17 +27,11 @@ internal object RootAppComponentContextModule { @ActivityContext context: Context, dispatchers: CoroutineDispatcherProvider, componentBuilder: DecomposeComponent.Builder, + uiDependencies: UiDependencies, ): AppComponentContext { - // TODO: Implement message handler - val dummyMessageHandler = object : UiMessageHandler { - override fun handleMessage(message: UiMessage) { - Timber.w("Unable to handle message: $message") - } - } - return DefaultAppComponentContext( componentContext = (context as AppCompatActivity).defaultComponentContext(), - messageHandler = dummyMessageHandler, + messageHandler = uiDependencies.eventMessageHandler, dispatchers = dispatchers, hiltComponentBuilder = componentBuilder, ) diff --git a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt index 97b0f48bee..22a522e553 100644 --- a/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UiDependenciesModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di +import androidx.compose.material3.SnackbarHostState import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.theme.AppThemeModeHolder import dagger.Module import dagger.Provides @@ -19,6 +21,8 @@ internal object UiDependenciesModule { return object : UiDependencies { override val hapticManager = hapticManager override val appThemeModeHolder = appThemeModeHolder + override val globalSnackbarHostState: SnackbarHostState = SnackbarHostState() + override val eventMessageHandler: EventMessageHandler = EventMessageHandler() } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt new file mode 100644 index 0000000000..9c29c8598b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -0,0 +1,44 @@ +package com.tangem.tap.di + +import android.content.Context +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.tap.common.feedback.ProxyFeedbackManager +import com.tangem.tap.common.finisher.AndroidAppFinisher +import com.tangem.tap.common.settings.IntentSettingsManager +import com.tangem.tap.common.share.IntentShareManager +import com.tangem.tap.common.url.CustomTabsUrlOpener +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 UtilsModule { + + @Provides + @Singleton + fun provideShareManager(): ShareManager = IntentShareManager() + + @Provides + @Singleton + fun provideUrlOpener(): UrlOpener = CustomTabsUrlOpener() + + @Provides + @Singleton + fun provideFeedbackManager(): FeedbackManager = ProxyFeedbackManager() + + @Provides + @Singleton + fun provideAppFinisher(@ApplicationContext context: Context): AppFinisher = AndroidAppFinisher(context) + + @Provides + @Singleton + fun provideSettingsManager(@ApplicationContext context: Context): SettingsManager = IntentSettingsManager(context) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt index 033e322536..04f4f03fb8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/FeedbackDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import android.content.Context +import com.tangem.domain.feedback.GetCardInfoUseCase import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.repository.FeedbackRepository @@ -15,6 +16,12 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object FeedbackDomainModule { + @Provides + @Singleton + fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetCardInfoUseCase { + return GetCardInfoUseCase(feedbackRepository = feedbackRepository) + } + @Provides @Singleton fun provideGetFeedbackEmailUseCase( diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index bac2d55655..90cc3380f0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -7,6 +7,7 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository +import com.tangem.domain.settings.repositories.PermissionRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.tap.domain.sdk.TangemSdkManager @@ -17,6 +18,7 @@ import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import javax.inject.Singleton +@Suppress("TooManyFunctions") @Module @InstallIn(SingletonComponent::class) internal object SettingsDomainModule { @@ -164,4 +166,52 @@ internal object SettingsDomainModule { ): IncrementAppLaunchCounterUseCase { return IncrementAppLaunchCounterUseCase(settingsRepository = settingsRepository) } + + // region PushPermissionRepository + @Provides + @Singleton + fun provideShouldInitiallyAskPermissionUseCase( + permissionRepository: PermissionRepository, + ): ShouldInitiallyAskPermissionUseCase { + return ShouldInitiallyAskPermissionUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideIsFirstTimeAskingPermissionUseCase( + permissionRepository: PermissionRepository, + ): IsFirstTimeAskingPermissionUseCase { + return IsFirstTimeAskingPermissionUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideSetFirstTimeAskingPushPermissionUseCase( + permissionRepository: PermissionRepository, + ): SetFirstTimeAskingPermissionUseCase { + return SetFirstTimeAskingPermissionUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideDelayPermissionRequestUseCase( + permissionRepository: PermissionRepository, + ): DelayPermissionRequestUseCase { + return DelayPermissionRequestUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideShouldAskPermissionUseCase(permissionRepository: PermissionRepository): ShouldAskPermissionUseCase { + return ShouldAskPermissionUseCase(repository = permissionRepository) + } + + @Provides + @Singleton + fun provideNeverRequestPermissionUseCase( + permissionRepository: PermissionRepository, + ): NeverRequestPermissionUseCase { + return NeverRequestPermissionUseCase(repository = permissionRepository) + } + // endregion } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index 54bfe9d2b7..12aad7794a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,8 +1,6 @@ package com.tangem.tap.di.domain -import com.tangem.domain.settings.* -import com.tangem.domain.staking.GetStakingAvailabilityUseCase -import com.tangem.domain.staking.GetStakingEntryInfoUseCase +import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.StakingRepository import dagger.Module import dagger.Provides @@ -14,6 +12,14 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object StakingDomainModule { + @Provides + @Singleton + fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase { + return GetStakingAvailabilityUseCase( + stakingRepository = stakingRepository, + ) + } + @Provides @Singleton fun provideGetStakingEntryInfoUseCase(stakingRepository: StakingRepository): GetStakingEntryInfoUseCase { @@ -24,9 +30,35 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase { - return GetStakingAvailabilityUseCase( + fun provideGetYieldUseCase(stakingRepository: StakingRepository): GetYieldUseCase { + return GetYieldUseCase( stakingRepository = stakingRepository, ) } + + @Provides + @Singleton + fun provideGetStakingTokensUseCase(stakingRepository: StakingRepository): FetchStakingTokensUseCase { + return FetchStakingTokensUseCase( + stakingRepository = stakingRepository, + ) + } + + @Provides + @Singleton + fun provideFetchStakingYieldBalanceUseCase(stakingRepository: StakingRepository): FetchStakingYieldBalanceUseCase { + return FetchStakingYieldBalanceUseCase(stakingRepository = stakingRepository) + } + + @Provides + @Singleton + fun provideGetStakingYieldBalanceUseCase(stakingRepository: StakingRepository): GetStakingYieldBalanceUseCase { + return GetStakingYieldBalanceUseCase(stakingRepository = stakingRepository) + } + + @Provides + @Singleton + fun provideCreateEnterActionUseCase(stakingRepository: StakingRepository): InitializeStakingProcessUseCase { + return InitializeStakingProcessUseCase(stakingRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 5990835735..6e2c244f20 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -54,8 +55,14 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetTokenListUseCase { - return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository) + return GetTokenListUseCase( + currenciesRepository, + quotesRepository, + networksRepository, + stakingRepository, + ) } @Provides @@ -64,8 +71,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetCardTokensListUseCase { - return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository) + return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository) } @Provides @@ -83,9 +91,16 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCurrencyStatusUpdatesUseCase { - return GetCurrencyStatusUpdatesUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers) + return GetCurrencyStatusUpdatesUseCase( + currenciesRepository, + quotesRepository, + networksRepository, + stakingRepository, + dispatchers, + ) } @Provides @@ -100,6 +115,7 @@ internal object TokensDomainModule { currencyChecksRepository: CurrencyChecksRepository, showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, promoRepository: PromoRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCurrencyWarningsUseCase { return GetCurrencyWarningsUseCase( @@ -112,6 +128,7 @@ internal object TokensDomainModule { swapRepository = swapRepository, showSwapPromoTokenUseCase = showSwapPromoTokenUseCase, promoRepository = promoRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -122,12 +139,14 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetPrimaryCurrencyStatusUpdatesUseCase { return GetPrimaryCurrencyStatusUpdatesUseCase( currenciesRepository, quotesRepository, networksRepository, + stakingRepository, dispatchers, ) } @@ -148,8 +167,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): FetchCardTokenListUseCase { - return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository) + return FetchCardTokenListUseCase(currenciesRepository, networksRepository, quotesRepository, stakingRepository) } @Provides @@ -190,6 +210,7 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -199,6 +220,7 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -209,12 +231,14 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetNetworkCoinStatusUseCase { return GetNetworkCoinStatusUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -225,12 +249,14 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetFeePaidCryptoCurrencyStatusSyncUseCase { return GetFeePaidCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -363,11 +389,13 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetWalletTotalBalanceUseCase { return GetWalletTotalBalanceUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 9635e67a29..8ce98f292d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -64,6 +64,20 @@ internal object TransactionDomainModule { return CreateTransactionUseCase(transactionRepository) } + @Provides + @Singleton + fun provideCreateTransactionExtrasUseCase( + transactionRepository: TransactionRepository, + ): CreateTransactionDataExtrasUseCase { + return CreateTransactionDataExtrasUseCase(transactionRepository) + } + + @Provides + @Singleton + fun provideEstimateFeeUseCase(walletManagersFacade: WalletManagersFacade): EstimateFeeUseCase { + return EstimateFeeUseCase(walletManagersFacade) + } + @Provides @Singleton fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase { diff --git a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt new file mode 100644 index 0000000000..4f4aa7e79b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.di.routing + +import com.tangem.common.routing.AppRouter +import com.tangem.tap.routing.ProxyAppRouter +import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.tap.routing.configurator.MutableAppRouterConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AppRouterModule { + + @Provides + @Singleton + fun provideAppRouter(config: AppRouterConfig, dispatchers: CoroutineDispatcherProvider): AppRouter = + ProxyAppRouter(config, dispatchers) + + @Provides + @Singleton + fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt b/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt new file mode 100644 index 0000000000..aca0f1678a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.routing + +import com.tangem.tap.routing.RoutingComponent +import com.tangem.tap.routing.impl.DefaultRoutingComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +internal interface RoutingComponentModule { + + @Binds + @ActivityScoped + fun bindRoutingComponentFactory(factory: DefaultRoutingComponent.Factory): RoutingComponent.Factory +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index b7ad02d630..d7bb96e4c3 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -1,13 +1,17 @@ package com.tangem.tap.domain.card import com.tangem.common.CompletionResult +import com.tangem.common.card.EllipticCurve +import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWallet @@ -45,7 +49,10 @@ internal class DefaultDerivationsRepository( tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations) .doOnSuccess { response -> updatePublicKeys(userWalletId = userWalletId, keys = response.entries).fold( - onSuccess = { return }, + onSuccess = { + validateDerivations(userWallet.scanResponse, derivations) + return + }, onFailure = { throw it }, ) } @@ -76,6 +83,22 @@ internal class DefaultDerivationsRepository( } } + /** + * It throws an exception if any of the provided derivations are invalid + * Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths + * It needs to be called after success [derivePublicKeys] or in same flows + */ + private fun validateDerivations(scanResponse: ScanResponse, derivations: Derivations) { + derivations.entries.forEach { derivationForKey -> + val wallet = scanResponse.card.wallets.firstOrNull { it.publicKey.toMapKey() == derivationForKey.key } + if (wallet == null) return@forEach + val hasHardenedNodes = derivationForKey.value.any { path -> path.nodes.any { node -> !node.isHardened } } + if (wallet.curve == EllipticCurve.Ed25519Slip0010 && hasHardenedNodes) { + throw TangemSdkError.NonHardenedDerivationNotSupported() + } + } + } + private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result { return runCatching(dispatchers.io) { userWalletsStore.update( diff --git a/app/src/main/java/com/tangem/tap/domain/model/PendingTransaction.kt b/app/src/main/java/com/tangem/tap/domain/model/PendingTransaction.kt index c4a810e12e..8548b11eba 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/PendingTransaction.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/PendingTransaction.kt @@ -5,7 +5,7 @@ import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.Wallet data class PendingTransaction( - val transactionData: TransactionData, + val transactionData: TransactionData.Uncompiled, val type: PendingTransactionType, ) { val address: String? = when (type) { @@ -21,7 +21,7 @@ data class PendingTransaction( enum class PendingTransactionType { Incoming, Outgoing, Unknown } -fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransaction? { +fun TransactionData.Uncompiled.toPendingTransaction(walletAddress: String): PendingTransaction? { if (this.status == TransactionStatus.Confirmed) return null val type: PendingTransactionType = when { @@ -32,7 +32,7 @@ fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransact return PendingTransaction(this, type) } -fun List.toPendingTransactions(walletAddress: String): List { +fun List.toPendingTransactions(walletAddress: String): List { return this.mapNotNull { it.toPendingTransaction(walletAddress) } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index ce649ece46..0bbdc3f5f4 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -5,12 +5,11 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics 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.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned @@ -21,6 +20,7 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback +import com.tangem.tap.features.disclaimer.redux.DisclaimerSource import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep @@ -118,7 +118,7 @@ internal object LegacyScanProcessor { disclaimerWillShow() store.dispatchWithMain( DisclaimerAction.Show( - fromScreen = AppScreen.Home, + from = DisclaimerSource.Home, callback = DisclaimerCallback( onAccept = { scope.launch(Dispatchers.Main) { @@ -170,7 +170,7 @@ internal object LegacyScanProcessor { if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) - navigateTo(AppScreen.OnboardingTwins) { onProgressStateChange(it) } + navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } } else { delay(DELAY_SDK_DIALOG_CLOSE) onSuccess(scanResponse) @@ -178,9 +178,9 @@ internal object LegacyScanProcessor { } } - private suspend inline fun navigateTo(screen: AppScreen, onProgressStateChange: (showProgress: Boolean) -> Unit) { + private suspend inline fun navigateTo(route: AppRoute, onProgressStateChange: (showProgress: Boolean) -> Unit) { delay(DELAY_SDK_DIALOG_CLOSE) - store.dispatchOnMain(NavigationAction.NavigateTo(screen)) + store.dispatchNavigationAction { push(route) } onProgressStateChange(false) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index ed777bd666..381d348980 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -3,16 +3,15 @@ package com.tangem.tap.domain.scanCard import arrow.fx.coroutines.resourceScope import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.StateDialog import com.tangem.domain.card.ScanCardException import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.scanCard.chains.* import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter @@ -60,7 +59,11 @@ internal object UseCaseScanProcessor { ), ) add(AnalyticsChain(Basic.CardWasScanned(analyticsSource))) - add(DisclaimerChain(store, disclaimerWillShow)) + val pushNotificationsToggles = + store.inject(getDependency = DaggerGraphState::pushNotificationsFeatureToggles) + if (pushNotificationsToggles.isPushNotificationsEnabled) { + add(DisclaimerChain(store, disclaimerWillShow)) + } add(CheckForOnboardingChain(store, store.state.globalState.tapWalletManager)) } @@ -132,8 +135,8 @@ internal object UseCaseScanProcessor { action() } - private suspend inline fun navigateTo(screen: AppScreen) { + private suspend inline fun navigateTo(route: AppRoute) { delay(DELAY_SDK_DIALOG_CLOSE) - store.dispatchOnMain(NavigationAction.NavigateTo(screen)) + store.dispatchNavigationAction { push(route) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index 66f708e28d..72d0b6ee59 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -2,8 +2,8 @@ package com.tangem.tap.domain.scanCard.chains import arrow.core.left import arrow.core.right +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen import com.tangem.domain.card.ScanCardException import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.util.twinsIsTwinned @@ -55,8 +55,8 @@ class CheckForOnboardingChain( canSkipBackup = previousChainResult.card.canSkipBackup, ), ) - val appScreen = OnboardingHelper.whereToNavigate(previousChainResult) - ScanChainException.OnboardingNeeded(appScreen).left() + val route = OnboardingHelper.whereToNavigate(previousChainResult) + ScanChainException.OnboardingNeeded(route).left() } else -> { Analytics.setContext(previousChainResult) @@ -69,7 +69,7 @@ class CheckForOnboardingChain( store.dispatchOnMain( TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), ) - ScanChainException.OnboardingNeeded(AppScreen.OnboardingTwins).left() + ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() } else { delay(DELAY_SDK_DIALOG_CLOSE) previousChainResult.right() diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt index 35ad2efbab..8aa003b952 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/DisclaimerChain.kt @@ -2,7 +2,6 @@ package com.tangem.tap.domain.scanCard.chains import arrow.core.left import arrow.core.right -import com.tangem.core.navigation.AppScreen import com.tangem.domain.card.ScanCardException import com.tangem.domain.core.chain.Chain import com.tangem.domain.core.chain.ResultChain @@ -13,6 +12,7 @@ import com.tangem.tap.features.disclaimer.Disclaimer import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback +import com.tangem.tap.features.disclaimer.redux.DisclaimerSource import kotlinx.coroutines.suspendCancellableCoroutine import org.rekotlin.Store import kotlin.coroutines.resume @@ -50,7 +50,7 @@ internal class DisclaimerChain( return suspendCancellableCoroutine { continuation -> store.dispatchOnMain( DisclaimerAction.Show( - fromScreen = AppScreen.Home, + from = DisclaimerSource.Home, callback = DisclaimerCallback( onAccept = { if (continuation.isActive) { diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt index ae5e9f8ad3..b36fb89f0e 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/ScanChainException.kt @@ -1,6 +1,6 @@ package com.tangem.tap.domain.scanCard.chains -import com.tangem.core.navigation.AppScreen +import com.tangem.common.routing.AppRoute import com.tangem.domain.card.ScanCardException sealed class ScanChainException : ScanCardException.ChainException() { @@ -19,5 +19,5 @@ sealed class ScanChainException : ScanCardException.ChainException() { * * @param onboardingRoute route where to navigate * */ - data class OnboardingNeeded(val onboardingRoute: AppScreen) : ScanChainException() + data class OnboardingNeeded(val onboardingRoute: AppRoute) : ScanChainException() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index abb7d6ce7e..01fe1e3deb 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -91,9 +91,8 @@ class WalletConnectSdkHelper { val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" } - val transactionData = TransactionData( + val transactionData = TransactionData.Uncompiled( amount = Amount(value, wallet.blockchain), - // TODO refactoring fee = Fee.Common(Amount(fee, wallet.blockchain)), sourceAddress = transaction.from, destinationAddress = destinationAddress, diff --git a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt index d9e668bf04..6321957ee1 100644 --- a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt @@ -14,7 +14,8 @@ import androidx.lifecycle.Lifecycle import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.store import com.tangem.wallet.R @@ -36,7 +37,7 @@ abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackP } override fun handleOnBackPressed() { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt index f2b19a713d..443a8af290 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.customtoken.impl.presentation +import androidx.compose.foundation.background import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -8,7 +9,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen @@ -32,15 +32,12 @@ internal class AddCustomTokenFragment : ComposeFragment() { val viewModel = hiltViewModel().apply { LocalLifecycleOwner.current.lifecycle.addObserver(this) } - val statusBarColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(color = statusBarColor) - } - val state by viewModel.uiState.collectAsStateWithLifecycle() AddCustomTokenScreen( - modifier = Modifier.systemBarsPadding(), + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .systemBarsPadding(), stateHolder = state, ) } diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt index 6391cc840f..858d49c31c 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt @@ -1,9 +1,11 @@ package com.tangem.tap.features.customtoken.impl.presentation.routers import com.tangem.blockchain.common.Blockchain -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.utils.popTo import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.store import com.tangem.wallet.R @@ -12,11 +14,11 @@ import com.tangem.wallet.R internal class DefaultCustomTokenRouter : CustomTokenRouter { override fun popBackStack() { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } override fun openWalletScreen() { - store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } override fun openUnsupportedNetworkAlert(blockchain: Blockchain) { @@ -32,7 +34,7 @@ internal class DefaultCustomTokenRouter : CustomTokenRouter { val alert = AppDialog.SimpleOkDialogRes( headerId = R.string.common_error, messageId = R.string.common_unknown_error, - onOk = { store.dispatch(NavigationAction.PopBackTo()) }, + onOk = { store.dispatchNavigationAction(AppRouter::pop) }, ) store.dispatchDialogShow(alert) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index fa88770744..b48655b7c7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -96,6 +96,8 @@ sealed class DetailsAction : Action { data class ChangeAppCurrency( val currency: AppCurrency, ) : AppSettings() + + data class Prepare(val state: AppSettingsState) : AppSettings() } data class ChangeAppCurrency(val currency: AppCurrency) : DetailsAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index c82a82d087..bde329b131 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -5,9 +5,11 @@ import com.tangem.common.* import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -19,6 +21,7 @@ import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.domain.wallets.legacy.asLockable 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.Settings import com.tangem.tap.common.extensions.* @@ -29,9 +32,6 @@ import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import com.tangem.wallet.R @@ -71,7 +71,8 @@ class DetailsMiddleware { is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action) is DetailsAction.ReCreateTwinsWallet -> { store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins)) + + store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } } is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action) is DetailsAction.ScanCard -> scanCard(state) @@ -91,7 +92,7 @@ class DetailsMiddleware { store.dispatch(DetailsAction.ReCreateTwinsWallet) return } else { - store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory)) + store.dispatchNavigationAction { push(AppRoute.ResetToFactory) } } } is DetailsAction.ResetToFactory.Proceed -> { @@ -132,15 +133,15 @@ class DetailsMiddleware { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync if (selectedUserWallet != null) { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } store.onUserWalletSelected(selectedUserWallet) } else { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } .fold(onSuccess = { true }, onFailure = { false }) if (isLocked && userWalletsListManager.hasUserWallets) { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome)) + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } } else { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { replaceAll(AppRoute.Home) } } } } @@ -164,7 +165,7 @@ class DetailsMiddleware { fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) { when (action) { is DetailsAction.ManageSecurity.OpenSecurity -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity)) + store.dispatchNavigationAction { push(AppRoute.DetailsSecurity) } } is DetailsAction.ManageSecurity.SaveChanges -> { val cardSettingsState = detailsState.cardSettingsState @@ -183,7 +184,7 @@ class DetailsMiddleware { is CompletionResult.Success -> { Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue)) store.dispatch(GlobalAction.UpdateSecurityOptions(selectedOption)) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success) } is CompletionResult.Failure -> { @@ -243,6 +244,7 @@ class DetailsMiddleware { is DetailsAction.AppSettings.SwitchPrivacySetting.Success, is DetailsAction.AppSettings.SwitchPrivacySetting.Failure, is DetailsAction.AppSettings.BiometricsStatusChanged, + is DetailsAction.AppSettings.Prepare, -> Unit } } @@ -272,7 +274,7 @@ class DetailsMiddleware { private fun enrollBiometrics() { Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication) - store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) + activityResultCaller.openSystemBiometrySettings() } private fun changeAppThemeMode(appThemeMode: AppThemeMode) { @@ -393,7 +395,7 @@ class DetailsMiddleware { deleteSavedAccessCodes() store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } return CompletionResult.Success(Unit) } @@ -432,7 +434,7 @@ class DetailsMiddleware { when (action) { is DetailsAction.AccessCodeRecovery.Open -> { Analytics.send(Settings.CardSettings.AccessCodeRecoveryButton()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery)) + store.dispatchNavigationAction { push(AppRoute.AccessCodeRecovery) } } is DetailsAction.AccessCodeRecovery.SaveChanges -> { scope.launch { @@ -444,7 +446,7 @@ class DetailsMiddleware { AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled), ), ) - store.dispatchOnMain(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) store.dispatchOnMain( DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled), ) @@ -500,7 +502,7 @@ class DetailsMiddleware { store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) }, disclaimerWillShow = { - store.dispatchOnMain(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, onSuccess = { scanResponse -> createUserWallet(scanResponse) @@ -544,7 +546,7 @@ class DetailsMiddleware { store.onUserWalletSelected(userWallet) store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } .doOnFailure { error -> if (error is UserWalletsListError.WalletAlreadySaved) { @@ -552,7 +554,7 @@ class DetailsMiddleware { store.onUserWalletSelected(userWallet) store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } else { Timber.e(error, "Unable to create user wallet") handleError(error, prevUseBiometricsForAccessCode) diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 5a915b21d5..837fd35328 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -1,6 +1,6 @@ package com.tangem.tap.features.details.redux -import com.tangem.core.navigation.AppScreen +import com.tangem.common.routing.AppRoute import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver @@ -66,10 +66,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { } private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState { + val router = store.inject(DaggerGraphState::appRouter) + return DetailsState( scanResponse = action.scanResponse, // If the current screen is ResetToFactory, we must save the ResetToFactory's state - cardSettingsState = if (store.state.navigationState.backStack.lastOrNull() == AppScreen.ResetToFactory) { + cardSettingsState = if (router.stack.lastOrNull() is AppRoute.ResetToFactory) { state.detailsState.cardSettingsState } else { null @@ -279,6 +281,9 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail isHidingEnabled = action.hideBalance, ), ) + is DetailsAction.AppSettings.Prepare -> state.copy( + appSettingsState = action.state, + ) is DetailsAction.AppSettings.EnrollBiometrics, is DetailsAction.AppSettings.CheckBiometricsStatus, -> state diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index b4917ceabb..f55288968a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -1,19 +1,17 @@ package com.tangem.tap.features.details.redux.walletconnect -import androidx.core.os.bundleOf import com.tangem.blockchain.common.WalletManager import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.Account import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError @@ -62,14 +60,7 @@ class WalletConnectMiddleware { // TODO check store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri)) } else { - store.dispatchOnMain( - NavigationAction.NavigateTo( - screen = AppScreen.QrScanning, - bundle = bundleOf( - QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT, - ), - ), - ) + store.dispatchNavigationAction { push(AppRoute.QrScanning(SourceType.WALLET_CONNECT)) } } } is WalletConnectAction.ShowClipboardOrScanQrDialog -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 750f4b04c5..65d11fc520 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.core.navigation.StateDialog import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt index d48050ec85..e4170e8003 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorFragment.kt @@ -6,8 +6,7 @@ import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.screen.ComposeFragment import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -22,12 +21,8 @@ internal class AppCurrencySelectorFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + NavigationBar3ButtonsScrim() AppCurrencySelectorScreen( modifier = modifier, state = uiState, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt index b29b503a39..168fccb7e7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -40,7 +41,9 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier val listState = rememberLazyListState() Scaffold( - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = modifier + .imePadding() + .nestedScroll(scrollBehavior.nestedScrollConnection), containerColor = TangemTheme.colors.background.secondary, topBar = { TopBar( @@ -49,6 +52,7 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier state = state, ) }, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), content = { paddingValues -> val contentModifier = Modifier .padding(paddingValues) @@ -62,7 +66,7 @@ internal fun AppCurrencySelectorScreen(state: AppCurrencySelectorState, modifier modifier = contentModifier, listState = listState, currencies = state.items, - selectedId = state.selectedId.orEmpty(), + selectedId = state.selectedId, onCurrencyClick = state.onCurrencyClick, ) } @@ -175,6 +179,7 @@ private fun SearchBar(onInputChange: (String) -> Unit, modifier: Modifier = Modi @Composable private fun LoadingList(modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } Column(modifier = modifier) { repeat(times = 10) { Row( @@ -199,6 +204,7 @@ private fun LoadingList(modifier: Modifier = Modifier) { ) } } + Spacer(Modifier.height(bottomBarHeight)) } } @@ -210,9 +216,11 @@ private fun CurrenciesList( onCurrencyClick: (Currency) -> Unit, modifier: Modifier = Modifier, ) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } LazyColumn( modifier = modifier, state = listState, + contentPadding = PaddingValues(bottom = bottomBarHeight), ) { items( items = currencies, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt index 7b07e7d471..41d27d02a9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appcurrency/AppCurrencySelectorViewModel.kt @@ -2,8 +2,9 @@ package com.tangem.tap.features.details.ui.appcurrency import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.ReduxNavController + import com.tangem.domain.appcurrency.GetAvailableCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.SelectAppCurrencyUseCase @@ -20,7 +21,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getAvailableCurrenciesUseCase: GetAvailableCurrenciesUseCase, private val selectAppCurrencyUseCase: SelectAppCurrencyUseCase, - private val reduxNavController: ReduxNavController, + private val router: AppRouter, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, ) : ViewModel(), AppCurrencySelectorIntents { @@ -34,7 +35,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor( val uiState: StateFlow = stateController.stateFlow override fun onBackClick() { - reduxNavController.popBackStack() + router.pop() } override fun onSearchClick() { @@ -52,7 +53,7 @@ internal class AppCurrencySelectorViewModel @Inject constructor( analyticsEventHandler.send( event = Settings.AppSettings.MainCurrencyChanged(currencyType = currency.name), ) - reduxNavController.popBackStack() + router.pop() } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 2efc912cca..2d6e9d554c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -6,10 +6,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter + import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -36,7 +38,7 @@ internal class AppSettingsFragment : ComposeFragment() { state = state, onBackClick = { store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index c60f34ecce..f3d0b721c9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,13 +1,17 @@ package com.tangem.tap.features.details.ui.appsettings import android.content.res.Configuration +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -32,6 +36,7 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> }, titleRes = R.string.app_settings_title, onBackClick = onBackClick, + addBottomInsets = false, ) } @@ -44,7 +49,11 @@ private fun AppSettings(state: AppSettingsScreenState.Content) { null -> Unit } - LazyColumn { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + LazyColumn( + contentPadding = PaddingValues(bottom = bottomBarHeight), + ) { items( items = state.items, key = Item::id, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 1a473c49ec..34a693dbb4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -1,16 +1,20 @@ package com.tangem.tap.features.details.ui.appsettings -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ViewModel -import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.* +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.apptheme.repository.AppThemeModeRepository +import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository +import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.features.details.DetailsFeatureToggles import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.features.details.redux.AppSetting @@ -26,14 +30,22 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import org.rekotlin.StoreSubscriber import javax.inject.Inject +@Suppress("LongParameterList") @HiltViewModel internal class AppSettingsViewModel @Inject constructor( private val appCurrencyRepository: AppCurrencyRepository, + private val walletsRepository: WalletsRepository, + private val canUseBiometryUseCase: CanUseBiometryUseCase, + private val balanceHidingRepository: BalanceHidingRepository, private val analyticsEventHandler: AnalyticsEventHandler, + private val appThemeModeRepository: AppThemeModeRepository, + private val settingsRepository: SettingsRepository, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, + private val detailsFeatureToggles: DetailsFeatureToggles, ) : ViewModel(), StoreSubscriber, DefaultLifecycleObserver { @@ -50,6 +62,9 @@ internal class AppSettingsViewModel @Inject constructor( init { bootstrapAppCurrencyUpdates() + if (detailsFeatureToggles.isRedesignEnabled) { + bootstrapBiometricsUpdates() + } subscribeToStoreChanges() sendItemsAnalytics() @@ -128,7 +143,7 @@ internal class AppSettingsViewModel @Inject constructor( } private fun showAppCurrencySelector() { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppCurrencySelector)) + store.dispatchNavigationAction { push(AppRoute.AppCurrencySelector) } } private fun showThemeModeSelector(selectedMode: AppThemeMode) { @@ -214,6 +229,19 @@ internal class AppSettingsViewModel @Inject constructor( .saveIn(appCurrencyUpdatesJobHolder) } + private fun bootstrapBiometricsUpdates() = viewModelScope.launch { + val state = AppSettingsState( + saveWallets = walletsRepository.shouldSaveUserWalletsSync(), + saveAccessCodes = settingsRepository.shouldSaveAccessCodes(), + isBiometricsAvailable = canUseBiometryUseCase(), + isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, + selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, + selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, + ) + + store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state)) + } + private fun subscribeToStoreChanges() { store.subscribe(subscriber = this) { state -> state.skipRepeats { oldState, newState -> diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index c81e2d819e..6fc835cc9c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -4,9 +4,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.fragment.app.viewModels -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter + import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -29,7 +31,7 @@ internal class CardSettingsFragment : ComposeFragment() { state = viewModel.screenState.value, onBackClick = { store.dispatch(DetailsAction.ResetCardSettingsData) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt index 7a365126b6..3beea0503a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt @@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -28,7 +29,7 @@ class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber Unit = {}, ) { val state = rememberScaffoldState(snackbarHostState = snackbarHostState) val backgroundColor = TangemTheme.colors.background.secondary BackHandler(onBack = onBackClick) - SystemBarsEffect { - setSystemBarsColor(backgroundColor) - } Scaffold( scaffoldState = state, topBar = { EmptyTopBarWithNavigation( + modifier = Modifier.statusBarsPadding(), onBackClick = onBackClick, backgroundColor = backgroundColor, ) }, - modifier = modifier.systemBarsPadding(), + modifier = modifier, + contentWindowInsets = WindowInsetsZero, backgroundColor = backgroundColor, - floatingActionButton = fab, - content = { paddings -> + floatingActionButton = { + Box(modifier = Modifier.navigationBarsPadding()) { + fab() + } + }, + content = { paddingValues -> Column( modifier = Modifier - .padding(paddings) + .run { + if (addBottomInsets) { + navigationBarsPadding() + } else { + this + } + } + .padding(paddingValues) .fillMaxSize(), ) { if (titleRes != null) { @@ -81,9 +92,11 @@ internal fun ScreenTitle(titleRes: Int, modifier: Modifier = Modifier) { @Composable internal fun EmptyTopBarWithNavigation( onBackClick: () -> Unit, + modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.primary, ) { TopAppBar( + modifier = modifier, title = { }, navigationIcon = { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index a382e52b7f..5b41307402 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -3,13 +3,14 @@ package com.tangem.tap.features.details.ui.details import android.os.Bundle import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -45,7 +46,7 @@ internal class DetailsFragment : ComposeFragment(), StoreSubscriber Unit, ) : SettingsItem( - iconResId = R.drawable.ic_card_settings, + iconResId = R.drawable.ic_card_settings_24, title = resourceReference(R.string.card_settings_title), ) @@ -95,7 +95,7 @@ internal sealed class SettingsItem( data class ReferralProgram( override val onClick: () -> Unit, ) : SettingsItem( - iconResId = R.drawable.ic_add_friends, + iconResId = R.drawable.ic_add_friends_24, title = resourceReference(R.string.details_referral_title), ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt index 95a6865b3c..4cc6783772 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt @@ -3,11 +3,11 @@ package com.tangem.tap.features.details.ui.details import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent @@ -16,15 +16,14 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.common.extensions.addContext -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerSource import com.tangem.tap.features.home.LocaleRegionProvider import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE import com.tangem.tap.scope @@ -128,30 +127,38 @@ internal class DetailsViewModel( } private fun navigateToTesterMenu() { - store.state.daggerGraphState.testerRouter?.startTesterScreen() + store.dispatchNavigationAction { + push(AppRoute.TesterMenu) + } } private fun navigateToToS() { - store.dispatchOnMain(DisclaimerAction.Show(AppScreen.Details)) + store.dispatchOnMain(DisclaimerAction.Show(DisclaimerSource.Details)) } private fun navigateToReferralProgram() { - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) + store.dispatchNavigationAction { push(AppRoute.ReferralProgram) } } private fun sendFeedback() { Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings)) - store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail())) + store.dispatchOnMain( + GlobalAction.SendEmail( + feedbackData = FeedbackEmail(), + scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } private fun navigateToAppSettings() { Analytics.send(Settings.ButtonAppSettings()) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppSettings)) + store.dispatchNavigationAction { push(AppRoute.AppSettings) } } private fun navigateToCardSettings() { Analytics.send(Settings.ButtonCardSettings()) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.CardSettings)) + store.dispatchNavigationAction { push(AppRoute.CardSettings) } } private fun linkMoreCards() { @@ -164,7 +171,7 @@ internal class DetailsViewModel( val scanResponse = selectedUserWallet.scanResponse Analytics.addContext(scanResponse) store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) } } private fun scanAndSaveUserWallet() { @@ -174,12 +181,12 @@ internal class DetailsViewModel( private fun navigateToWalletConnect() { Analytics.send(Settings.ButtonWalletConnect()) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions)) + store.dispatchNavigationAction { push(AppRoute.WalletConnectSessions) } } private fun handleSocialNetworkClick(link: SocialNetworkLink) { Analytics.send(Settings.ButtonSocialNetwork(link.network)) - store.dispatchOnMain(NavigationAction.OpenUrl(link.url)) + store.dispatchOpenUrl(link.url) } private fun getSocialLinks(): ImmutableList { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index 74424cd8ba..a17e32ffd9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -30,7 +31,7 @@ internal class ResetCardFragment : ComposeFragment(), StoreSubscriber() } } else { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess if (isLocked && userWalletsListManager.hasUserWallets) { - store.dispatch(NavigationAction.PopBackTo(AppScreen.Welcome)) + store.dispatchNavigationAction { popTo() } } else { - store.dispatch(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index c44b9ca3fa..f61e54fd78 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store import dagger.hilt.android.AndroidEntryPoint @@ -29,7 +30,7 @@ internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber
- store.dispatch( - NavigationAction.NavigateTo( - screen = AppScreen.QrScanning, - bundle = bundleOf( - QrScanningRouter.SOURCE_KEY to SourceType.WALLET_CONNECT, - ), - ), - ) + store.dispatchNavigationAction { + push(AppRoute.QrScanning(source = SourceType.WALLET_CONNECT)) + } } setOnDismissListener { store.dispatch(GlobalAction.HideDialog) diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt index 52e3057f46..9611dc80b5 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerAction.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.disclaimer.redux -import com.tangem.core.navigation.AppScreen import com.tangem.tap.common.entities.ProgressState import com.tangem.tap.features.disclaimer.Disclaimer import org.rekotlin.Action @@ -10,7 +9,7 @@ sealed class DisclaimerAction : Action { data class SetDisclaimer(val disclaimer: Disclaimer) : DisclaimerAction() data class Show( - val fromScreen: AppScreen, + val from: DisclaimerSource, val callback: DisclaimerCallback? = null, ) : DisclaimerAction() diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt index 3e152187ea..60ffa51781 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerMiddleware.kt @@ -1,7 +1,8 @@ package com.tangem.tap.features.disclaimer.redux -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppState import com.tangem.tap.mainScope import com.tangem.tap.store @@ -25,17 +26,19 @@ private fun handleDisclaimerMiddleware(action: Action, appState: AppState) { when (action) { is DisclaimerAction.Show -> { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) + store.dispatchNavigationAction { + push(AppRoute.Disclaimer(isTosAccepted = action.from == DisclaimerSource.Details)) + } } is DisclaimerAction.AcceptDisclaimer -> { mainScope.launch { state.disclaimer.accept() - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) state.callback?.onAccept?.invoke() } } is DisclaimerAction.OnBackPressed -> { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) state.callback?.onDismiss?.invoke() } } diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt index 6c2205ca01..9b655697ba 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerReducer.kt @@ -17,7 +17,7 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState { disclaimer = action.disclaimer, ) is DisclaimerAction.Show -> disclaimerState.copy( - showedFromScreen = action.fromScreen, + showedFrom = action.from, callback = action.callback, ) is DisclaimerAction.AcceptDisclaimer, is DisclaimerAction.OnBackPressed -> disclaimerState.copy( diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerSource.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerSource.kt new file mode 100644 index 0000000000..cdfcbbdb90 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerSource.kt @@ -0,0 +1,5 @@ +package com.tangem.tap.features.disclaimer.redux + +enum class DisclaimerSource { + Home, Details +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt index 8d37c385eb..c5ccfc7cfc 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/redux/DisclaimerState.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.disclaimer.redux import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.AppScreen + import com.tangem.tap.common.entities.ProgressState import com.tangem.tap.features.disclaimer.Disclaimer import com.tangem.tap.features.disclaimer.DummyDisclaimer @@ -9,7 +9,7 @@ import org.rekotlin.StateType data class DisclaimerState( val disclaimer: Disclaimer = DummyDisclaimer(), - val showedFromScreen: AppScreen = AppScreen.Home, + val showedFrom: DisclaimerSource = DisclaimerSource.Home, val callback: DisclaimerCallback? = null, val progressState: ProgressState? = null, ) : StateType diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt index d6767e13ac..9d217e6111 100644 --- a/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/disclaimer/ui/DisclaimerFragment.kt @@ -7,7 +7,7 @@ import android.webkit.WebView import androidx.lifecycle.lifecycleScope import androidx.transition.TransitionInflater import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.core.navigation.AppScreen + import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.tap.common.entities.ProgressState import com.tangem.tap.common.extensions.beginDelayedTransition @@ -18,6 +18,7 @@ import com.tangem.tap.features.BaseFragment import com.tangem.tap.features.addBackPressHandler import com.tangem.tap.features.disclaimer.Disclaimer import com.tangem.tap.features.disclaimer.redux.DisclaimerAction +import com.tangem.tap.features.disclaimer.redux.DisclaimerSource import com.tangem.tap.features.disclaimer.redux.DisclaimerState import com.tangem.tap.store import com.tangem.wallet.R @@ -76,17 +77,14 @@ class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubs override fun configureTransitions() { val inflater = TransitionInflater.from(requireContext()) - when (store.state.disclaimerState.showedFromScreen) { - AppScreen.Home -> { + when (store.state.disclaimerState.showedFrom) { + DisclaimerSource.Home -> { enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom) exitTransition = inflater.inflateTransition(android.R.transition.slide_top) } - AppScreen.Details -> { + DisclaimerSource.Details -> { super.configureTransitions() } - else -> { - /* no-op */ - } } } diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 8e081474fb..f3fae68135 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -6,17 +6,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.core.view.WindowCompat import androidx.lifecycle.lifecycleScope +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.screen.ComposeFragment import com.tangem.domain.tokens.TokensAction import com.tangem.tap.common.analytics.events.IntroductionProcess +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.home.compose.StoriesScreen import com.tangem.tap.features.home.redux.HomeAction @@ -43,15 +42,12 @@ class HomeFragment : ComposeFragment(), StoreSubscriber { @Composable override fun ScreenContent(modifier: Modifier) { BackHandler(onBack = requireActivity()::finish) - SystemBarsEffect { - setSystemBarsColor(color = Color.Transparent, darkIcons = false) - } + SystemBarsIconsDisposable(darkIcons = false) ScreenContent() } override fun onStart() { super.onStart() - activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) } store.subscribe(subscriber = this) { state -> state @@ -86,7 +82,7 @@ class HomeFragment : ComposeFragment(), StoreSubscriber { }, onSearchTokensClick = { Analytics.send(IntroductionProcess.ButtonTokensList()) - store.dispatch(NavigationAction.NavigateTo(AppScreen.ManageTokens)) + store.dispatchNavigationAction { push(AppRoute.ManageTokens) } store.dispatch(TokensAction.SetArgs.ReadAccess) }, ) diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 92efb060a0..203a2f983f 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -4,11 +4,10 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder @@ -119,7 +118,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch { scope.launch { store.onUserWalletSelected(userWallet = userWallet) } } .doOnResult { - navigateTo(AppScreen.Wallet) + navigateTo(AppRoute.Wallet) } } @@ -143,8 +142,8 @@ private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) { } } -private suspend fun navigateTo(appScreen: AppScreen) { - store.dispatchOnMain(NavigationAction.NavigateTo(appScreen)) +private suspend fun navigateTo(route: AppRoute) { + store.dispatchNavigationAction { push(route) } delay(HIDE_PROGRESS_DELAY) store.dispatch(HomeAction.ScanInProgress(scanInProgress = false)) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 53a3565731..b127b60b18 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -3,11 +3,10 @@ package com.tangem.tap.features.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -16,9 +15,11 @@ import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase import com.tangem.domain.feedback.FeedbackManagerFeatureToggles import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase +import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.send.api.featuretoggles.SendFeatureToggles +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.tap.common.extensions.setContext import com.tangem.tap.features.main.model.MainScreenState import com.tangem.tap.store @@ -34,7 +35,7 @@ import javax.inject.Inject internal class MainViewModel @Inject constructor( private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase, private val listenToFlipsUseCase: ListenToFlipsUseCase, - private val reduxNavController: ReduxNavController, + private val router: AppRouter, private val fetchAppCurrenciesUseCase: FetchAppCurrenciesUseCase, private val deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase, private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase, @@ -44,6 +45,8 @@ internal class MainViewModel @Inject constructor( private val sendFeatureToggles: SendFeatureToggles, private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, + private val stakingFeatureToggles: StakingFeatureToggles, + private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel(), MainIntents { @@ -70,6 +73,10 @@ internal class MainViewModel @Inject constructor( displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() + if (stakingFeatureToggles.isStakingEnabled) { + fetchStakingTokens() + } + viewModelScope.launch(dispatchers.main) { deleteDeprecatedLogsUseCase() } @@ -115,6 +122,14 @@ internal class MainViewModel @Inject constructor( } } + private fun fetchStakingTokens() { + viewModelScope.launch(dispatchers.main) { + fetchStakingTokensUseCase() + .onLeft { Timber.e(it, "Unable to fetch the staking tokens list") } + .onRight { Timber.d("Staking token list was fetched successfully") } + } + } + private fun updateSendFeatureToggle() { viewModelScope.launch(dispatchers.main) { sendFeatureToggles.fetchNewSendEnabled() @@ -136,7 +151,7 @@ internal class MainViewModel @Inject constructor( if (state.value.modalNotification?.isShow != true && !it.isUpdateFromToast) { listenToFlipsUseCase.changeUpdateEnabled(false) stateHolder.updateWithHiddenBalancesNotification() - reduxNavController.navigate(NavigationAction.NavigateTo(AppScreen.ModalNotification)) + router.push(AppRoute.ModalNotification) } } .launchIn(viewModelScope) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt index 0134ddaf15..97f17bd9b5 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding import com.tangem.common.extensions.VoidCallback -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index f03dc73425..f8fb8f2ba8 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -3,10 +3,10 @@ package com.tangem.tap.features.onboarding import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.CardDTO @@ -56,18 +56,18 @@ object OnboardingHelper { } } - fun whereToNavigate(scanResponse: ScanResponse): AppScreen { + fun whereToNavigate(scanResponse: ScanResponse): AppRoute { return when (val type = scanResponse.productType) { - ProductType.Note -> AppScreen.OnboardingNote + ProductType.Note -> AppRoute.OnboardingNote ProductType.Wallet, ProductType.Wallet2, ProductType.Ring, -> if (scanResponse.card.settings.isBackupAllowed) { - AppScreen.OnboardingWallet + AppRoute.OnboardingWallet() } else { - AppScreen.OnboardingOther + AppRoute.OnboardingOther } - ProductType.Twins -> AppScreen.OnboardingTwins + ProductType.Twins -> AppRoute.OnboardingTwins ProductType.Start2Coin, ProductType.Visa, -> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported") @@ -111,14 +111,14 @@ object OnboardingHelper { backupCardsIds = backupCardsIds?.toSet(), ), ) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchNavigationAction { push(AppRoute.Wallet) } delay(timeMillis = 1_800) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.SaveWallet)) + store.dispatchNavigationAction { push(AppRoute.SaveWallet) } } // If device has no biometry and save wallet screen has been shown, then go through old scenario else -> { proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) - store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchNavigationAction { push(AppRoute.Wallet) } } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt index 5e0028b00d..89ffc2161c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt @@ -7,15 +7,20 @@ import androidx.core.view.MenuProvider import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.feedback.SupportInfo import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R /** [REDACTED_AUTHOR] */ -class OnboardingMenuProvider : MenuProvider { +class OnboardingMenuProvider( + private val scanResponseProvider: Provider, +) : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { menuInflater.inflate(R.menu.menu_onboarding, menu) } @@ -24,7 +29,12 @@ class OnboardingMenuProvider : MenuProvider { R.id.menu_item_chat_support -> { Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) // changed on email support [REDACTED_TASK_KEY] - store.dispatch(GlobalAction.SendEmail(SupportInfo())) + store.dispatch( + GlobalAction.SendEmail( + feedbackData = SupportInfo(), + scanResponse = scanResponseProvider(), + ), + ) true } else -> false diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt index 032cce60d3..0922d3f023 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt @@ -9,6 +9,8 @@ import com.tangem.tap.common.extensions.show import com.tangem.tap.common.transitions.HomeToOnboardingTransition import com.tangem.tap.features.BaseStoreFragment import com.tangem.tap.features.onboarding.OnboardingMenuProvider +import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentOnboardingMainBinding import com.tangem.wallet.databinding.ViewOnboardingProgressBinding @@ -32,7 +34,13 @@ abstract class BaseOnboardingFragment : BaseStoreFragment(R.layout.fragment_o (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) } - override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider() + override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( + scanResponseProvider = Provider { + store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: store.state.detailsState.scanResponse + ?: error("ScanResponse must be not null") + }, + ) protected fun showConfetti(show: Boolean) = with(binding.vConfetti) { lavConfetti.show(show) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index ef5296a6b4..ef320383b2 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -2,8 +2,9 @@ package com.tangem.tap.features.onboarding.products.note.redux import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.NavigationAction + import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext @@ -204,7 +205,7 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch OnboardingDialog.InterruptOnboarding( onOk = { OnboardingHelper.onInterrupted() - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 033b790e84..2bc6f07ce1 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -3,9 +3,10 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.blockchain.extensions.Result import com.tangem.common.CompletionResult import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.common.extensions.makePrimaryWalletManager import com.tangem.domain.common.extensions.withMainContext @@ -39,6 +40,7 @@ import kotlinx.coroutines.runBlocking import org.rekotlin.Action import org.rekotlin.DispatchFunction import org.rekotlin.Middleware +import kotlin.reflect.KClass object TwinCardsMiddleware { val handler = twinsWalletMiddleware @@ -328,7 +330,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { if (walletsRepository.shouldSaveUserWalletsSync()) { OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) } else { - store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } } } } @@ -352,7 +354,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { store.dispatch(TwinCardsAction.CardsManager.Release) action.shouldResetTwinCardsWidget(shouldReturnCardBack) { - store.dispatchOnMain(NavigationAction.PopBackTo(getPopBackScreen())) + store.dispatchNavigationAction { popTo(routeClass = getPopBackScreen()) } } } store.dispatchDialogShow(OnboardingDialog.InterruptOnboarding(onOkCallback)) @@ -362,18 +364,19 @@ private fun handle(action: Action, dispatch: DispatchFunction) { } } -private fun getPopBackScreen(): AppScreen { +private fun getPopBackScreen(): KClass { val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) return if (userWalletsListManager.hasUserWallets) { val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync } .fold(onSuccess = { true }, onFailure = { false }) + if (isLocked) { - AppScreen.Welcome + AppRoute.Welcome::class } else { - AppScreen.Wallet + AppRoute.Wallet::class } } else { - AppScreen.Home + AppRoute.Home::class } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt index 9ba9492c54..594dd4e77c 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt @@ -1,7 +1,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.getTwinCardNumber +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.tap.common.redux.AppState import org.rekotlin.Action @@ -35,7 +35,10 @@ private fun internalReduce(action: Action, state: AppState): TwinCardsState { ) } is TwinCardsAction.SetStepOfScreen -> { - state = state.copy(currentStep = action.step) + state = state.copy( + currentStep = action.step, + welcomeOnlyScanResponse = (action.step as? TwinCardsStep.WelcomeOnly)?.scanResponse, + ) } is TwinCardsAction.SetUserUnderstand -> { state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt index a5608d4371..fcc38388ac 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.onboarding.products.twins.redux import com.tangem.blockchain.common.WalletManager -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.common.TwinCardNumber +import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapError import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.features.onboarding.OnboardingWalletBalance @@ -29,6 +29,7 @@ data class TwinCardsState( val balanceNonCriticalError: TapError? = null, val balanceCriticalError: TapError? = null, val showConfetti: Boolean = false, + val welcomeOnlyScanResponse: ScanResponse? = null, ) : StateType { val steps: List diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt index 2cbf5949d1..e1e0a58536 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.view.animation.OvershootInterpolator import androidx.annotation.LayoutRes import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.view.MenuProvider import androidx.core.view.isVisible import androidx.transition.TransitionManager import coil.load @@ -25,12 +26,14 @@ import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget import com.tangem.tap.common.transitions.InternalNoteLayoutTransition import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.tap.features.addBackPressHandler +import com.tangem.tap.features.onboarding.OnboardingMenuProvider import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding import dagger.hilt.android.AndroidEntryPoint @@ -61,6 +64,15 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment( } } + override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( + scanResponseProvider = Provider { + store.state.twinCardsState.welcomeOnlyScanResponse + ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: store.state.detailsState.scanResponse + ?: error("ScanResponse must be not null") + }, + ) + @Suppress("MagicNumber") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 925dc9bcc2..efa987c847 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -7,16 +7,16 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.common.extensions.ifNotNull import com.tangem.common.extensions.toHexString +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.services.Result import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.models.Artwork import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.Artwork import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator @@ -25,10 +25,7 @@ import com.tangem.operations.backup.BackupService import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @@ -164,7 +161,7 @@ private fun handleWalletAction(action: Action) { store.dispatch(GlobalAction.Onboarding.Stop) if (scanResponse == null) { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) store.dispatch(HomeAction.ReadCard(scope = action.scope)) } else { val backupState = store.state.onboardingWalletState.backupState @@ -529,7 +526,8 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) addedBackupCardsCount = backupService.addedBackupCardsCount, ), ) - store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + + store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) } } is BackupAction.SkipBackup -> { Analytics.send(Onboarding.Backup.Skipped()) @@ -614,7 +612,7 @@ private fun handleOnBackPressed(state: OnboardingWalletState) { } BackupStep.Finished -> { OnboardingHelper.onInterrupted() - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } } } @@ -625,7 +623,7 @@ private fun showInterruptOnboardingDialog() { onOk = { OnboardingHelper.onInterrupted() store.dispatch(BackupAction.DiscardBackup) - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) }, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 64b81fc771..6663311df8 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -2,7 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.graphics.Bitmap import android.net.Uri -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.redux.StateDialog import org.rekotlin.StateType /** diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt index e92ebc5ca3..475a6d64b9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt @@ -1,6 +1,10 @@ package com.tangem.tap.features.onboarding.products.wallet.ui +import android.app.Activity +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.collectAsState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.windowsize.rememberWindowSize import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen @@ -16,6 +20,7 @@ import com.tangem.wallet.R */ internal class OnboardingSeedPhraseStateHandler( private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(), + private val activity: Activity, ) { fun newState( @@ -49,11 +54,16 @@ internal class OnboardingSeedPhraseStateHandler( val subScreen = viewModel.currentScreen.collectAsState().value setMainScreenToolbarTitle(walletFragment, subScreen) - onboardingSeedPhraseApi.ScreenContent( - uiState = viewModel.uiState, - subScreen = subScreen, - progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress, - ) + TangemTheme( + isDark = isSystemInDarkTheme(), + windowSize = rememberWindowSize(activity = activity), + ) { + onboardingSeedPhraseApi.ScreenContent( + uiState = viewModel.uiState, + subScreen = subScreen, + progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress, + ) + } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 5c7b1e8589..e0577994f9 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -21,13 +21,13 @@ import com.google.android.material.tabs.TabLayoutMediator import com.tangem.common.CardIdFormatter import com.tangem.common.CompletionResult import com.tangem.common.core.CardIdDisplayFormat +import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.ui.extensions.setStatusBarColor import com.tangem.domain.common.util.cardTypesResolver import com.tangem.feature.onboarding.data.model.CreateWalletResponse -import com.tangem.feature.onboarding.navigation.OnboardingRouter import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter @@ -46,6 +46,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.* import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog import com.tangem.tap.mainScope import com.tangem.tap.store +import com.tangem.utils.Provider import com.tangem.wallet.R import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding @@ -66,9 +67,10 @@ class OnboardingWalletFragment : internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer } - private val canSkipBackup by lazy { arguments?.getBoolean(OnboardingRouter.CAN_SKIP_BACKUP) ?: true } + private val canSkipBackup by lazy { arguments?.getBoolean(AppRoute.OnboardingWallet.CAN_SKIP_BACKUP_KEY) ?: true } + + private lateinit var seedPhraseStateHandler: OnboardingSeedPhraseStateHandler - private val seedPhraseStateHandler: OnboardingSeedPhraseStateHandler = OnboardingSeedPhraseStateHandler() private val seedPhraseViewModel by viewModels() private lateinit var cardsWidget: WalletCardsWidget @@ -79,6 +81,7 @@ class OnboardingWalletFragment : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + seedPhraseStateHandler = OnboardingSeedPhraseStateHandler(activity = requireActivity()) val newSeedPhraseRouter = makeSeedPhraseRouter() seedPhraseRouter = newSeedPhraseRouter @@ -113,7 +116,12 @@ class OnboardingWalletFragment : ) } - override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider() + override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( + scanResponseProvider = Provider { + store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: error("ScanResponse must be not null") + }, + ) private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) { val viewBackupCount = flCardsContainer.childCount - 1 @@ -505,7 +513,13 @@ class OnboardingWalletFragment : onOpenChat = { Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) // changed on email support [REDACTED_TASK_KEY] - store.dispatch(GlobalAction.SendEmail(SupportInfo())) + store.dispatch( + GlobalAction.SendEmail( + feedbackData = SupportInfo(), + scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) }, onOpenUriClick = { uri -> store.dispatchOpenUrl(uri.toString()) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt index ace9edcf19..45c8213362 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt @@ -23,7 +23,13 @@ object WalletActivationErrorDialog { setNegativeButton(R.string.common_support) { _, _ -> // changed on email support [REDACTED_TASK_KEY] Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) - store.dispatch(GlobalAction.SendEmail(SupportInfo())) + store.dispatch( + GlobalAction.SendEmail( + feedbackData = SupportInfo(), + scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } setOnDismissListener { store.dispatchDialogHide() } setCancelable(false) 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 45eb5225a2..82799c76e6 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 @@ -3,29 +3,22 @@ package com.tangem.tap.features.saveWallet.redux import com.tangem.common.* import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction 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.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.extensions.onUserWalletSelected +import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState -import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.launch import org.rekotlin.Middleware -import org.rekotlin.Store import timber.log.Timber internal class SaveWalletMiddleware { @@ -83,12 +76,12 @@ internal class SaveWalletMiddleware { Timber.e(error, "Unable to save user wallet") } .doOnSuccess { mainScope.launch { store.onUserWalletSelected(userWallet) } } - .doOnResult { store.navigateToWallet() } + .doOnResult { navigateToWallet() } } } private fun enrollBiometrics() { - store.dispatchOnMain(NavigationAction.OpenBiometricsSettings) + activityResultCaller.openSystemBiometrySettings() } private fun allowToUseBiometrics(state: SaveWalletState) { @@ -133,7 +126,7 @@ internal class SaveWalletMiddleware { ) store.dispatchWithMain(SaveWalletAction.AllowToUseBiometrics.Success) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } private fun dismiss(state: SaveWalletState) { @@ -162,13 +155,9 @@ internal class SaveWalletMiddleware { } } - private suspend fun Store.navigateToWallet() { - dispatchWithMain( - if (store.state.navigationState.backStack.contains(AppScreen.Wallet)) { - NavigationAction.PopBackTo(AppScreen.Wallet) - } else { - NavigationAction.NavigateTo(AppScreen.Wallet) - }, - ) + private fun navigateToWallet() { + store.dispatchNavigationAction { + replaceAll(AppRoute.Wallet) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt index 3c3409459e..35ee864987 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/SendScreenAction.kt @@ -7,7 +7,8 @@ import com.tangem.blockchain.common.FeePaidCurrency import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.core.TangemSdkError -import com.tangem.core.navigation.StateDialog +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered import com.tangem.tap.common.redux.ToastNotificationAction @@ -185,12 +186,16 @@ sealed class SendAction : SendScreenAction { ) : Dialog() sealed class SendTransactionFails : Dialog() { - data class CardSdkError(val error: TangemSdkError) : Dialog() - data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog() + data class CardSdkError(val error: TangemSdkError, val scanResponse: ScanResponse) : Dialog() + data class BlockchainSdkError( + val error: com.tangem.blockchain.common.BlockchainSdkError, + val scanResponse: ScanResponse, + ) : Dialog() } data class RequestFeeError( val error: com.tangem.blockchain.common.BlockchainSdkError, + val scanResponse: ScanResponse, val onRetry: () -> Unit, ) : Dialog() diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt index e3096c6a4f..ea4092c252 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/RequestFeeMiddleware.kt @@ -1,6 +1,9 @@ package com.tangem.tap.features.send.redux.middlewares -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero @@ -92,6 +95,7 @@ class RequestFeeMiddleware { dispatch( SendAction.Dialog.RequestFeeError( error = blockchainSdkError, + scanResponse = scanResponse, onRetry = { dispatch(FeeAction.RequestFee) }, ), ) diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 756b65e48d..ecb0e071d8 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -14,10 +14,10 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.Result import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.common.core.TangemSdkError +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.demo.DemoTransactionSender @@ -270,7 +270,7 @@ private fun sendTransaction( ), ) Analytics.sendSelectedCurrencyEvent(mainCurrencyType) - dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } } is Result.Failure -> { @@ -288,11 +288,25 @@ private fun sendTransaction( val tangemSdkError = error.tangemError as? TangemSdkError ?: return@withMainContext if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext - dispatch(SendAction.Dialog.SendTransactionFails.CardSdkError(tangemSdkError)) + dispatch( + SendAction.Dialog.SendTransactionFails.CardSdkError( + error = tangemSdkError, + scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) + .selectedUserWalletSync?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } is BlockchainSdkError.CreateAccountUnderfunded -> { // from XLM, XRP, Polkadot - dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) + dispatch( + SendAction.Dialog.SendTransactionFails.BlockchainSdkError( + error = error, + scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) + .selectedUserWalletSync?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } is BlockchainSdkError.Kaspa.UtxoAmountError -> { dispatch( @@ -326,12 +340,19 @@ private fun sendTransaction( AppDialog.SimpleOkDialogRes( headerId = R.string.common_done, messageId = R.string.alert_demo_feature_disabled, - onOk = { dispatch(NavigationAction.PopBackTo()) }, + onOk = { store.dispatchNavigationAction(AppRouter::pop) }, ), ) } else -> { - dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) + dispatch( + SendAction.Dialog.SendTransactionFails.BlockchainSdkError( + error = error, + scanResponse = store.inject(DaggerGraphState::generalUserWalletsListManager) + .selectedUserWalletSync?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } } } diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt index 1f3b82b267..76daaa5802 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/AmountReducer.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.send.redux.reducers -import com.tangem.common.Strings.STARS import com.tangem.common.extensions.isZero import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.features.send.redux.AmountAction @@ -11,6 +10,7 @@ import com.tangem.tap.features.send.redux.states.AmountState import com.tangem.tap.features.send.redux.states.InputViewValue import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.redux.states.SendState +import com.tangem.utils.Strings.STARS import java.math.BigDecimal /** diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt index d3d3cd7e19..115ab92034 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/states/SendState.kt @@ -4,8 +4,8 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.WalletManager import com.tangem.common.extensions.isZero -import com.tangem.core.navigation.StateDialog import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.CurrencyConverter import com.tangem.tap.common.entities.IndeterminateProgressButton diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index fd6d7105d3..34650ad800 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -8,7 +8,6 @@ import android.text.method.DigitsKeyListener import android.view.View import android.view.inputmethod.EditorInfo import android.widget.EditText -import androidx.core.os.bundleOf import androidx.core.view.postDelayed import androidx.core.widget.addTextChangedListener import androidx.fragment.app.viewModels @@ -20,19 +19,18 @@ import androidx.recyclerview.widget.RecyclerView import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.textfield.TextInputEditText import com.tangem.Message +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.sdk.extensions.hideSoftKeyboard import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.getFromClipboard import com.tangem.tap.common.extensions.setOnImeActionListener import com.tangem.tap.common.recyclerView.SpaceItemDecoration @@ -162,14 +160,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { imvQrCode.setOnClickListener { Analytics.send(Token.Send.ButtonQRCode()) - store.dispatchOnMain( - NavigationAction.NavigateTo( - screen = AppScreen.QrScanning, - bundle = bundleOf( - QrScanningRouter.SOURCE_KEY to SourceType.SEND, - ), - ), - ) + store.dispatchNavigationAction { + push(AppRoute.QrScanning(source = SourceType.SEND)) + } } } @@ -370,7 +363,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { override fun handleOnBackPressed() { val externalTransactionData = store.state.sendState.externalTransactionData if (externalTransactionData == null) { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } else { store.dispatch(TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt index 9f59533686..881e9f6df2 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendViewModel.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.send.ui import androidx.lifecycle.* import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase @@ -12,7 +13,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.features.send.api.navigation.SendRouter import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.di.DelayedWork import com.tangem.tap.features.send.redux.AddressActionUi @@ -52,7 +52,7 @@ internal class SendViewModel @Inject constructor( .launchIn(viewModelScope) } - private val cryptoCurrency: CryptoCurrency? = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + private val cryptoCurrency: CryptoCurrency? = savedStateHandle[AppRoute.Send.CRYPTO_CURRENCY_KEY] override fun onCreate(owner: LifecycleOwner) { getBalanceHidingSettingsUseCase() diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt index cf27394348..bb8a5ea183 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/RequestFeeErrorDialog.kt @@ -23,7 +23,12 @@ object RequestFeeErrorDialog { setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) setNegativeButton(R.string.details_row_title_contact_to_support) { _, _ -> Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send)) - store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) + store.dispatch( + GlobalAction.SendEmail( + feedbackData = SendTransactionFailedEmail(errorMessage), + scanResponse = dialog.scanResponse, + ), + ) } setPositiveButton(R.string.common_retry) { _, _ -> dialog.onRetry() } setNeutralButton(R.string.common_cancel) { _, _ -> } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt index 5687caa590..a768ea033b 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -8,6 +8,7 @@ import com.tangem.common.module.ModuleMessageConverter import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic +import com.tangem.domain.models.scan.ScanResponse import com.tangem.sdk.extensions.localizedDescription import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.feedback.SendTransactionFailedEmail @@ -21,21 +22,21 @@ import com.tangem.wallet.R */ object SendTransactionFailsDialog { fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.CardSdkError): AlertDialog { - return create(context, dialog.error.localizedDescription(context)) + return create(context, dialog.error.localizedDescription(context), dialog.scanResponse) } fun create(context: Context, dialog: SendAction.Dialog.SendTransactionFails.BlockchainSdkError): AlertDialog { val errorConverter = BlockchainSdkErrorConverter(context) - return create(context, errorConverter.convert(dialog.error)) + return create(context, errorConverter.convert(dialog.error), dialog.scanResponse) } - private fun create(context: Context, errorMessage: String): AlertDialog { + private fun create(context: Context, errorMessage: String, scanResponse: ScanResponse): AlertDialog { return AlertDialog.Builder(context).apply { setTitle(R.string.alert_failed_to_send_transaction_title) setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, errorMessage)) setNeutralButton(R.string.details_row_title_contact_to_support) { _, _ -> Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Send)) - store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage))) + store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(errorMessage), scanResponse)) } setPositiveButton(R.string.common_cancel) { _, _ -> } setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt index 272c773c95..ed260691fe 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/TokensListFragment.kt @@ -1,13 +1,10 @@ package com.tangem.tap.features.tokens.impl.presentation -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel @@ -30,12 +27,7 @@ internal class TokensListFragment : ComposeFragment() { val viewModel = hiltViewModel().apply { LocalLifecycleOwner.current.lifecycle.addObserver(this) } - val statusBarColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(color = statusBarColor) - } TokensListScreen( - modifier = Modifier.systemBarsPadding(), stateHolder = viewModel.uiState, ) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt index 27418f3d01..a62f01e74b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/router/DefaultTokensListRouter.kt @@ -1,9 +1,11 @@ package com.tangem.tap.features.tokens.impl.presentation.router import com.tangem.blockchain.common.Blockchain -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter + import com.tangem.tap.common.extensions.dispatchDialogShow +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.store @@ -18,11 +20,11 @@ import com.tangem.wallet.R internal class DefaultTokensListRouter : TokensListRouter { override fun popBackStack() { - store.dispatch(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) } override fun openAddCustomTokenScreen() { - store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken)) + store.dispatchNavigationAction { push(AppRoute.AddCustomToken) } } override fun showAddressCopiedNotification() { @@ -54,7 +56,7 @@ internal class DefaultTokensListRouter : TokensListRouter { val alert = AppDialog.SimpleOkDialogRes( headerId = R.string.common_error, messageId = R.string.common_unknown_error, - onOk = { store.dispatch(NavigationAction.PopBackTo()) }, + onOk = { store.dispatchNavigationAction(AppRouter::pop) }, ) store.dispatchDialogShow(alert) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt index 56366dfb51..d045437bb8 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity @@ -34,6 +33,8 @@ import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemContentType import androidx.paging.compose.itemKey +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme @@ -61,32 +62,45 @@ internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modi Scaffold( modifier = modifier, - topBar = { TokensListToolbar(state = stateHolder.toolbarState) }, + topBar = { + TokensListToolbar(state = stateHolder.toolbarState) + }, floatingActionButton = { if (stateHolder is TokensListStateHolder.ManageContent) { val density = LocalDensity.current val verticalPadding = TangemTheme.dimens.spacing32 SaveChangesButton( - modifier = Modifier.onSizeChanged { - with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding } - }, + modifier = Modifier + .navigationBarsPadding() + .onSizeChanged { + with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding } + }, showProgress = stateHolder.isSavingInProgress, onClick = stateHolder.onSaveButtonClick, ) } }, floatingActionButtonPosition = FabPosition.Center, - backgroundColor = TangemTheme.colors.background.primary, - ) { scaffoldPadding -> + backgroundColor = TangemTheme.colors.background.secondary, + ) { _ -> val tokens = stateHolder.tokens.collectAsLazyPagingItems() - TokensListContent( - isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible, - tokens = tokens, - scaffoldPadding = scaffoldPadding, - bottomMarginDp = floatingButtonHeight, - ) + if (stateHolder !is TokensListStateHolder.ManageContent) { + NavigationBar3ButtonsScrim() + } + + Box { + TokensListContent( + isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible, + tokens = tokens, + bottomMarginDp = floatingButtonHeight, + ) + + if (stateHolder is TokensListStateHolder.ManageContent) { + BottomFade(Modifier.align(Alignment.BottomCenter)) + } + } Crossfade(targetState = stateHolder.isLoading, label = "Update progress bar visibility") { if (it) { @@ -112,23 +126,24 @@ private fun LoadingContent() { } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun TokensListContent( isDifferentAddressesBlockVisible: Boolean, tokens: LazyPagingItems, - scaffoldPadding: PaddingValues, bottomMarginDp: Dp, ) { val state = rememberLazyListState() + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } LazyColumn( modifier = Modifier + .background(color = TangemTheme.colors.background.primary) .imePadding() - .fillMaxSize() - .padding(scaffoldPadding), + .fillMaxSize(), state = state, - contentPadding = PaddingValues(bottom = bottomMarginDp), + contentPadding = PaddingValues( + bottom = bottomMarginDp + bottomBarHeight, + ), ) { item( key = "DifferentAddressesWarning$isDifferentAddressesBlockVisible", diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt index 5e0aafc999..0a4dd9c309 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListToolbar.kt @@ -1,10 +1,7 @@ package com.tangem.tap.features.tokens.impl.presentation.ui import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -16,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -42,8 +40,15 @@ internal fun TokensListToolbar(state: TokensListToolbarState) { } else { AppBarDefaults.TopAppBarElevation } + val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } + TopAppBar( backgroundColor = TangemTheme.colors.background.secondary, + contentPadding = PaddingValues( + start = TangemTheme.dimens.spacing4, + end = TangemTheme.dimens.spacing4, + top = statusBarHeight, + ), elevation = toolbarElevation, ) { IconButton(onClick = state.onBackButtonClick) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt index 050dc5f876..b10741f96c 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListMigration.kt @@ -3,8 +3,8 @@ package com.tangem.tap.features.tokens.impl.presentation.viewmodels import arrow.core.Either import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.domain.card.DerivePublicKeysUseCase import com.tangem.domain.common.util.derivationStyleProvider @@ -16,7 +16,7 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.common.extensions.dispatchDebugErrorNotification -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.inject import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store @@ -126,7 +126,7 @@ internal class TokensListMigration( val isNothingToDoWithBlockchain = blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty() if (isNothingToDoWithTokens && isNothingToDoWithBlockchain) { store.dispatchDebugErrorNotification(message = "Nothing to save") - store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } return } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 53867da367..f5dc7f92b4 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -11,9 +11,9 @@ import androidx.lifecycle.viewModelScope import androidx.paging.* import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.extensions.getActiveIconRes import com.tangem.core.ui.extensions.getGreyedOutIconRes import com.tangem.domain.card.DerivePublicKeysUseCase @@ -26,7 +26,7 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.TokenWithBlockchain import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.tap.common.extensions.dispatchWithMain +import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.fullNameWithoutTestnet import com.tangem.tap.common.extensions.getNetworkName import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType @@ -326,7 +326,7 @@ internal class TokensListViewModel @Inject constructor( ) uiState = state.copy(isSavingInProgress = false) - store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet)) + store.dispatchNavigationAction { popTo() } } } 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 39a0168df7..a4233d6ea8 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 @@ -1,18 +1,16 @@ package com.tangem.tap.features.wallet.redux.middlewares -import androidx.core.os.bundleOf import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.NetworkAddress -import com.tangem.feature.swap.presentation.SwapFragment -import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.* @@ -150,7 +148,7 @@ object TradeCryptoMiddleware { } private fun openReceiptUrl(transactionId: String) { - store.dispatchOnMain(NavigationAction.PopBackTo()) + store.dispatchNavigationAction(AppRouter::pop) store.state.globalState.exchangeManager.getSellCryptoReceiptUrl( action = CurrencyExchangeManager.Action.Sell, transactionId = transactionId, @@ -158,11 +156,7 @@ object TradeCryptoMiddleware { } private fun openSwap(currency: CryptoCurrency) { - val bundle = bundleOf( - SwapFragment.CURRENCY_BUNDLE_KEY to currency, - ) - - store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle)) + store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) } } private fun handleSendToken(action: TradeCryptoAction.SendToken) { @@ -214,11 +208,12 @@ object TradeCryptoMiddleware { ) } - val bundle = bundleOf( - SendRouter.CRYPTO_CURRENCY_KEY to currency, - SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + val route = AppRoute.Send( + currency = currency, + userWalletId = action.userWallet.walletId, ) - store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + + store.dispatchNavigationAction { push(route) } } } @@ -285,17 +280,17 @@ object TradeCryptoMiddleware { ) } - val bundle = bundleOf( - SendRouter.CRYPTO_CURRENCY_KEY to currency, - SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue, + val route = AppRoute.Send( + currency = currency, + userWalletId = action.userWallet.walletId, ) - store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + store.dispatchNavigationAction { push(route) } } } private fun handleNewSendToken(action: TradeCryptoAction.SendToken) { handleNewSend( - userWalletId = action.userWallet.walletId.stringValue, + userWalletId = action.userWallet.walletId, txInfo = action.transactionInfo, currency = action.tokenCurrency, ) @@ -303,25 +298,26 @@ object TradeCryptoMiddleware { private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) { handleNewSend( - userWalletId = action.userWallet.walletId.stringValue, + userWalletId = action.userWallet.walletId, txInfo = action.transactionInfo, currency = action.coinStatus.currency, ) } private fun handleNewSend( - userWalletId: String, + userWalletId: UserWalletId, txInfo: TradeCryptoAction.TransactionInfo?, currency: CryptoCurrency, ) { - val bundle = bundleOf( - SendRouter.CRYPTO_CURRENCY_KEY to currency, - SendRouter.USER_WALLET_ID_KEY to userWalletId, - SendRouter.TRANSACTION_ID_KEY to txInfo?.transactionId, - SendRouter.DESTINATION_ADDRESS_KEY to txInfo?.destinationAddress, - SendRouter.AMOUNT_KEY to txInfo?.amount, - SendRouter.TAG_KEY to txInfo?.tag, + val route = AppRoute.Send( + currency = currency, + userWalletId = userWalletId, + transactionId = txInfo?.transactionId, + destinationAddress = txInfo?.destinationAddress, + amount = txInfo?.amount, + tag = txInfo?.tag, ) - store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Send, bundle = bundle)) + + store.dispatchNavigationAction { push(route) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index abfc205397..2a564e8a0d 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -6,11 +6,12 @@ import com.tangem.common.doOnFailure import com.tangem.common.doOnResult import com.tangem.common.doOnSuccess import com.tangem.common.flatMap +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction + import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletBuilder @@ -98,7 +99,7 @@ internal class WelcomeMiddleware { signInType = Basic.SignedIn.SignInType.Biometric, ) - store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchNavigationAction { push(AppRoute.Wallet) } store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success) store.onUserWalletSelected(userWallet = selectedUserWallet) @@ -130,7 +131,7 @@ internal class WelcomeMiddleware { .doOnSuccess { sendSignedInAnalyticsEvent(scanResponse = scanResponse, signInType = Basic.SignedIn.SignInType.Card) - store.dispatchWithMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + store.dispatchNavigationAction { push(AppRoute.Wallet) } store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) store.onUserWalletSelected(userWallet = userWallet) @@ -170,7 +171,7 @@ internal class WelcomeMiddleware { } .doOnResult { store.dispatchWithMain(WelcomeAction.CloseError) - store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home)) + store.dispatchNavigationAction { popTo() } } } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index 60ab61900f..c34497d9f0 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -16,7 +16,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.Analytics import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.tap.common.analytics.events.SignIn @@ -49,19 +48,14 @@ internal class WelcomeFragment : ComposeFragment() { val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) val warning by rememberUpdatedState(newValue = state.warning) - val backgroundColor = TangemTheme.colors.background.primary - SystemBarsEffect { - setSystemBarsColor(backgroundColor) - } - BackHandler { requireActivity().finish() } Box( modifier = modifier - .systemBarsPadding() - .background(backgroundColor), + .background(TangemTheme.colors.background.primary) + .systemBarsPadding(), ) { WelcomeScreenContent( showUnlockProgress = state.showUnlockWithBiometricsProgress, @@ -88,8 +82,4 @@ internal class WelcomeFragment : ComposeFragment() { } } } - - internal companion object { - const val INITIAL_INTENT_KEY = "intent" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt index bc672b7c2f..bb885005f9 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt @@ -1,8 +1,11 @@ package com.tangem.tap.features.welcome.ui -import android.content.Intent +import android.os.Bundle import androidx.lifecycle.* import com.tangem.common.core.TangemError +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.Analytics import com.tangem.domain.wallets.legacy.UserWalletsListError import com.tangem.tap.common.analytics.events.SignIn @@ -26,7 +29,8 @@ internal class WelcomeViewModel @Inject constructor( StoreSubscriber, DefaultLifecycleObserver { - private val initialIntent: Intent? = savedStateHandle[WelcomeFragment.INITIAL_INTENT_KEY] + private val initialIntent: SerializableIntent? = savedStateHandle.get(AppRoute.Welcome.INITIAL_INTENT_KEY) + ?.let { it.unbundle(SerializableIntent.serializer()) } private val stateInternal = MutableStateFlow(WelcomeScreenState()) val state: StateFlow = stateInternal @@ -38,7 +42,7 @@ internal class WelcomeViewModel @Inject constructor( initGlobalState() val welcomeAction = if (initialIntent != null) { - WelcomeAction.ProceedWithIntent(initialIntent) + WelcomeAction.ProceedWithIntent(initialIntent.toIntent()) } else { WelcomeAction.ProceedWithBiometrics() } diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt index 7e953fdfd5..a5ef219c9b 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt @@ -8,6 +8,6 @@ internal class DefaultStakeKitAuthProvider( ) : StakeKitAuthProvider { override fun getApiKey(): String { - return configManager.config.express?.apiKey ?: error("No StakeKit api key provided") + return configManager.config.stakeKitApiKey ?: error("No StakeKit api key provided") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index cf7de3bfe1..a2c9dd483f 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -1,18 +1,14 @@ package com.tangem.tap.proxy -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet -import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected -import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.ExchangeService import org.rekotlin.Action @@ -23,7 +19,7 @@ import javax.inject.Inject * Holds objects from old modules, that missing in DI graph. * Object sets manually to use in new modules and [AppStateHolder] proxies its to DI. */ -class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolder { +class AppStateHolder @Inject constructor() : ReduxStateHolder { @Deprecated("Use scan response from selected user wallet") var scanResponse: ScanResponse? = null @@ -35,16 +31,6 @@ class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolde return scanResponse?.card } - override fun navigate(action: NavigationAction) { - mainStore?.dispatchOnMain(action) - } - - override fun getBackStack(): List = mainStore?.state?.navigationState?.backStack.orEmpty() - - override fun popBackStack(screen: AppScreen?) { - mainStore?.dispatchOnMain(NavigationAction.PopBackTo(screen)) - } - override fun dispatch(action: Action) { mainStore?.dispatch(action) } @@ -57,7 +43,7 @@ class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolde mainStore?.onUserWalletSelected(userWallet) } - override fun sendFeedbackEmail() { - mainStore?.dispatch(GlobalAction.SendEmail(FeedbackEmail())) + override fun dispatchDialogShow(dialog: StateDialog) { + mainStore?.dispatchDialogShow(dialog) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index a18e07daef..c199b92e7b 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -1,37 +1,17 @@ package com.tangem.tap.proxy -import androidx.core.text.isDigitsOnly -import com.google.firebase.crashlytics.FirebaseCrashlytics -import com.tangem.Message -import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras -import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras -import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras -import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager -import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.optimism.EthereumOptimisticRollupWalletManager -import com.tangem.blockchain.blockchains.stellar.StellarMemo -import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras -import com.tangem.blockchain.blockchains.ton.TonTransactionExtras -import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.externallinkprovider.TxExploreState -import com.tangem.blockchain.network.ResultChecker import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.hexToBytes -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.* -import com.tangem.lib.crypto.models.transactions.SendTxResult -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TangemSigner import java.math.BigDecimal import java.math.BigInteger import java.math.MathContext @@ -39,84 +19,10 @@ import java.math.RoundingMode @Suppress("LargeClass") class TransactionManagerImpl( - private val appStateHolder: AppStateHolder, - private val cardSdkConfigRepository: CardSdkConfigRepository, private val walletManagersFacade: WalletManagersFacade, private val userWalletsListManager: UserWalletsListManager, ) : TransactionManager { - override suspend fun sendApproveTransaction( - txData: ApproveTxData, - derivationPath: String?, - analyticsData: AnalyticsData, - ): SendTxResult { - val blockchain = requireNotNull(Blockchain.fromNetworkId(txData.networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - walletManager.update() - val amount = Amount(value = BigDecimal.ZERO, blockchain = blockchain) - return sendTransactionInternal( - walletManager = walletManager, - amount = amount, - blockchain = blockchain, - feeAmount = txData.feeAmount, - gasLimit = txData.gasLimit, - destinationAddress = txData.destinationAddress, - dataToSign = txData.dataToSign, - ) - } - - override suspend fun sendTransaction( - txData: SwapTxData, - isSwap: Boolean, - derivationPath: String?, - analyticsData: AnalyticsData, - ): SendTxResult { - val blockchain = requireNotNull(Blockchain.fromNetworkId(txData.networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - walletManager.update() - val amount = if (isSwap) { - createAmountForSwap(txData.amountToSend, txData.currencyToSend, blockchain) - } else { - createAmount(txData.amountToSend, txData.currencyToSend, blockchain) - } - return sendTransactionInternal( - walletManager = walletManager, - amount = amount, - blockchain = blockchain, - feeAmount = txData.feeAmount, - gasLimit = txData.gasLimit, - destinationAddress = txData.destinationAddress, - dataToSign = txData.dataToSign, - ) - } - - @Suppress("LongParameterList") - private suspend fun sendTransactionInternal( - walletManager: WalletManager, - amount: Amount, - blockchain: Blockchain, - feeAmount: BigDecimal, - gasLimit: Int, - destinationAddress: String, - dataToSign: String, - ): SendTxResult { - val txData = walletManager.createTransaction( - amount = amount, - fee = Fee.Common(Amount(value = feeAmount, blockchain = blockchain)), - destination = destinationAddress, - ).copy(hash = dataToSign, extras = createExtras(walletManager, gasLimit, dataToSign)) - - val signer = transactionSigner(walletManager) - - val sendResult = try { - (walletManager as? TransactionSender)?.send(txData, signer) ?: error("Cannot cast to TransactionSender") - } catch (ex: Exception) { - FirebaseCrashlytics.getInstance().recordException(ex) - return SendTxResult.UnknownError(ex) - } - return handleSendResult(result = sendResult) - } - override fun getExplorerTransactionLink(networkId: String, txAddress: String): String { val blockchain = Blockchain.fromNetworkId(networkId) ?: error("blockchain not found") return when (val txUrlState = blockchain.getExploreTxUrl(txAddress)) { @@ -125,39 +31,11 @@ class TransactionManagerImpl( } } - override fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { - val blockchain = Blockchain.fromNetworkId(networkId) - if (memo == null) return null - return when (blockchain) { - Blockchain.Stellar -> { - val xlmMemo = if (memo.isNotEmpty() && memo.isDigitsOnly()) { - StellarMemo.Id(memo.toBigInteger()) - } else { - StellarMemo.Text(memo) - } - StellarTransactionExtras(xlmMemo) - } - Blockchain.Binance -> BinanceTransactionExtras(memo) - Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } - Blockchain.Cosmos -> CosmosTransactionExtras(memo) - Blockchain.TON -> TonTransactionExtras(memo) - Blockchain.Hedera -> HederaTransactionExtras(memo) - Blockchain.Algorand -> AlgorandTransactionExtras(memo) - else -> null - } - } - override suspend fun updateWalletManager(networkId: String, derivationPath: String?) { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } getActualWalletManager(blockchain, derivationPath).update() } - override fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val gasPriceValue = requireNotNull(gasPrice.toLongOrNull()) { "gasprice should be Long" } - return (gasPriceValue * estimatedGas).toBigDecimal().movePointLeft(blockchain.decimals()) - } - @Throws(IllegalStateException::class) override suspend fun getFee( networkId: String, @@ -363,60 +241,6 @@ class TransactionManagerImpl( } } - private fun handleSendResult(result: Result): SendTxResult { - when (result) { - is Result.Success -> { - return SendTxResult.Success - } - is Result.Failure -> { - if (ResultChecker.isNetworkError(result)) return SendTxResult.NetworkError(result.error) - val error = result.error as? BlockchainSdkError ?: return SendTxResult.UnknownError() - when (error) { - is BlockchainSdkError.WrappedTangemError -> { - val errorByCode = mapErrorByCode(error) - if (errorByCode != null) { - return errorByCode - } - val tangemSdkError = error.tangemError as? TangemSdkError ?: return SendTxResult.UnknownError() - if (tangemSdkError is TangemSdkError.UserCancelled) return SendTxResult.UserCancelledError - return SendTxResult.TangemSdkError(tangemSdkError.code, tangemSdkError.cause) - } - else -> { - return SendTxResult.TangemSdkError(error.code, error.cause) - } - } - } - } - } - - private fun mapErrorByCode(error: BlockchainSdkError.WrappedTangemError): SendTxResult? { - return when (error.code) { - USER_CANCELLED_ERROR_CODE -> { - return SendTxResult.UserCancelledError - } - else -> { - null - } - } - } - - private fun transactionSigner(walletManager: WalletManager): TransactionSigner { - val actualCard = requireNotNull(appStateHolder.getActualCard()) { "no card found" } - return TangemSigner( - card = actualCard, - tangemSdk = cardSdkConfigRepository.sdk, - initialMessage = Message(), - ) { signResponse -> - appStateHolder.mainStore?.dispatch( - GlobalAction.UpdateWalletSignedHashes( - walletSignedHashes = signResponse.totalSignedHashes, - walletPublicKey = walletManager.wallet.publicKey.seedKey, - remainingSignatures = signResponse.remainingSignatures, - ), - ) - } - } - private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { val selectedUserWallet = requireNotNull( userWalletsListManager.selectedUserWalletSync, @@ -430,24 +254,6 @@ class TransactionManagerImpl( return requireNotNull(walletManager) { "no wallet manager found" } } - private fun createExtras( - walletManager: WalletManager, - gasLimit: Int, - transactionHash: String, - ): TransactionExtras? { - return when (walletManager) { - is EthereumWalletManager -> { - return EthereumTransactionExtras( - data = transactionHash.removePrefix(HEX_PREFIX).hexToBytes(), - gasLimit = gasLimit.toBigInteger(), - ) - } - else -> { - null - } - } - } - /** * Create proxy fees * @@ -512,25 +318,6 @@ class TransactionManagerImpl( } } - private fun createAmountForSwap(amount: BigDecimal, currency: Currency?, blockchain: Blockchain): Amount { - return when (currency) { - is Currency.NativeToken, - null, - -> { - Amount(value = amount, blockchain = blockchain) - } - is Currency.NonNativeToken -> { - // 1. when creates swap amount for NonNativeToken, amount should be ZERO - // 2. Amount has .Coin type, as workaround to use destinationAddress in bsdk, not contractAddress - Amount( - currencySymbol = currency.symbol, - value = BigDecimal.ZERO, - decimals = currency.decimalCount, - ) - } - } - } - private fun convertNonNativeToken(token: Currency.NonNativeToken): Token { return Token( name = token.name, @@ -564,8 +351,6 @@ class TransactionManagerImpl( } companion object { - private const val HEX_PREFIX = "0x" - private const val USER_CANCELLED_ERROR_CODE = 50002 private const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50% private const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50% } diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index 37fc1060b4..5c9c990ba5 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -1,16 +1,16 @@ package com.tangem.tap.proxy.di -import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.tap.proxy.* +import com.tangem.tap.proxy.AppStateHolder +import com.tangem.tap.proxy.TransactionManagerImpl +import com.tangem.tap.proxy.UserWalletManagerImpl import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.flow.* import javax.inject.Singleton @Module @@ -38,14 +38,10 @@ internal object ProxyModule { @Provides @Singleton fun provideTransactionManager( - appStateHolder: AppStateHolder, - cardSdkConfigRepository: CardSdkConfigRepository, walletManagersFacade: WalletManagersFacade, userWalletsListManager: UserWalletsListManager, ): TransactionManager { return TransactionManagerImpl( - appStateHolder = appStateHolder, - cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, userWalletsListManager = userWalletsListManager, ) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index f854ba0dae..0f3c13fbf9 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -4,8 +4,9 @@ import com.tangem.core.navigation.email.EmailSender import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.features.managetokens.navigation.ManageTokensUi +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter @@ -20,10 +21,11 @@ sealed interface DaggerGraphAction : Action { val walletRouter: WalletRouter, val walletConnectInteractor: WalletConnectInteractor, val tokenDetailsRouter: TokenDetailsRouter, - val manageTokensUi: ManageTokensUi, val cardSdkConfigRepository: CardSdkConfigRepository, val sendRouter: SendRouter, val qrScanningRouter: QrScanningRouter, val emailSender: EmailSender, + val stakingRouter: StakingRouter, + val pushNotificationsRouter: PushNotificationsRouter, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index b25fdce12b..2a20bab5b6 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -18,11 +18,12 @@ object DaggerGraphReducer { walletRouter = action.walletRouter, walletConnectInteractor = action.walletConnectInteractor, tokenDetailsRouter = action.tokenDetailsRouter, - manageTokensUi = action.manageTokensUi, cardSdkConfigRepository = action.cardSdkConfigRepository, sendRouter = action.sendRouter, qrScanningRouter = action.qrScanningRouter, emailSender = action.emailSender, + stakingRouter = action.stakingRouter, + pushNotificationsRouter = action.pushNotificationsRouter, ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index a61bdcde57..ee435bc6ec 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -2,7 +2,10 @@ package com.tangem.tap.proxy.redux import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.email.EmailSender +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository @@ -13,6 +16,8 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.FeedbackManagerFeatureToggles +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.GetFeedbackEmailUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase @@ -24,17 +29,17 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.details.DetailsFeatureToggles -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles -import com.tangem.features.managetokens.navigation.ManageTokensUi +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.featuretoggles.SendFeatureToggles import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles import com.tangem.tap.proxy.AppStateHolder @@ -50,8 +55,6 @@ data class DaggerGraphState( val walletConnectSessionsRepository: WalletConnectSessionsRepository? = null, val walletConnectInteractor: WalletConnectInteractor? = null, val tokenDetailsRouter: TokenDetailsRouter? = null, - val manageTokensFeatureToggles: ManageTokensFeatureToggles? = null, - val manageTokensUi: ManageTokensUi? = null, val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, @@ -76,7 +79,14 @@ data class DaggerGraphState( val blockchainSDKFactory: BlockchainSDKFactory? = null, val emailSender: EmailSender? = null, val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, + val getFeedbackEmailUseCase: GetFeedbackEmailUseCase? = null, + val getCardInfoUseCase: GetCardInfoUseCase? = null, val assetLoader: AssetLoader? = null, val detailsFeatureToggles: DetailsFeatureToggles? = null, - val detailsEntryPoint: DetailsEntryPoint? = null, + val stakingRouter: StakingRouter? = null, + val urlOpener: UrlOpener? = null, + val shareManager: ShareManager? = null, + val appRouter: AppRouter? = null, + val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles? = null, + val pushNotificationsRouter: PushNotificationsRouter? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt new file mode 100644 index 0000000000..aca4ce8efb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -0,0 +1,61 @@ +package com.tangem.tap.routing + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.decompose.navigation.Router +import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlin.reflect.KClass + +internal class ProxyAppRouter( + private val config: AppRouterConfig, + private val dispatchers: CoroutineDispatcherProvider, +) : AppRouter { + + private val routerScope: CoroutineScope + get() = requireNotNull(config.routerScope) { + "Router scope is not set in config" + } + + private val innerRouter: Router + get() = requireNotNull(config.componentRouter) { + "Inner router is not set in config" + } + + override val stack: List + get() = requireNotNull(config.stack) { + "Stack is not set in config" + } + + override fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.push(route, onComplete) + } + } + + override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.replaceAll(*routes, onComplete = onComplete) + } + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.pop(onComplete) + } + } + + override fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.popTo(route, onComplete) + } + } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + routerScope.launch(dispatchers.mainImmediate) { + innerRouter.popTo(routeClass, onComplete) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt new file mode 100644 index 0000000000..a7f5148b19 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.routing + +import android.content.Intent +import androidx.fragment.app.Fragment +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.value.Value +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.navigation.Router +import com.tangem.utils.Provider + +internal interface RoutingComponent { + + val router: Router + + val stack: Value> + + sealed class Child { + + data object Initial : Child() + + data class LegacyFragment( + val name: String, + val fragmentProvider: Provider, + ) : Child() + + data class LegacyIntent(val intent: Intent) : Child() + } + + interface Factory { + fun create(context: AppComponentContext): RoutingComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt b/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt new file mode 100644 index 0000000000..470c85f674 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/configurator/AppRouterConfig.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.routing.configurator + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.navigation.Router +import kotlinx.coroutines.CoroutineScope + +internal interface AppRouterConfig { + + var routerScope: CoroutineScope? + var componentRouter: Router? + var stack: List? +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt b/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt new file mode 100644 index 0000000000..ae724ac04e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/configurator/MutableAppRouterConfig.kt @@ -0,0 +1,12 @@ +package com.tangem.tap.routing.configurator + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.navigation.Router +import kotlinx.coroutines.CoroutineScope + +internal class MutableAppRouterConfig : AppRouterConfig { + + override var routerScope: CoroutineScope? = null + override var componentRouter: Router? = null + override var stack: List? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt new file mode 100644 index 0000000000..e766371cb7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt @@ -0,0 +1,54 @@ +package com.tangem.tap.routing.impl + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.value.Value +import com.arkivanov.essenty.backhandler.BackCallback +import com.arkivanov.essenty.lifecycle.doOnDestroy +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.navigation.getOrCreateTyped +import com.tangem.tap.routing.RoutingComponent +import com.tangem.tap.routing.RoutingComponent.Child +import com.tangem.tap.routing.utils.ChildFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Suppress("LongParameterList") +internal class DefaultRoutingComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + private val childFactory: ChildFactory, +) : RoutingComponent, AppComponentContext by context { + + private val backCallback = BackCallback(priority = Int.MIN_VALUE, onBack = router::pop) + + override val stack: Value> = childStack( + source = navigationProvider.getOrCreateTyped(), + serializer = AppRoute.serializer(), + initialConfiguration = getInitialRoute(), + handleBackButton = false, + childFactory = ::child, + ) + + init { + backHandler.register(backCallback) + + lifecycle.doOnDestroy { + childFactory.doOnDestroy() + } + } + + private fun getInitialRoute(): AppRoute = AppRoute.Initial + + private fun child(route: AppRoute, context: ComponentContext): Child { + return childFactory.createChild(route, { childByContext(context) }) + } + + @AssistedFactory + interface Factory : RoutingComponent.Factory { + override fun create(context: AppComponentContext): DefaultRoutingComponent + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..8b3c22a7f9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -0,0 +1,194 @@ +package com.tangem.tap.routing.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.feature.referral.ReferralFragment +import com.tangem.feature.swap.presentation.SwapFragment +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.features.details.DetailsFeatureToggles +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter +import com.tangem.features.send.api.navigation.SendRouter +import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.tester.api.TesterRouter +import com.tangem.features.tokendetails.navigation.TokenDetailsRouter +import com.tangem.features.wallet.navigation.WalletRouter +import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment +import com.tangem.tap.features.details.ui.appcurrency.AppCurrencySelectorFragment +import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment +import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment +import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment +import com.tangem.tap.features.details.ui.details.DetailsFragment +import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment +import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment +import com.tangem.tap.features.details.ui.walletconnect.WalletConnectFragment +import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment +import com.tangem.tap.features.home.HomeFragment +import com.tangem.tap.features.main.ui.ModalNotificationBottomSheetFragment +import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment +import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment +import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment +import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment +import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment +import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment +import com.tangem.tap.features.welcome.ui.WelcomeFragment +import com.tangem.tap.routing.RoutingComponent.Child +import com.tangem.utils.Provider +import dagger.hilt.android.scopes.ActivityScoped +import java.util.WeakHashMap +import javax.inject.Inject + +@ActivityScoped +@Suppress("LongParameterList") +internal class ChildFactory @Inject constructor( + private val detailsComponentFactory: DetailsComponent.Factory, + private val walletSettingsComponentFactory: WalletSettingsComponent.Factory, + private val disclaimerComponentFactory: DisclaimerComponent.Factory, + private val sendRouter: SendRouter, + private val tokenDetailsRouter: TokenDetailsRouter, + private val walletRouter: WalletRouter, + private val qrScanningRouter: QrScanningRouter, + private val stakingRouter: StakingRouter, + private val testerRouter: TesterRouter, + private val detailsFeatureToggles: DetailsFeatureToggles, + private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, + private val pushNotificationRouter: PushNotificationsRouter, +) { + + @Suppress("LongMethod", "CyclomaticComplexMethod") + fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { + componentContexts[route] = contextFactory(route) + + return when (route) { + is AppRoute.Initial -> { + Child.Initial + } + is AppRoute.AccessCodeRecovery -> { + route.asFragmentChild(Provider { AccessCodeRecoveryFragment() }) + } + is AppRoute.AddCustomToken -> { + route.asFragmentChild(Provider { AddCustomTokenFragment() }) + } + is AppRoute.AppCurrencySelector -> { + route.asFragmentChild(Provider { AppCurrencySelectorFragment() }) + } + is AppRoute.ModalNotification -> { + route.asFragmentChild(Provider { ModalNotificationBottomSheetFragment() }) + } + is AppRoute.SaveWallet -> { + route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() }) + } + is AppRoute.Send -> { + route.asFragmentChild(Provider { sendRouter.getEntryFragment() }) + } + is AppRoute.AppSettings -> { + route.asFragmentChild(Provider { AppSettingsFragment() }) + } + is AppRoute.CardSettings -> { + route.asFragmentChild(Provider { CardSettingsFragment() }) + } + is AppRoute.Details -> { + if (detailsFeatureToggles.isRedesignEnabled) { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = DetailsComponent.Params(route.userWalletId), + componentFactory = detailsComponentFactory, + ) + } else { + route.asFragmentChild(Provider { DetailsFragment() }) + } + } + is AppRoute.DetailsSecurity -> { + route.asFragmentChild(Provider { SecurityModeFragment() }) + } + is AppRoute.Disclaimer -> { + if (pushNotificationsFeatureToggles.isPushNotificationsEnabled) { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = DisclaimerComponent.Params(route.isTosAccepted), + componentFactory = disclaimerComponentFactory, + ) + } else { + route.asFragmentChild(Provider { DisclaimerFragment() }) + } + } + is AppRoute.Home -> { + route.asFragmentChild(Provider { HomeFragment() }) + } + is AppRoute.ManageTokens -> { + route.asFragmentChild(Provider { TokensListFragment() }) + } + is AppRoute.OnboardingNote -> { + route.asFragmentChild(Provider { OnboardingNoteFragment() }) + } + is AppRoute.OnboardingOther -> { + route.asFragmentChild(Provider { OnboardingOtherCardsFragment() }) + } + is AppRoute.OnboardingTwins -> { + route.asFragmentChild(Provider { OnboardingTwinsFragment() }) + } + is AppRoute.OnboardingWallet -> { + route.asFragmentChild(Provider { OnboardingWalletFragment() }) + } + is AppRoute.QrScanning -> { + route.asFragmentChild(Provider { qrScanningRouter.getEntryFragment() }) + } + is AppRoute.ReferralProgram -> { + route.asFragmentChild(Provider { ReferralFragment() }) + } + is AppRoute.ResetToFactory -> { + route.asFragmentChild(Provider { ResetCardFragment() }) + } + is AppRoute.Swap -> { + route.asFragmentChild(Provider { SwapFragment() }) + } + is AppRoute.Wallet -> { + route.asFragmentChild(Provider { walletRouter.getEntryFragment() }) + } + is AppRoute.WalletConnectSessions -> { + route.asFragmentChild(Provider { WalletConnectFragment() }) + } + is AppRoute.CurrencyDetails -> { + route.asFragmentChild(Provider { tokenDetailsRouter.getEntryFragment() }) + } + is AppRoute.Welcome -> { + route.asFragmentChild(Provider { WelcomeFragment() }) + } + is AppRoute.TesterMenu -> { + Child.LegacyIntent(testerRouter.getEntryIntent()) + } + is AppRoute.Staking -> { + route.asFragmentChild(Provider { stakingRouter.getEntryFragment() }) + } + is AppRoute.PushNotification -> { + route.asFragmentChild(Provider { pushNotificationRouter.entryFragment() }) + } + is AppRoute.WalletSettings -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = WalletSettingsComponent.Params(route.userWalletId), + componentFactory = walletSettingsComponentFactory, + ) + } + } + } + + fun doOnDestroy() { + componentContexts.clear() + } + + private fun contextProvider( + appRoute: AppRoute, + contextFactory: (route: AppRoute) -> AppComponentContext, + ): Provider = Provider { + componentContexts.getOrPut(appRoute) { contextFactory(appRoute) } + } + + private companion object { + + val componentContexts = WeakHashMap() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt new file mode 100644 index 0000000000..bdade79e4e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt @@ -0,0 +1,40 @@ +package com.tangem.tap.routing.utils + +import androidx.fragment.app.Fragment +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.RouteBundleParams +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.tap.DecomposeFragment +import com.tangem.tap.routing.RoutingComponent.Child +import com.tangem.utils.Provider + +internal fun AppRoute.asFragmentChild(fragmentProvider: Provider): Child { + val provider = Provider { + val bundle = (this as? RouteBundleParams)?.getBundle() + + fragmentProvider().apply { + arguments = bundle + } + } + + return Child.LegacyFragment(path, provider) +} + +internal fun > AppRoute.asComponentChild( + contextProvider: Provider, + params: P, + componentFactory: F, +): Child { + val fragmentProvider = Provider { + DecomposeFragment.newInstance( + tag = path, + contextProvider = contextProvider, + params = params, + componentFactory = componentFactory, + ) + } + + return Child.LegacyFragment(path, fragmentProvider) +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_scan_fails.xml b/app/src/main/res/layout/dialog_scan_fails.xml new file mode 100644 index 0000000000..dd5924f8c5 --- /dev/null +++ b/app/src/main/res/layout/dialog_scan_fails.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/values.xml b/app/src/main/res/values/values.xml new file mode 100644 index 0000000000..5bbea5d78d --- /dev/null +++ b/app/src/main/res/values/values.xml @@ -0,0 +1,4 @@ + + + false + \ No newline at end of file diff --git a/app/src/mocked/res/values/values.xml b/app/src/mocked/res/values/values.xml new file mode 100644 index 0000000000..8cc3a85358 --- /dev/null +++ b/app/src/mocked/res/values/values.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/common/routing/.gitignore b/common/routing/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/common/routing/.gitignore @@ -0,0 +1 @@ +/build diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts new file mode 100644 index 0000000000..ae4552a1f0 --- /dev/null +++ b/common/routing/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.common.routing" +} + +dependencies { + /* Core */ + implementation(projects.core.decompose) + + /* Domain */ + implementation(projects.domain.qrScanning.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.staking.models) + + /* Libs - Other */ + api(deps.kotlin.serialization) + implementation(deps.androidx.core.ktx) + implementation(deps.timber) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..8bd5b8a2c1 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -0,0 +1,208 @@ +package com.tangem.common.routing + +import android.os.Bundle +import com.tangem.common.routing.bundle.RouteBundleParams +import com.tangem.common.routing.bundle.bundle +import com.tangem.common.routing.entity.SerializableIntent +import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +sealed class AppRoute(val path: String) : Route { + + @Serializable + data object Initial : AppRoute(path = "/initial") + + @Serializable + data object Home : AppRoute(path = "/home") + + @Serializable + data class Welcome( + val intent: SerializableIntent? = null, + ) : AppRoute(path = "/welcome"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val INITIAL_INTENT_KEY = "intent" + } + } + + @Serializable + data class Disclaimer( + val isTosAccepted: Boolean, + ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val IS_TOS_ACCEPTED_KEY = "isTosAccepted" + } + } + + @Serializable + data object OnboardingNote : AppRoute(path = "/onboarding/note") + + @Serializable + data class OnboardingWallet( + val canSkipBackup: Boolean = true, + ) : AppRoute(path = "/onboarding/wallet${if (canSkipBackup) "/skippable" else ""}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val CAN_SKIP_BACKUP_KEY = "canSkipBackup" + } + } + + @Serializable + data object OnboardingTwins : AppRoute(path = "/onboarding/twins") + + @Serializable + data object OnboardingOther : AppRoute(path = "/onboarding/other") + + @Serializable + data object Wallet : AppRoute(path = "/wallet") + + @Serializable + data class CurrencyDetails( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + const val CRYPTO_CURRENCY_KEY = "currency" + } + } + + @Serializable + data class Send( + val userWalletId: UserWalletId, + val currency: CryptoCurrency, + val transactionId: String? = null, + val amount: String? = null, + val tag: String? = null, + val destinationAddress: String? = null, + ) : AppRoute( + path = "/send/${userWalletId.stringValue}/${currency.id.value}?" + + "&$transactionId" + + "&$amount" + + "&$tag" + + "&$destinationAddress", + ), + RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + const val CRYPTO_CURRENCY_KEY = "currency" + const val TRANSACTION_ID_KEY = "transactionId" + const val AMOUNT_KEY = "amount" + const val TAG_KEY = "tag" + const val DESTINATION_ADDRESS_KEY = "destinationAddress" + } + } + + @Serializable + data class Details( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/details/${userWalletId.stringValue}") + + @Serializable + data object DetailsSecurity : AppRoute(path = "/details/security") + + @Serializable + data object CardSettings : AppRoute(path = "/card_settings") + + @Serializable + data object AppSettings : AppRoute(path = "/app_settings") + + @Serializable + data object ResetToFactory : AppRoute(path = "/reset_to_factory") + + @Serializable + data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery") + + @Serializable + data object ManageTokens : AppRoute(path = "/manage_tokens") + + @Serializable + data object AddCustomToken : AppRoute(path = "/add_custom_token") + + @Serializable + data object WalletConnectSessions : AppRoute(path = "/wallet_connect_sessions") + + @Serializable + data class QrScanning( + val source: SourceType, + val networkName: String? = null, + ) : AppRoute(path = "/$source/qr_scanning${if (networkName != null) "/$networkName" else ""}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val SOURCE_KEY = "source" + const val NETWORK_KEY = "networkName" + } + } + + @Serializable + data object ReferralProgram : AppRoute(path = "/referral_program") + + @Serializable + data class Swap( + val currency: CryptoCurrency, + ) : AppRoute(path = "/swap/${currency.id.value}"), RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val CURRENCY_BUNDLE_KEY = "currency" + } + } + + @Serializable + data object TesterMenu : AppRoute(path = "/tester_menu") + + @Serializable + data object SaveWallet : AppRoute(path = "/save_wallet") + + @Serializable + data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") + + @Serializable + data object ModalNotification : AppRoute(path = "/modal_notification") + + @Serializable + data class Staking( + val userWalletId: UserWalletId, + val cryptoCurrencyId: CryptoCurrency.ID, + val yield: Yield, + ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/${yield.id}"), + RouteBundleParams { + + override fun getBundle(): Bundle = bundle(serializer()) + + companion object { + const val USER_WALLET_ID_KEY = "userWalletId" + const val CRYPTO_CURRENCY_ID_KEY = "cryptoCurrencyId" + const val YIELD_KEY = "yield" + } + } + + @Serializable + data object PushNotification : AppRoute(path = "/push_notification") + + @Serializable + data class WalletSettings( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}") +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt new file mode 100644 index 0000000000..cb0f21a793 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt @@ -0,0 +1,58 @@ +package com.tangem.common.routing + +import kotlin.reflect.KClass + +/** + * Interface for a router in the application. + * It provides methods for navigating through the application. + * + * Same as [com.tangem.core.decompose.navigation.Router] but without Decompose dependency. + * + * ***Must be removed after Decompose migration.*** + */ +interface AppRouter { + + /** + * The current navigation stack. + */ + val stack: List + + /** + * Pushes a new route to the navigation stack. + * + * @param route The route to push. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun push(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Replaces ***all*** routes in the navigation stack with the specified [routes]. + * + * @param routes The routes to replace the current stack with. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops the top route from the navigation stack. + * + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops routes from the navigation stack until the specified [route] is found. + * + * @param route The route to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun popTo(route: AppRoute, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops routes from the navigation stack until the ***first*** specified [routeClass] is found. + * + * @param routeClass The route class to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit = {}) +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleDecoder.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleDecoder.kt new file mode 100644 index 0000000000..6a51004489 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleDecoder.kt @@ -0,0 +1,108 @@ +package com.tangem.common.routing.bundle + +import android.os.Bundle +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.modules.SerializersModule + +@ExperimentalSerializationApi +internal class BundleDecoder( + private val bundle: Bundle, + private val elementsCount: Int = -1, + private val isInitializer: Boolean = true, + override val serializersModule: SerializersModule, +) : AbstractDecoder() { + + private var index = -1 + private var elementKey: String? = null + + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + if (++index >= elementsCount) { + return CompositeDecoder.DECODE_DONE + } + + elementKey = descriptor.getElementName(index) + return index + } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { + val b = if (isInitializer) { + bundle + } else { + requireNotNull(bundle.getBundle(elementKey)) { + "Bundle is missing for key $elementKey while decoding" + } + } + + val count = when (descriptor.kind) { + StructureKind.MAP, + StructureKind.LIST, + -> b.getInt("\$size") + else -> descriptor.elementsCount + } + + return BundleDecoder( + bundle = b, + elementsCount = count, + isInitializer = false, + serializersModule = serializersModule, + ) + } + + override fun endStructure(descriptor: SerialDescriptor) { + /* no-op */ + } + + override fun decodeBoolean(): Boolean { + return bundle.getBoolean(elementKey) + } + + override fun decodeByte(): Byte { + return bundle.getByte(elementKey) + } + + override fun decodeChar(): Char { + return bundle.getChar(elementKey) + } + + override fun decodeDouble(): Double { + return bundle.getDouble(elementKey) + } + + override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { + return bundle.getInt(elementKey) + } + + override fun decodeFloat(): Float { + return bundle.getFloat(elementKey) + } + + override fun decodeInt(): Int { + return bundle.getInt(elementKey) + } + + override fun decodeLong(): Long { + return bundle.getLong(elementKey) + } + + override fun decodeNotNullMark(): Boolean { + return bundle.containsKey(elementKey) + } + + override fun decodeNull(): Nothing? { + return null + } + + override fun decodeShort(): Short { + return bundle.getShort(elementKey) + } + + override fun decodeString(): String { + return requireNotNull(bundle.getString(elementKey)) { + "String is missing for key $elementKey while decoding" + } + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleEncoder.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleEncoder.kt new file mode 100644 index 0000000000..e0be7b8aa3 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleEncoder.kt @@ -0,0 +1,107 @@ +package com.tangem.common.routing.bundle + +import android.os.Bundle +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.encoding.AbstractEncoder +import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.modules.SerializersModule + +@ExperimentalSerializationApi +internal class BundleEncoder( + private val bundle: Bundle, + private val parentBundle: Bundle? = null, + private val keyInParent: String? = null, + private val isInitializer: Boolean = true, + override val serializersModule: SerializersModule, +) : AbstractEncoder() { + + private var elementKey: String? = null + + override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { + elementKey = descriptor.getElementName(index) + return super.encodeElement(descriptor, index) + } + + override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { + return if (isInitializer) { + BundleEncoder( + bundle = bundle, + parentBundle = null, + keyInParent = elementKey, + isInitializer = false, + serializersModule = serializersModule, + ) + } else { + BundleEncoder( + bundle = Bundle(), + parentBundle = bundle, + keyInParent = elementKey, + isInitializer = false, + serializersModule = serializersModule, + ) + } + } + + override fun endStructure(descriptor: SerialDescriptor) { + if (descriptor.kind in arrayOf(StructureKind.LIST, StructureKind.MAP)) { + val size = elementKey?.toIntOrNull()?.let { it + 1 } ?: 0 + bundle.putInt("\$size", size) + } + + if (keyInParent.isNullOrBlank()) { + return + } + + parentBundle?.putBundle(keyInParent, bundle) + } + + override fun encodeBoolean(value: Boolean) { + bundle.putBoolean(elementKey, value) + } + + override fun encodeByte(value: Byte) { + bundle.putByte(elementKey, value) + } + + override fun encodeChar(value: Char) { + bundle.putChar(elementKey, value) + } + + override fun encodeDouble(value: Double) { + bundle.putDouble(elementKey, value) + } + + override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) { + bundle.putInt(elementKey, index) + } + + override fun encodeFloat(value: Float) { + bundle.putFloat(elementKey, value) + } + + override fun encodeInt(value: Int) { + bundle.putInt(elementKey, value) + } + + override fun encodeLong(value: Long) { + bundle.putLong(elementKey, value) + } + + override fun encodeNull() { + /* no-op */ + } + + override fun encodeShort(value: Short) { + bundle.putShort(elementKey, value) + } + + override fun encodeString(value: String) { + bundle.putString(elementKey, value) + } + + override fun encodeNotNullMark() { + /* no-op */ + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleUtils.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleUtils.kt new file mode 100644 index 0000000000..4c638fde65 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/BundleUtils.kt @@ -0,0 +1,60 @@ +package com.tangem.common.routing.bundle + +import android.os.Bundle +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.modules.EmptySerializersModule +import kotlinx.serialization.modules.SerializersModule + +val defaultSerializersModule: SerializersModule = EmptySerializersModule() + +/** + * Deserialize this bundle into an object of type [T]. + * + * @receiver [Bundle] to deserialize. + * @param deserializer [DeserializationStrategy] of the [T] class. + * + * @return Object of type T deserialized from bundle. + */ +@OptIn(ExperimentalSerializationApi::class) +fun Bundle.unbundle( + deserializer: DeserializationStrategy, + serializersModule: SerializersModule = defaultSerializersModule, +): T { + val decoder = BundleDecoder( + bundle = this, + elementsCount = -1, + isInitializer = true, + serializersModule = serializersModule, + ) + + return deserializer.deserialize(decoder) +} + +/** + * Serialize [T] into a bundle. + * + * @receiver Object to serialize. + * @param serializer [SerializationStrategy] of the [T] class. + * + * @return bundle serialized from value + */ +@OptIn(ExperimentalSerializationApi::class) +fun T.bundle( + serializer: SerializationStrategy, + serializersModule: SerializersModule = defaultSerializersModule, +): Bundle { + val bundle = Bundle(serializer.descriptor.elementsCount) + val encoder = BundleEncoder( + bundle = bundle, + parentBundle = null, + keyInParent = null, + isInitializer = true, + serializersModule = serializersModule, + ) + + serializer.serialize(encoder, value = this) + + return bundle +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/RouteBundleParams.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/RouteBundleParams.kt new file mode 100644 index 0000000000..12ae0795d3 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/bundle/RouteBundleParams.kt @@ -0,0 +1,8 @@ +package com.tangem.common.routing.bundle + +import android.os.Bundle + +interface RouteBundleParams { + + fun getBundle(): Bundle +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableBundle.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableBundle.kt new file mode 100644 index 0000000000..fcac1b0422 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableBundle.kt @@ -0,0 +1,24 @@ +package com.tangem.common.routing.entity + +import android.os.Bundle +import kotlinx.serialization.Serializable + +@Serializable +data class SerializableBundle( + val map: Map, +) { + + constructor(bundle: Bundle) : this( + map = bundle.keySet().mapNotNull { key -> + bundle.getString(key)?.let { key to it } + }.toMap(), + ) + + fun toBundle(): Bundle { + return Bundle().apply { + map.forEach { (key, value) -> + putString(key, value) + } + } + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt new file mode 100644 index 0000000000..38ef6f2096 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/entity/SerializableIntent.kt @@ -0,0 +1,51 @@ +package com.tangem.common.routing.entity + +import android.content.ComponentName +import android.content.Intent +import android.net.Uri +import kotlinx.serialization.Serializable + +@Serializable +data class SerializableIntent( + val action: String?, + val dataString: String?, + val categories: Set?, + val type: String?, + val packageValue: String?, + val component: String?, + val flags: Int, + val extras: SerializableBundle?, +) { + + constructor(intent: Intent) : this( + action = intent.action, + dataString = intent.dataString, + categories = intent.categories, + type = intent.type, + packageValue = intent.`package`, + component = intent.component?.flattenToString(), + flags = intent.flags, + extras = intent.extras?.let(::SerializableBundle), + ) + + fun toIntent(): Intent { + val intent = Intent() + + intent.action = action + intent.setDataAndType( + dataString?.let { Uri.parse(it) }, + type, + ) + categories?.let { categories -> + for (category in categories) { + intent.addCategory(category) + } + } + intent.`package` = packageValue + intent.component = component?.let { ComponentName.unflattenFromString(it) } + intent.flags = flags + extras?.let { intent.putExtras(it.toBundle()) } + + return intent + } +} \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/utils/AppRouterExt.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/utils/AppRouterExt.kt new file mode 100644 index 0000000000..1bc85f941f --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/utils/AppRouterExt.kt @@ -0,0 +1,16 @@ +package com.tangem.common.routing.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter + +/** + * Pops routes from the navigation stack until the specified route [R] is found. + * + * ***Must be removed after Decompose migration.*** + * + * @param R The route to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ +inline fun AppRouter.popTo(noinline onComplete: (isSuccess: Boolean) -> Unit = {}) { + popTo(R::class, onComplete) +} \ No newline at end of file diff --git a/common/src/main/java/com/tangem/common/Strings.kt b/common/src/main/java/com/tangem/common/Strings.kt deleted file mode 100644 index 943b0de614..0000000000 --- a/common/src/main/java/com/tangem/common/Strings.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.common - -object Strings { - - const val STARS = "\u2217\u2217\u2217" -} \ No newline at end of file diff --git a/features/manage-tokens/.gitignore b/common/ui-charts/.gitignore similarity index 100% rename from features/manage-tokens/.gitignore rename to common/ui-charts/.gitignore diff --git a/common/ui-charts/build.gradle.kts b/common/ui-charts/build.gradle.kts new file mode 100644 index 0000000000..319c15eebc --- /dev/null +++ b/common/ui-charts/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.common.ui.charts" +} + +dependencies { + /** Project - Core */ + implementation(projects.core.ui) + + /** Compose */ + implementation(deps.tangem.vico.core) + implementation(deps.tangem.vico.compose) + implementation(deps.tangem.vico.compose.m3) + + implementation(deps.lifecycle.compose) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt new file mode 100644 index 0000000000..9bbaeaa48b --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -0,0 +1,402 @@ +package com.tangem.common.ui.charts + +import android.content.res.Configuration +import androidx.annotation.FloatRange +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontSynthesis +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.font.resolveAsTypeface +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.* +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis +import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis +import com.patrykandpatrick.vico.compose.common.of +import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout +import com.patrykandpatrick.vico.core.cartesian.Zoom +import com.patrykandpatrick.vico.core.cartesian.axis.* +import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener +import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget +import com.patrykandpatrick.vico.core.common.Dimensions +import com.patrykandpatrick.vico.core.common.component.LineComponent +import com.patrykandpatrick.vico.core.common.shape.Shape +import com.tangem.common.ui.charts.layer.rememberMarketChartLayer +import com.tangem.common.ui.charts.marker.rememberTangemChartMarker +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider +import com.tangem.common.ui.charts.state.* +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.DateTimeFormatters +import com.tangem.core.ui.utils.toTimeFormat +import kotlinx.coroutines.launch +import java.math.BigDecimal +import java.math.RoundingMode + +private const val GUIDELINES_COUNT = 3 + +/** + * MarketChart ui component for representing coin prices. + * + * @param modifier The modifier to be applied to the chart. + * @param state The state of the Market Chart, which includes data and look of the chart. + * @param splitChartSegmentColor The color of the grayed by marker chart segment. + * @param backgroundSplitChartSegmentColorAlpha The alpha of the background the [splitChartSegmentColor] + * @param backgroundColorAlpha The alpha of the background color of the chart. + * @param noChartContent A composable function that defines the content to be displayed when there is no data to display. + */ +@Composable +fun MarketChart( + modifier: Modifier = Modifier, + state: MarketChartState = rememberMarketChartState(), + splitChartSegmentColor: Color, + @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float, + @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float, + noChartContent: @Composable BoxScope.() -> Unit, +) { + var canvasWidth by remember { mutableIntStateOf(0) } + var canvasHeight by remember { mutableIntStateOf(0) } + + val layer = rememberLayerFromState( + state = state, + splitChartSegmentColor = splitChartSegmentColor, + backgroundColorAlpha = backgroundColorAlpha, + backgroundSplitChartSegmentColorAlpha = backgroundSplitChartSegmentColorAlpha, + canvasHeight = canvasHeight, + ) + val chart = rememberCartesianChart( + layer, + startAxis = rememberMarketChartStartAxis( + yValueFormatter = state.yValueFormatter, + ), + bottomAxis = rememberMarketChartBottomAxis( + xValueFormatter = state.xValueFormatter, + ), + ) + val marker = rememberTangemChartMarker( + color = state.chartColor, + innerCircleColor = Color.White, + ) + val density = LocalDensity.current + + CartesianChartHost( + modifier = modifier.onGloballyPositioned { + with(density) { + canvasWidth = it.size.width + canvasHeight = if (it.size.height != 0) { + // FIXME get height bounded to min max chart points + it.size.height - 20.dp.toPx().toInt() - 27.dp.toPx().toInt() + } else { + 0 + } + } + }, + chart = chart, + modelProducer = state.modelProducer, + scrollState = rememberVicoScrollState(scrollEnabled = false), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + markerVisibilityListener = state.rememberMarketVisibilityListener(canvasWidth = canvasWidth), + diffAnimationSpec = null, + marker = marker, + placeholder = noChartContent, + ) +} + +@Composable +private fun MarketChartState.rememberMarketVisibilityListener(canvasWidth: Int): CartesianMarkerVisibilityListener { + val state = this + return remember(state.markerVisibilityListener, canvasWidth) { + val maxCanvasXFloat = canvasWidth.toFloat().takeIf { it != 0f } + + object : CartesianMarkerVisibilityListener { + override fun onShown(marker: CartesianMarker, targets: List) { + state.stopDrawingAnimation() + val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX + + state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } + state.markerVisibilityListener.onShown(marker, targets) + } + + override fun onHidden(marker: CartesianMarker) { + state.markerFraction = null + state.markerVisibilityListener.onHidden(marker) + } + + override fun onUpdated(marker: CartesianMarker, targets: List) { + val xCanvas = (targets[0] as LineCartesianLayerMarkerTarget).canvasX + + state.markerFraction = maxCanvasXFloat?.let { xCanvas / it } + state.markerVisibilityListener.onUpdated(marker, targets) + } + } + } +} + +@Composable +private fun rememberLayerFromState( + state: MarketChartState, + splitChartSegmentColor: Color, + @FloatRange(from = 0.0, to = 1.0) backgroundColorAlpha: Float, + @FloatRange(from = 0.0, to = 1.0) backgroundSplitChartSegmentColorAlpha: Float, + canvasHeight: Int, +): LineCartesianLayer { + return rememberMarketChartLayer( + lineColor = state.chartColor, + backgroundLineColor = state.chartColor.copy(alpha = backgroundColorAlpha), + secondLineColor = splitChartSegmentColor, + backgroundSecondLineColor = splitChartSegmentColor.copy(alpha = backgroundSplitChartSegmentColorAlpha), + secondColorOnTheRightSide = state.markerHighlightRightSide.not(), + startDrawingAnimation = state.startDrawingAnimationState, + markerFraction = state.markerFraction, + axisValueOverrider = AxisValueOverrider.adaptiveYValues(yFraction = 1.2f, round = true), // FIXME ? + canvasHeight = canvasHeight, + ) +} + +@Composable +private fun rememberMarketChartStartAxis( + yValueFormatter: CartesianValueFormatter, +): VerticalAxis { + return rememberCustomStartAxis( + axis = null, + tick = null, + guideline = null, + labelGuideline = rememberChartAxisGuidelineComponent( + color = TangemTheme.colors.icon.inactive.copy(alpha = 0.12f), + ), + label = rememberAxisLabelComponent( + color = TangemTheme.colors.text.tertiary, + background = null, + padding = Dimensions.of( + start = TangemTheme.dimens.spacing4, + end = TangemTheme.dimens.spacing4, + ), + textSize = TangemTheme.typography.caption2.fontSize, + typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(), + ), + horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside, + verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center, + itemPlacer = AxisItemPlacer.Vertical.count({ GUIDELINES_COUNT }, false), + valueFormatter = yValueFormatter, + ) +} + +@Composable +fun rememberMarketChartBottomAxis( + xValueFormatter: CartesianValueFormatter, +): HorizontalAxis { + return rememberBottomAxis( + label = rememberAxisLabelComponent( + color = TangemTheme.colors.text.tertiary, + textSize = TangemTheme.typography.caption2.fontSize, + padding = Dimensions.of(top = TangemTheme.dimens.spacing20), + typeface = TangemTheme.typography.caption2.toGraphicsTypeFace(), + ), + tick = null, + axis = null, + guideline = null, + sizeConstraint = BaseAxis.SizeConstraint.Exact(sizeDp = 37f), // FIXME ? + itemPlacer = remember { + AxisItemPlacer.Horizontal.default( + spacing = 25, // FIXME ? + offset = 60, // FIXME ? + shiftExtremeTicks = false, + addExtremeLabelPadding = false, + ) + }, + valueFormatter = xValueFormatter, + ) +} + +@Composable +private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent { + return rememberAxisGuidelineComponent( + color = color, + shape = Shape.Rectangle, + margins = Dimensions( + startDp = TangemTheme.dimens.spacing4.value, + endDp = TangemTheme.dimens.spacing4.value, + topDp = 0f, + bottomDp = 0f, + ), + thickness = TangemTheme.dimens.size2, + ) +} + +@Composable +internal fun TextStyle.toGraphicsTypeFace(): android.graphics.Typeface { + val resolver = LocalFontFamilyResolver.current + return remember(resolver, this) { + resolver.resolveAsTypeface( + fontFamily = this.fontFamily, + fontWeight = this.fontWeight ?: FontWeight.Normal, + fontStyle = this.fontStyle ?: FontStyle.Normal, + fontSynthesis = this.fontSynthesis ?: FontSynthesis.All, + ) + }.value +} + +// region Preview + +@Suppress("LongMethod") +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun MarketChartPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { + val y = previewData.second + val x = previewData.first + + val dataProducer = remember { + MarketChartDataProducer.build { + chartLook = MarketChartLook( + type = MarketChartLook.Type.Growing, + markerHighlightRightSide = true, + animationOnDataChange = true, + ) + } + } + + LaunchedEffect(key1 = Unit) { + dataProducer.runTransactionSuspend { + chartData = MarketChartData.Data( + x = x, + y = y, + ) + updateLook { + it.copy( + xAxisFormatter = { value -> + value.toLong().toTimeFormat(DateTimeFormatters.dateMMMMd) + }, + yAxisFormatter = { value -> + value.setScale(3, RoundingMode.HALF_UP).toPlainString() + }, + ) + } + } + } + var markerPoint by remember { + mutableStateOf(Pair(null, null)) + } + + val coroutineScope = rememberCoroutineScope() + val look by dataProducer.lookState.collectAsState() + + TangemThemePreview { + val growingColor = TangemTheme.colors.icon.accent + val fallingColor = TangemTheme.colors.icon.warning + + val chartState = rememberMarketChartState( + dataProducer = dataProducer, + onMarkerShown = { x, y -> + markerPoint = Pair(x, y) + }, + colorMapper = { + when (it) { + MarketChartLook.Type.Growing -> growingColor + MarketChartLook.Type.Falling -> fallingColor + } + }, + ) + + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .fillMaxWidth(), + ) { + Text(text = "Point: ${markerPoint.first}, ${markerPoint.second}") + + MarketChart( + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.tertiary) + .height(173.dp), + state = chartState, + splitChartSegmentColor = TangemTheme.colors.icon.inactive, + backgroundSplitChartSegmentColorAlpha = 0.24f, + backgroundColorAlpha = 0.24f, + noChartContent = { }, + ) + SpacerH16() + + Button(onClick = { chartState.startDrawingAnimation() }) { + Text("Start drawing animation") + } + Button( + onClick = { + dataProducer.runTransaction { + updateLook { + it.copy(markerHighlightRightSide = !it.markerHighlightRightSide) + } + } + }, + ) { + Text( + text = "Change marker highlight side", + ) + } + Button(onClick = { + coroutineScope.launch { + dataProducer.runTransactionSuspend { + updateData { + MarketChartData.Data( + x = it.x, + y = it.y.reversed(), + ) + } + } + } + },) { + Text("Change Data") + } + Button(onClick = { + dataProducer.runTransaction { + updateLook { it.copy(animationOnDataChange = it.animationOnDataChange.not()) } + } + },) { + Text("Change animationOnDataChange = ${look.animationOnDataChange}") + } + + Button(onClick = { + dataProducer.runTransaction { + updateLook { + it.copy( + type = if (it.type == MarketChartLook.Type.Growing) { + MarketChartLook.Type.Falling + } else { + MarketChartLook.Type.Growing + }, + ) + } + } + },) { + Text("Change color type") + } + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt new file mode 100644 index 0000000000..d9902450d3 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChartMini.kt @@ -0,0 +1,111 @@ +package com.tangem.common.ui.charts + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.* +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec +import com.patrykandpatrick.vico.compose.common.shader.BrushShader +import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout +import com.patrykandpatrick.vico.core.cartesian.Zoom +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.common.shader.ColorShader +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlin.random.Random + +@Composable +fun MarketChartMini( + rawData: MarketChartRawData, + modifier: Modifier = Modifier, + type: MarketChartLook.Type = MarketChartLook.Type.Growing, + growingColor: Color = TangemTheme.colors.icon.accent, + fallingColor: Color = TangemTheme.colors.icon.warning, +) { + val model = remember(rawData) { + CartesianChartModel(LineCartesianLayerModel.build { series(rawData.y) }) + } + + val lineColor = when (type) { + MarketChartLook.Type.Growing -> growingColor + MarketChartLook.Type.Falling -> fallingColor + } + + val lineSpec = rememberLineSpec( + shader = ColorShader(lineColor.toArgb()), + thickness = 1.dp, + backgroundShader = BrushShader( + brush = Brush.verticalGradient( + colors = listOf(lineColor.copy(alpha = 0.22f), Color.Transparent), + ), + ), + ) + + val layer = rememberLineCartesianLayer(listOf(lineSpec)) + val chart = rememberCartesianChart(layer) + + CartesianChartHost( + modifier = modifier, + chart = chart, + model = model, + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + scrollState = rememberVicoScrollState(scrollEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + ) +} + +// region Preview + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + val data = MarketChartRawData( + x = List(20) { Random.nextFloat() }, + y = List(20) { Random.nextFloat() }, + ) + + TangemThemePreview { + Column { + MarketChartMini(rawData = data, type = MarketChartLook.Type.Growing) + SpacerH16() + MarketChartMini(rawData = data, type = MarketChartLook.Type.Falling) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewColumn() { + val data = MarketChartRawData( + x = List(20) { Random.nextFloat() }, + y = List(20) { Random.nextFloat() }, + ) + + TangemThemePreview { + LazyColumn { + items(100) { + MarketChartMini( + rawData = data, + type = if (it % 3 == 0) MarketChartLook.Type.Growing else MarketChartLook.Type.Falling, + ) + } + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt new file mode 100644 index 0000000000..b224a1838d --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/layer/MarketChartLayer.kt @@ -0,0 +1,329 @@ +package com.tangem.common.ui.charts.layer + +import android.content.res.Configuration +import androidx.annotation.FloatRange +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.fullWidth +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberSplitLineSpec +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState +import com.patrykandpatrick.vico.compose.common.shader.BrushShader +import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout +import com.patrykandpatrick.vico.core.cartesian.Zoom +import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer +import com.patrykandpatrick.vico.core.common.shader.ColorShader +import com.patrykandpatrick.vico.core.common.shader.DynamicShader +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import java.math.BigDecimal + +/** + * Creates and remembers a LineCartesianLayer for a chart with specific characteristics. + * + * @param lineColor The color of the main line in the chart. + * @param backgroundLineColor The color of the line's background. + * @param secondLineColor The color of the line for the second part of the chart. + * @param backgroundSecondLineColor The color of the line's background for the second part of the chart. + * @param startDrawingAnimation A mutable state that triggers the start of the drawing animation when set to true. + * @param axisValueOverrider An AxisValueOverrider that provides custom values for the axis. + * @param secondColorOnTheRightSide A boolean that determines if the second color should be on the right side of the chart. Default is false. + * @param markerFraction A float between 0.0 and 1.0 that represents the fraction of the chart where the marker is located. Default is null. + * + * @return A LineCartesianLayer that represents a layer in a chart with the specified characteristics. + */ +@Suppress("LongParameterList") +@Composable +internal fun rememberMarketChartLayer( + lineColor: Color, + backgroundLineColor: Color, + secondLineColor: Color, + backgroundSecondLineColor: Color, + startDrawingAnimation: MutableState, + axisValueOverrider: AxisValueOverrider, + secondColorOnTheRightSide: Boolean, + @FloatRange(from = 0.0, to = 1.0) markerFraction: Float?, + canvasHeight: Int, +): LineCartesianLayer { + var animationFraction: Float? by remember { mutableStateOf(null) } + + LaunchedEffect(startDrawingAnimation.value) { + animationFraction = null + if (startDrawingAnimation.value) { + animate( + initialValue = 0f, + targetValue = 1f, + animationSpec = tween(easing = LinearEasing, durationMillis = 1000), + ) { start, _ -> + if (start == 1f) { + animationFraction = null + startDrawingAnimation.value = false + } else { + animationFraction = start + } + } + } + } + + return rememberRawMarketChartLayer( + lineColor = lineColor, + backgroundLineColor = backgroundLineColor, + secondLineColor = secondLineColor, + backgroundSecondLineColor = backgroundSecondLineColor, + axisValueOverrider = axisValueOverrider, + secondColorOnTheRightSide = secondColorOnTheRightSide, + markerFraction = markerFraction, + animationFraction = animationFraction, + canvasHeight = canvasHeight, + ) +} + +@Suppress("LongParameterList") +@Composable +private fun rememberRawMarketChartLayer( + lineColor: Color, + backgroundLineColor: Color, + secondLineColor: Color, + backgroundSecondLineColor: Color, + axisValueOverrider: AxisValueOverrider, + canvasHeight: Int, + secondColorOnTheRightSide: Boolean = false, + @FloatRange(from = 0.0, to = 1.0) markerFraction: Float? = null, + @FloatRange(from = 0.0, to = 1.0) animationFraction: Float? = null, +): LineCartesianLayer { + val backgroundColorLineGradient = listOf(backgroundLineColor, Color.Transparent) + val backgroundSecondLineColorGradient = listOf(backgroundSecondLineColor, Color.Transparent) + + val markerSet = markerFraction != null + val animationRunning = animationFraction != null && animationFraction != 1f + + val layerColors = when { + !animationRunning && markerSet && secondColorOnTheRightSide -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + lineColorRight = secondLineColor, + backLineColorRight = backgroundSecondLineColorGradient, + ) + } + !animationRunning && markerSet && !secondColorOnTheRightSide -> { + LayerColors( + lineColor = secondLineColor, + backLineColor = backgroundSecondLineColorGradient, + lineColorRight = lineColor, + backLineColorRight = backgroundColorLineGradient, + ) + } + animationRunning -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + lineColorRight = Color.Transparent, + backLineColorRight = listOf(Color.Transparent, Color.Transparent), + ) + } + else -> { + LayerColors( + lineColor = lineColor, + backLineColor = backgroundColorLineGradient, + ) + } + } + + return rememberLayer( + fractionValue = animationFraction ?: markerFraction, + axisValueOverrider = axisValueOverrider, + layerColors = layerColors, + canvasHeight = canvasHeight, + ) +} + +private data class LayerColors( + val lineColor: Color, + val backLineColor: List, + val lineColorRight: Color? = null, + val backLineColorRight: List? = null, +) + +@Composable +private fun rememberLayer( + fractionValue: Float?, + axisValueOverrider: AxisValueOverrider, + layerColors: LayerColors, + canvasHeight: Int, +): LineCartesianLayer { + val endGradientColorPosition = if (canvasHeight != 0) { + canvasHeight * END_GRADIENT_COLOR_POSITION_PERCENTAGE + } else { + Float.POSITIVE_INFINITY + } + + return rememberLineCartesianLayer( + listOf( + if (layerColors.lineColorRight == null || layerColors.backLineColorRight == null || fractionValue == null) { + rememberLineSpec( + shader = remember(layerColors.lineColor) { ColorShader(color = layerColors.lineColor.toArgb()) }, + backgroundShader = remember(layerColors.backLineColor, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColor, + endY = endGradientColorPosition, + ), + ) + }, + ) + } else { + rememberSplitLineSpec( + shader = remember(layerColors.lineColor, layerColors.lineColorRight, fractionValue) { + DynamicShader.Companion.horizontalGradient( + colors = intArrayOf(layerColors.lineColor.toArgb(), layerColors.lineColorRight.toArgb()), + positions = floatArrayOf(fractionValue, fractionValue), + ) + }, + backgroundShaderFirst = remember(layerColors.backLineColor, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColor, + endY = endGradientColorPosition, + ), + ) + }, + backgroundShaderSecond = remember(layerColors.backLineColorRight, endGradientColorPosition) { + BrushShader( + brush = Brush.verticalGradient( + colors = layerColors.backLineColorRight, + endY = endGradientColorPosition, + ), + ) + }, + xSplitFraction = fractionValue, + ) + }, + ), + axisValueOverrider = axisValueOverrider, + ) +} + +private const val END_GRADIENT_COLOR_POSITION_PERCENTAGE = 0.9f + +// region Preview + +@Suppress("LongMethod") +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun LayerChartPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { + val y = previewData.second.map { it.toFloat() } + val x = List(y.size) { it.toFloat() } + val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }) + var lineColor by remember { + mutableStateOf(Color.Blue) + } + + TangemThemePreview { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + verticalArrangement = Arrangement.spacedBy(48.dp), + ) { + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), + secondLineColor = Color.Gray, + backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), + secondColorOnTheRightSide = true, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), + secondLineColor = Color.Gray, + backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), + markerFraction = 0.35f, + secondColorOnTheRightSide = true, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), + secondLineColor = Color.Gray, + backgroundSecondLineColor = Color.Gray.copy(alpha = 0.24f), + markerFraction = 0.35f, + secondColorOnTheRightSide = false, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberRawMarketChartLayer( + lineColor = lineColor, + backgroundLineColor = lineColor.copy(alpha = 0.24f), + secondLineColor = lineColor, + backgroundSecondLineColor = lineColor.copy(alpha = 0.24f), + markerFraction = 0.35f, + secondColorOnTheRightSide = true, + animationFraction = 0.7f, + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + canvasHeight = 495, + ), + ), + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + model = model, + ) + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt new file mode 100644 index 0000000000..e27ce63a70 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/marker/ChartMarker.kt @@ -0,0 +1,146 @@ +package com.tangem.common.ui.charts.marker + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost +import com.patrykandpatrick.vico.compose.cartesian.fullWidth +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer +import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineSpec +import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart +import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState +import com.patrykandpatrick.vico.compose.common.component.rememberLayeredComponent +import com.patrykandpatrick.vico.compose.common.component.rememberShapeComponent +import com.patrykandpatrick.vico.compose.common.component.rememberUnboundedLineComponent +import com.patrykandpatrick.vico.compose.common.of +import com.patrykandpatrick.vico.compose.common.shader.color +import com.patrykandpatrick.vico.compose.common.shape.dashed +import com.patrykandpatrick.vico.core.cartesian.* +import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModel +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.DefaultCartesianMarker +import com.patrykandpatrick.vico.core.common.Dimensions +import com.patrykandpatrick.vico.core.common.component.TextComponent +import com.patrykandpatrick.vico.core.common.shader.DynamicShader +import com.patrykandpatrick.vico.core.common.shape.Shape +import com.tangem.common.ui.charts.preview.MarketChartPreviewDataProvider +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import java.math.BigDecimal + +/** + * @param color The color of the indicator and guideline. + * @param innerCircleColor The color of the inner circle of the indicator. + * + * @return A [CartesianMarker] that consists of a dashed guideline and a layered indicator with a shadow effect. + */ +@Composable +internal fun rememberTangemChartMarker(color: Color, innerCircleColor: Color): CartesianMarker { + val indicatorFrontComponent = rememberShapeComponent( + shape = Shape.Pill, + color = innerCircleColor, + ) + val indicatorCenterComponent = rememberShapeComponent( + shape = Shape.Pill, + color = color, + ) + val indicatorRearComponent = rememberShapeComponent( + shape = Shape.Pill, + color = if (color == Color.Transparent) { + Color.Transparent + } else { + color.copy(alpha = INDICATOR_REAR_COLOR_ALPHA) + }, + ) + val indicator = rememberLayeredComponent( + rear = indicatorRearComponent, + front = rememberLayeredComponent( + rear = indicatorCenterComponent, + front = indicatorFrontComponent, + padding = indicatorPadding, + ), + padding = indicatorPadding, + ) + val guideline = rememberUnboundedLineComponent( + color = color, + verticalAddDrawSpace = TangemTheme.dimens.spacing24, + shape = remember { Shape.dashed(Shape.Rectangle, 4.dp, 4.dp) }, + ) + return remember(indicator, guideline) { + object : DefaultCartesianMarker( + label = TextComponent.build { textSizeSp = 0f }, + indicator = indicator, + indicatorSizeDp = INDICATOR_SIZE_DP, + guideline = guideline, + ) { + override fun getInsets( + context: CartesianMeasureContext, + outInsets: Insets, + horizontalDimensions: HorizontalDimensions, + ) { + with(context) { + super.getInsets(context, outInsets, horizontalDimensions) + val baseShadowInsetDp = + CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER * LABEL_BACKGROUND_SHADOW_RADIUS_DP + outInsets.top += (baseShadowInsetDp - LABEL_BACKGROUND_SHADOW_DY_DP).pixels + outInsets.bottom += (baseShadowInsetDp + LABEL_BACKGROUND_SHADOW_DY_DP).pixels + } + } + } + } +} + +private val indicatorPadding = Dimensions.of(3.dp) +private const val LABEL_BACKGROUND_SHADOW_RADIUS_DP = 4f +private const val LABEL_BACKGROUND_SHADOW_DY_DP = 2f +private const val CLIPPING_FREE_SHADOW_RADIUS_MULTIPLIER = 1.4f +private const val INDICATOR_SIZE_DP = 16f +private const val INDICATOR_REAR_COLOR_ALPHA = .24f + +// region Preview + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemChartMarkerPreview( + @PreviewParameter(MarketChartPreviewDataProvider::class) previewData: Pair, List>, +) { + val marker = rememberTangemChartMarker(Color.Red, Color.White) + val y = previewData.second.map { it.toFloat() } + val x = List(y.size) { it.toFloat() } + val model = CartesianChartModel(LineCartesianLayerModel.build { series(x, y) }) + + val centerAprx = (model.models[0].minX + model.models[0].maxX) / 2f + val center = model.models[0].getXDeltaGcd().let { centerAprx - centerAprx % it } + + TangemThemePreview { + Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { + CartesianChartHost( + modifier = Modifier.fillMaxWidth(), + chart = rememberCartesianChart( + rememberLineCartesianLayer( + listOf(rememberLineSpec(shader = DynamicShader.color(Color.Blue))), + axisValueOverrider = AxisValueOverrider.fixed(minY = model.models[0].minY), + ), + persistentMarkers = mapOf(center to marker), + ), + model = model, + marker = marker, + zoomState = rememberVicoZoomState(initialZoom = Zoom.Content, zoomEnabled = false), + horizontalLayout = HorizontalLayout.fullWidth(), + ) + } + } +} + +// endregion Preview \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt new file mode 100644 index 0000000000..662cb41bc6 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/preview/MarketChartPreviewDataProvider.kt @@ -0,0 +1,152 @@ +@file:Suppress("MagicNumber") + +package com.tangem.common.ui.charts.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import java.math.BigDecimal + +internal class MarketChartPreviewDataProvider : PreviewParameterProvider, List>> { + override val values: Sequence, List>> + get() { + val bitcoinPrice = listOf( + 59270, 61748, 61941, 62889, 62740, 62989, 63858, 63608, 63951, 63917, + 63253, 63765, 63862, 64502, 63876, 64028, 63875, 64531, 64249, 63680, + 63228, 63159, 63249, 63664, 63469, 63711, 63003, 62333, 62882, 62183, + 62231, 62244, 62200, 61188, 61683, 61199, 61036, 62116, 62436, 63065, + 62908, 63067, 63294, 60904, 60677, 60790, 60705, 61041, 60667, 61158, + 61112, 60795, 60900, 60771, 61124, 61343, 61371, 61484, 61133, 62372, + 62704, 63007, 63092, 62905, 62470, 62019, 61756, 61776, 61557, 61540, + 61904, 62151, 62419, 64663, 65955, 66229, 65991, 66176, 66517, 65838, + 65210, 65216, 65611, 66467, 66209, 67247, 66913, 67059, 66978, 66845, + 67240, 66874, 66993, 66940, 67185, 67330, 67340, 66845, 66062, 66273, + 66645, 66865, 67005, 67382, 70049, 71464, 71293, 70875, 71137, 69720, + 69323, 70139, 69964, 69716, 69855, 70442, 69774, 69125, 69404, 69714, + 69967, 68042, 67077, 67938, 67833, 67182, 67306, 68327, 69093, 68517, + 68726, 68759, 69061, 68880, 69148, 69305, 69044, 69313, 69122, 68821, + 68854, 68506, 68823, 68613, 68420, 70356, 69241, 69401, 68004, 67630, + 68311, 68311, 68291, 68302, 68717, 67926, 67690, 67314, 67285, 67567, + 68026, 67552, 67740, 68519, 68611, 68363, 68503, 68171, 68326, 67121, + 67626, 67481, 67657, 67567, 67608, 67607, 67683, 67729, 67733, 67709, + ).map { it.toBigDecimal() } + val bitcoinTimestamps = listOf( + 1714752000000, 1714766400000, 1714780800000, 1714795200000, 1714809600000, + 1714824000000, 1714838400000, 1714852800000, 1714867200000, 1714881600000, + 1714896000000, 1714910400000, 1714924800000, 1714939200000, 1714953600000, + 1714968000000, 1714982400000, 1714996800000, 1715011200000, 1715025600000, + 1715040000000, 1715054400000, 1715068800000, 1715083200000, 1715097600000, + 1715112000000, 1715126400000, 1715140800000, 1715155200000, 1715169600000, + 1715184000000, 1715198400000, 1715212800000, 1715227200000, 1715241600000, + 1715256000000, 1715270400000, 1715284800000, 1715299200000, 1715313600000, + 1715328000000, 1715342400000, 1715356800000, 1715371200000, 1715385600000, + 1715400000000, 1715414400000, 1715428800000, 1715443200000, 1715457600000, + 1715472000000, 1715486400000, 1715500800000, 1715515200000, 1715529600000, + 1715544000000, 1715558400000, 1715572800000, 1715587200000, 1715601600000, + 1715616000000, 1715630400000, 1715644800000, 1715659200000, 1715673600000, + 1715688000000, 1715702400000, 1715716800000, 1715731200000, 1715745600000, + 1715760000000, 1715774400000, 1715788800000, 1715803200000, 1715817600000, + 1715832000000, 1715846400000, 1715860800000, 1715875200000, 1715889600000, + 1715904000000, 1715918400000, 1715932800000, 1715947200000, 1715961600000, + 1715976000000, 1715990400000, 1716004800000, 1716019200000, 1716033600000, + 1716048000000, 1716062400000, 1716076800000, 1716091200000, 1716105600000, + 1716120000000, 1716134400000, 1716148800000, 1716163200000, 1716177600000, + 1716192000000, 1716206400000, 1716220800000, 1716235200000, 1716249600000, + 1716264000000, 1716278400000, 1716292800000, 1716307200000, 1716321600000, + 1716336000000, 1716350400000, 1716364800000, 1716379200000, 1716393600000, + 1716408000000, 1716422400000, 1716436800000, 1716451200000, 1716465600000, + 1716480000000, 1716494400000, 1716508800000, 1716523200000, 1716537600000, + 1716552000000, 1716566400000, 1716580800000, 1716595200000, 1716609600000, + 1716624000000, 1716638400000, 1716652800000, 1716667200000, 1716681600000, + 1716696000000, 1716710400000, 1716724800000, 1716739200000, 1716753600000, + 1716768000000, 1716782400000, 1716796800000, 1716811200000, 1716825600000, + 1716840000000, 1716854400000, 1716868800000, 1716883200000, 1716897600000, + 1716912000000, 1716926400000, 1716940800000, 1716955200000, 1716969600000, + 1716984000000, 1716998400000, 1717012800000, 1717027200000, 1717041600000, + 1717056000000, 1717070400000, 1717084800000, 1717099200000, 1717113600000, + 1717128000000, 1717142400000, 1717156800000, 1717171200000, 1717185600000, + 1717200000000, 1717214400000, 1717228800000, 1717243200000, 1717257600000, + 1717272000000, 1717286400000, 1717300800000, 1717315200000, 1717329600000, + ).map { it.toBigDecimal() } + val notcoinPrice = listOf( + "0.02026708", "0.02033274", "0.02112643", "0.02090579", + "0.02047231", "0.0215645", "0.0089055", "0.00674459", + "0.00733586", "0.00758165", "0.0068592", "0.00680505", + "0.00685493", "0.00702113", "0.00725545", "0.00696515", + "0.00681556", "0.00677416", "0.00669383", "0.00659057", + "0.00668081", "0.0066211", "0.0065232", "0.00612542", + "0.00600531", "0.00570974", "0.00559875", "0.00555247", + "0.005495", "0.00547841", "0.00545514", "0.00553865", + "0.00563302", "0.00568475", "0.00562512", "0.00551265", + "0.005398", "0.00560001", "0.00572701", "0.00563223", + "0.00555209", "0.00549261", "0.00524999", "0.00531381", + "0.00539702", "0.00530647", "0.00533963", "0.00527143", + "0.00525981", "0.00495323", "0.00481584", "0.0048854", + "0.00480298", "0.00471316", "0.00473191", "0.00476389", + "0.00476992", "0.00484649", "0.00471095", "0.00501396", + "0.00495481", "0.00545029", "0.0053947", "0.00533251", + "0.00518064", "0.00504481", "0.00507209", "0.00516952", + "0.00524886", "0.00542661", "0.00544231", "0.00579898", + "0.00681472", "0.00720634", "0.00824023", "0.00856427", + "0.00821144", "0.00818103", "0.00960254", "0.00911172", + "0.00888481", "0.00925911", "0.0091132", "0.009285", + "0.00886256", "0.00938211", "0.00936566", "0.0104387", + "0.01088452", "0.01200575", "0.01217514", "0.011921", + "0.01293908", "0.0126124", "0.01224851", "0.01191149", + "0.01179783", "0.01164533", "0.01175718", "0.01169137", + "0.01212768", "0.01215952", "0.01300256", "0.01589522", + "0.01588223", "0.01780629", "0.01919794", "0.01922187", + "0.02165268", "0.02400163", "0.02290975", "0.02383495", + "0.02088105", "0.02373489", "0.02269442", "0.02226249", + "0.02148038", "0.0232045", "0.02623129", "0.02378975", + "0.02438442", "0.02417507", "0.02269891", "0.02236198", + "0.02209164", "0.02169842", "0.02137774", "0.02188837", + "0.0216619", "0.02238334", "0.02186701", "0.02186521", + "0.0217595", "0.02099916", "0.02129634", "0.02143028", + "0.02192513", "0.02172005", "0.02184525", "0.01873974", + "0.01899164", "0.01957115", "0.0204723", "0.0199247", + "0.01935441", "0.01886976", "0.01856944", "0.0179975", + "0.0178625", "0.01796981", + ).map { BigDecimal(it) } + val notcoinTimestamps = listOf( + 1715428800000, 1715443200000, 1715457600000, 1715472000000, + 1715486400000, 1715860800000, 1715875200000, 1715889600000, + 1715904000000, 1715918400000, 1715932800000, 1715947200000, + 1715961600000, 1715976000000, 1715990400000, 1716004800000, + 1716019200000, 1716033600000, 1716048000000, 1716062400000, + 1716076800000, 1716091200000, 1716105600000, 1716120000000, + 1716134400000, 1716148800000, 1716163200000, 1716177600000, + 1716192000000, 1716206400000, 1716220800000, 1716235200000, + 1716249600000, 1716264000000, 1716278400000, 1716292800000, + 1716307200000, 1716321600000, 1716336000000, 1716350400000, + 1716364800000, 1716379200000, 1716393600000, 1716408000000, + 1716422400000, 1716436800000, 1716451200000, 1716465600000, + 1716480000000, 1716494400000, 1716508800000, 1716523200000, + 1716537600000, 1716552000000, 1716566400000, 1716580800000, + 1716595200000, 1716609600000, 1716624000000, 1716638400000, + 1716652800000, 1716667200000, 1716681600000, 1716696000000, + 1716710400000, 1716724800000, 1716739200000, 1716753600000, + 1716768000000, 1716782400000, 1716796800000, 1716811200000, + 1716825600000, 1716840000000, 1716854400000, 1716868800000, + 1716883200000, 1716897600000, 1716912000000, 1716926400000, + 1716940800000, 1716955200000, 1716969600000, 1716984000000, + 1716998400000, 1717012800000, 1717027200000, 1717041600000, + 1717056000000, 1717070400000, 1717084800000, 1717099200000, + 1717113600000, 1717128000000, 1717142400000, 1717156800000, + 1717171200000, 1717185600000, 1717200000000, 1717214400000, + 1717228800000, 1717243200000, 1717257600000, 1717272000000, + 1717286400000, 1717300800000, 1717315200000, 1717329600000, + 1717344000000, 1717358400000, 1717372800000, 1717387200000, + 1717401600000, 1717416000000, 1717430400000, 1717444800000, + 1717459200000, 1717473600000, 1717488000000, 1717502400000, + 1717516800000, 1717531200000, 1717545600000, 1717560000000, + 1717574400000, 1717588800000, 1717603200000, 1717617600000, + 1717632000000, 1717646400000, 1717660800000, 1717675200000, + 1717689600000, 1717704000000, 1717718400000, 1717732800000, + 1717747200000, 1717761600000, 1717776000000, 1717790400000, + 1717804800000, 1717819200000, 1717833600000, 1717848000000, + 1717862400000, 1717876800000, 1717891200000, 1717905600000, + 1717920000000, 1717934400000, + ).map { it.toBigDecimal() } + + return sequenceOf(bitcoinTimestamps to bitcoinPrice, notcoinTimestamps to notcoinPrice) + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt new file mode 100644 index 0000000000..e01681994c --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/AxisLabelFormatter.kt @@ -0,0 +1,17 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Stable +import java.math.BigDecimal + +/** + * Used for formatting the axis labels in a chart. + * It takes a BigDecimal value and returns a CharSequence that represents the formatted label. + * + * @param value The value to be formatted. + * @return The formatted label as a CharSequence. + */ +@Stable +fun interface AxisLabelFormatter { + + fun format(value: BigDecimal): CharSequence +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt new file mode 100644 index 0000000000..7e4887fedc --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartData.kt @@ -0,0 +1,36 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Immutable +import java.math.BigDecimal + +@Immutable +sealed interface MarketChartData { + + /** + * This interface represents the state when there is no data for the Market Chart. + */ + @Immutable + sealed interface NoData : MarketChartData { + @Immutable + data object Empty : NoData + + @Immutable + data object Loading : NoData + + @Immutable + data object ErrorAndRetry : NoData + } + + /** + * This data class represents the data for the Market Chart. + * It includes properties for x and y values. + * + * @property x List of x values. + * @property y List of y values. + */ + @Immutable + data class Data( + val x: List = listOf(), + val y: List = listOf(), + ) : MarketChartData +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt new file mode 100644 index 0000000000..bcad7d3ceb --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt @@ -0,0 +1,201 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Stable +import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer +import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel +import com.patrykandpatrick.vico.core.common.data.ExtraStore +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +/** + * This class represents a transaction for updating the state and look of a Market Chart. + * + * @property chartLook The updated look of the Market Chart. + * @property chartData The updated state of the Market Chart. + */ +class Transaction( + private val currentData: MarketChartData, + private val currentLook: MarketChartLook, +) { + var chartLook: MarketChartLook? = null + var chartData: MarketChartData.NoData? = null + + fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) { + chartLook = block(currentLook) + } + + fun updateState(block: (prev: MarketChartData) -> MarketChartData.NoData) { + chartData = block(currentData) + } +} + +/** + * This class represents a transaction for updating the state and look of a Market Chart. + * It extends the Transaction class and allows update state by data. + * + * @property chartData The updated state of the Market Chart. + * @property chartLook The updated look of the Market Chart. + */ +class TransactionSuspend( + private val currentData: MarketChartData, + private val currentLook: MarketChartLook, +) { + internal var nonSuspendTransaction: Transaction? = null + var chartData: MarketChartData? = null + var chartLook: MarketChartLook? + get() = nonSuspendTransaction?.chartLook + set(value) { + if (nonSuspendTransaction == null) { + nonSuspendTransaction = Transaction(currentData, currentLook) + } + nonSuspendTransaction?.chartLook = value + } + + fun updateLook(block: (prev: MarketChartLook) -> MarketChartLook) { + chartLook = block(currentLook) + } + + internal fun updateState(block: (prev: MarketChartData) -> MarketChartData) { + chartData = block(currentData) + } + + internal fun updateData(block: (prev: MarketChartData.Data) -> MarketChartData.Data) { + chartData = when (val currentState = currentData) { + is MarketChartData.Data -> block(currentState) + else -> currentState + } + } +} + +@Stable +class MarketChartDataProducer private constructor( + initialData: MarketChartData, + initialLook: MarketChartLook, + val pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, + private val dispatcher: CoroutineDispatcher = Dispatchers.Default, +) { + internal val startDrawingAnimation = MutableSharedFlow() + internal val dataState = MutableStateFlow(initialData) + internal val lookState = MutableStateFlow(initialLook) + internal val entries = MutableStateFlow>(emptyList()) + + internal val modelProducer = CartesianChartModelProducer.build(dispatcher = dispatcher) + + /** + * This function runs a suspending transaction block to update the state and look of the Market Chart. + */ + suspend fun runTransactionSuspend(block: TransactionSuspend.() -> Unit) = + handleTransactionSuspend(transaction = TransactionSuspend(dataState.value, lookState.value).apply(block)) + + /** + * This function runs a non-suspending transaction block to update the state and look of the Market Chart. + */ + fun runTransaction(block: Transaction.() -> Unit) = + handleTransaction(transaction = Transaction(dataState.value, lookState.value).apply(block)) + + private suspend fun handleTransactionSuspend(transaction: TransactionSuspend) { + val nonSuspendTransaction = transaction.nonSuspendTransaction + val chartData = transaction.chartData + val oldData = dataState.value + + if (chartData != null) { + dataState.value = chartData + } + + if (chartData is MarketChartData.Data && (oldData !is MarketChartData.Data || oldData != chartData)) { + if (lookState.value.animationOnDataChange) { + startDrawingAnimation.emit(Unit) + } + withContext(dispatcher) { + val rawData = pointsValuesConverter.convert(chartData) + + val entriesLocal = + rawData.x.mapIndexed { index, fl -> LineCartesianLayerModel.Entry(fl, rawData.y[index]) } + + entries.value = entriesLocal + + modelProducer.runTransaction { + add(LineCartesianLayerModel.Partial(series = listOf(entriesLocal))) + + updateExtras { + it[entriesKey] = entriesLocal + it[xKey] = chartData.x + it[yKey] = chartData.y + } + }.await() + } + } + + nonSuspendTransaction?.let { handleTransaction(it) } + } + + private fun handleTransaction(transaction: Transaction) { + transaction.chartData?.let { + dataState.value = it + } + transaction.chartLook?.let { + lookState.value = it + } + } + + companion object { + internal val entriesKey = ExtraStore.Key>() + internal val xKey = ExtraStore.Key>() + internal val yKey = ExtraStore.Key>() + + private val initialData: MarketChartData = MarketChartData.NoData.Empty + private val initialLook: MarketChartLook = MarketChartLook() + + /** + * This function builds a MarketChartDataProducer with the given parameters. + * It runs a suspending transaction block to initialize the data and look of the Market Chart. + * + * @param dispatcher The dispatcher to be used for data updates. + * @param block The transaction block to be run. + * @return A MarketChartDataProducer. + */ + suspend fun buildSuspend( + pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + block: TransactionSuspend.() -> Unit, + ): MarketChartDataProducer { + val transaction = TransactionSuspend(initialData, initialLook).apply(block) + + return MarketChartDataProducer( + initialData = initialData, + initialLook = initialLook, + dispatcher = dispatcher, + pointsValuesConverter = pointsValuesConverter, + ).apply { + handleTransactionSuspend(transaction) + } + } + + /** + * This function builds a MarketChartDataProducer with the given parameters. + * It runs a non-suspending transaction block to initialize the data and look of the Market Chart. + * + * @param dispatcher The dispatcher to be used for data updates. + * @param block The transaction block to be run. + * @return A MarketChartDataProducer. + */ + fun build( + pointsValuesConverter: PointValuesConverter = DefaultPointValuesConverter, + dispatcher: CoroutineDispatcher = Dispatchers.Default, + block: Transaction.() -> Unit, + ): MarketChartDataProducer { + val transaction = Transaction(initialData, initialLook).apply(block) + + return MarketChartDataProducer( + initialData = transaction.chartData ?: initialData, + initialLook = transaction.chartLook ?: initialLook, + dispatcher = dispatcher, + pointsValuesConverter = pointsValuesConverter, + ) + } + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt new file mode 100644 index 0000000000..c9ecaae2c0 --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt @@ -0,0 +1,28 @@ +package com.tangem.common.ui.charts.state + +/** + * This class represents the look and feel of a Market Chart. + * It includes properties for type, marker highlight, animation on data change, animate data appearance, + * and formatters for x and y axis. + * + * @property type The type of the chart, can be either Growing or Falling. + * @property markerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart. + * @property animationOnDataChange A boolean indicating whether to animate on data change. + * @property animateDataAppearance A boolean indicating whether to animate data appearance. + * @property xAxisFormatter A formatter for the x-axis labels. + * @property yAxisFormatter A formatter for the y-axis labels. + */ +data class MarketChartLook( + val type: Type = Type.Growing, + val markerHighlightRightSide: Boolean = true, + val animationOnDataChange: Boolean = false, + val animateDataAppearance: Boolean = false, + val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, + val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, +) { + + enum class Type { + Growing, + Falling, + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt new file mode 100644 index 0000000000..a1b7e91f2c --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartRawData.kt @@ -0,0 +1,9 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.Immutable + +@Immutable +data class MarketChartRawData( + val y: List, + val x: List = List(y.size) { 1f }, +) \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt new file mode 100644 index 0000000000..92a0379f5d --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt @@ -0,0 +1,139 @@ +package com.tangem.common.ui.charts.state + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker +import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarkerVisibilityListener +import com.patrykandpatrick.vico.core.cartesian.marker.LineCartesianLayerMarkerTarget +import java.math.BigDecimal + +/** + * MarketChartState used for MarketChart ui component. + * + * @param dataProducer The producer of the data for the Market Chart. + * @param colorMapper A function that maps a MarketChartLook.Type to a Color. + * @param onMarkerShown A callback function that is called when the marker is shown, hidden, or updated. + * @return A MarketChartState. + */ +@Composable +fun rememberMarketChartState( + dataProducer: MarketChartDataProducer = remember { MarketChartDataProducer.build {} }, + colorMapper: (MarketChartLook.Type) -> Color = { + when (it) { + MarketChartLook.Type.Growing -> Color.Green + MarketChartLook.Type.Falling -> Color.Red + } + }, + onMarkerShown: (x: BigDecimal?, y: BigDecimal?) -> Unit = { _, _ -> }, +): MarketChartState { + val lookState = dataProducer.lookState.collectAsStateWithLifecycle() + + val state = remember(dataProducer, lookState, colorMapper, onMarkerShown) { + MarketChartState(dataProducer, lookState, colorMapper, onMarkerShown) + } + + LaunchedEffect(Unit) { + dataProducer.startDrawingAnimation.collect { + state.startDrawingAnimation() + } + } + + return state +} + +/** + * Represents the state of a Market Chart. + * + * @property dataProducer The producer of the data for the Market Chart. + * @property lookState The look state of the Market Chart. + * @property colorMapper A function that maps a MarketChartLook.Type to a Color. + * @property markerCallback A callback function that is called when the marker is shown, hidden, or updated. + * @property isDrawingAnimationInProgress A boolean indicating whether the drawing animation is in progress. + */ +@Stable +class MarketChartState internal constructor( + private val dataProducer: MarketChartDataProducer, + private val lookState: State, + private val colorMapper: (MarketChartLook.Type) -> Color, + private val markerCallback: (x: BigDecimal?, y: BigDecimal?) -> Unit, +) { + internal val startDrawingAnimationState = mutableStateOf(false) + internal val modelProducer = dataProducer.modelProducer + + internal val chartColor by derivedStateOf { + colorMapper(lookState.value.type) + } + + internal val markerHighlightRightSide by derivedStateOf { + lookState.value.markerHighlightRightSide + } + + internal val xValueFormatter by derivedStateOf { + CartesianValueFormatter { value, _, _ -> + val state = dataProducer.dataState.value as? MarketChartData.Data + ?: return@CartesianValueFormatter value.toString() + + lookState.value.xAxisFormatter.format( + value = dataProducer.pointsValuesConverter.prepareRawXForFormat(value, state), + ) + } + } + + internal val yValueFormatter by derivedStateOf { + CartesianValueFormatter { value, _, _ -> + val state = dataProducer.dataState.value as? MarketChartData.Data + ?: return@CartesianValueFormatter value.toString() + + lookState.value.yAxisFormatter.format( + value = dataProducer.pointsValuesConverter.prepareRawYForFormat(value, state), + ) + } + } + + internal var markerFraction: Float? by mutableStateOf(null) + + internal val markerVisibilityListener = object : CartesianMarkerVisibilityListener { + override fun onShown(marker: CartesianMarker, targets: List) { + val point = getPoint(targets) ?: run { + markerCallback(null, null) + return + } + markerCallback(point.first, point.second) + } + + override fun onHidden(marker: CartesianMarker) { + markerCallback(null, null) + } + + override fun onUpdated(marker: CartesianMarker, targets: List) { + val point = getPoint(targets) ?: run { + markerCallback(null, null) + return + } + markerCallback(point.first, point.second) + } + } + + val isDrawingAnimationInProgress: Boolean by derivedStateOf { + startDrawingAnimationState.value + } + + private fun getPoint(targets: List): Pair? { + val entry = (targets[0] as LineCartesianLayerMarkerTarget).points[0].entry + val entryIndex = dataProducer.entries.value.indexOf(entry).takeIf { it != -1 } ?: return null + val state = dataProducer.dataState.value as? MarketChartData.Data ?: return null + val x = state.x.getOrNull(entryIndex) ?: return null + val y = state.y.getOrNull(entryIndex) ?: return null + return x to y + } + + fun startDrawingAnimation() { + startDrawingAnimationState.value = true + } + + fun stopDrawingAnimation() { + startDrawingAnimationState.value = false + } +} \ No newline at end of file diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt new file mode 100644 index 0000000000..1dab1cd0cc --- /dev/null +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/PointValuesConverter.kt @@ -0,0 +1,69 @@ +package com.tangem.common.ui.charts.state + +import java.math.BigDecimal + +/** + * Interface to convert chart data values to Floats and backwards. + * + * We need to convert the values on the graph to floating point values in order to display them correctly on the canvas. + * We also need to determine exactly which floating point value on the graph corresponds to the decimal point, + * so that we can format the actual value and display on the x/y axis. + */ +interface PointValuesConverter { + + fun convert(data: MarketChartData.Data): MarketChartRawData + + fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal + + fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal +} + +object DefaultPointValuesConverter : PointValuesConverter { + + override fun convert(data: MarketChartData.Data): MarketChartRawData { + val minX = data.x.min() + val minY = data.y.min() + + val normY = data.y.map { normalize(it, minY) } + val normX = data.x.map { normalize(it, minX) } + + return MarketChartRawData( + x = normX, + y = normY, + ) + } + + override fun prepareRawXForFormat(rawX: Float, data: MarketChartData.Data): BigDecimal { + val dataMin = data.x.min() + val scale = dataMin.scale() + val bVal = if (scale > 2) { + rawX.toBigDecimal().movePointLeft(scale - 2) + dataMin + } else { + rawX.toBigDecimal() + dataMin + } + + return bVal + } + + override fun prepareRawYForFormat(rawY: Float, data: MarketChartData.Data): BigDecimal { + val dataMin = data.y.min() + val scale = dataMin.scale() + val bVal = if (scale > 2) { + rawY.toBigDecimal().movePointLeft(scale - 2) + dataMin + } else { + rawY.toBigDecimal() + dataMin + } + + return bVal + } + + // TODO enhance algorithm for values with big difference between min and max, which cannot fit in Float + private fun normalize(value: BigDecimal, min: BigDecimal, scale: Int = min.scale()): Float { + val n = value - min + return if (scale > 2) { + n.movePointRight(scale - 2).toFloat() + } else { + n.toFloat() + } + } +} \ No newline at end of file diff --git a/common/ui/.gitignore b/common/ui/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/common/ui/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts new file mode 100644 index 0000000000..e57cd07bd1 --- /dev/null +++ b/common/ui/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.common.ui" +} + +dependencies { + + /** Compose */ + implementation(deps.compose.material3) + implementation(deps.compose.material) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.navigation) + implementation(deps.compose.navigation.hilt) + + /** Deps */ + implementation(deps.kotlin.immutable.collections) + + /** Project - Common */ + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Project - Domain */ + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.appCurrency.models) + implementation(deps.tangem.blockchain) { + exclude(module = "joda-time") + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenClickIntents.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenClickIntents.kt new file mode 100644 index 0000000000..a913361583 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenClickIntents.kt @@ -0,0 +1,24 @@ +package com.tangem.common.ui.amountScreen + +/** Amount screen clicks */ +interface AmountScreenClickIntents { + + /** On amount [value] changed */ + fun onAmountValueChange(value: String) + + /** Click triggered on value paste */ + fun onAmountPasteTriggerDismiss() + + /** On max amount click */ + fun onMaxValueClick() + + /** + * On currency change from crypto currency to app currency clicked + * + * @param isFiat indicates currency to change + */ + fun onCurrencyChangeClick(isFiat: Boolean) + + /** On next screen click */ + fun onAmountNext() +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt similarity index 67% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index 6181fc2174..64615bc792 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/SendAmountContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui.amount +package com.tangem.common.ui.amountScreen import android.content.res.Configuration import androidx.compose.foundation.background @@ -9,20 +9,24 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.common.ui.amountScreen.ui.amountField +import com.tangem.common.ui.amountScreen.ui.buttons import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData -import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.core.ui.res.TangemThemePreview +/** + * Amount screen with field + * @param amountState amount state + * @param isBalanceHiding flag hidden balances + * @param clickIntents amount screen clicks + */ @Composable -internal fun SendAmountContent( - amountState: SendStates.AmountState?, - isBalanceHiding: Boolean, - clickIntents: SendClickIntents, -) { - if (amountState == null) return +fun AmountScreenContent(amountState: AmountState, isBalanceHiding: Boolean, clickIntents: AmountScreenClickIntents) { + if (amountState !is AmountState.Data) return + // Do not put fillMaxSize() in here LazyColumn( modifier = Modifier @@ -48,19 +52,19 @@ internal fun SendAmountContent( @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun SendAmountContentPreview( - @PreviewParameter(SendAmountContentPreviewProvider::class) amountState: SendStates.AmountState, + @PreviewParameter(SendAmountContentPreviewProvider::class) amountState: AmountState, ) { TangemThemePreview { - SendAmountContent( + AmountScreenContent( amountState = amountState, isBalanceHiding = false, - clickIntents = SendClickIntentsStub, + clickIntents = AmountScreenClickIntentsStub, ) } } -private class SendAmountContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence +private class SendAmountContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( AmountStatePreviewData.amountState, ) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt new file mode 100644 index 0000000000..cdacdd3ce7 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt @@ -0,0 +1,45 @@ +package com.tangem.common.ui.amountScreen.converters + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer + +/** + * Selected currency change from crypto currency to app currency and vice versa + * + * @property cryptoCurrencyStatus current cryptocurrency status + * @property value is crypto currency or app currency + */ +class AmountCurrencyTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: Boolean, +) : Transformer { + + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + + val amountTextField = prevState.amountTextField + + val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() + val isDoneActionEnabled = prevState.isPrimaryButtonEnabled + return if (amountTextField.isFiatValue == value && !isValidFiatRate) { + prevState + } else { + return prevState.copy( + amountTextField = amountTextField.copy( + isFiatValue = value, + isValuePasted = true, + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ), + selectedButton = prevState.segmentedButtonConfig.indexOfFirst { it.isFiat == value }, + ) + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt new file mode 100644 index 0000000000..ebf69ed80d --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountPastedTriggerDismissTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.common.ui.amountScreen.converters + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.utils.transformer.Transformer + +/** + * Dismisses indication on pasted value + */ +class AmountPastedTriggerDismissTransformer : Transformer { + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + + return prevState.copy( + amountTextField = prevState.amountTextField.copy( + isValuePasted = false, + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt new file mode 100644 index 0000000000..78ce58f37d --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -0,0 +1,68 @@ +package com.tangem.common.ui.amountScreen.converters + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.utils.checkExceedBalance +import com.tangem.common.ui.amountScreen.utils.getFiatValue +import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +/** + * Reduces amount by specific value + * + * @property cryptoCurrencyStatus current cryptocurrency status + * @property value reduced by value + */ +class AmountReduceByTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: ReduceByData, +) : Transformer { + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + + val amountTextField = prevState.amountTextField + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + val amountValue = prevState.amountTextField.cryptoAmount.value ?: return prevState + + val decimalCryptoValue = amountValue.minus(value.reduceAmountByDiff) + val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) + val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = false, + decimals = fiatDecimals, + ) + + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) { + decimalFiatValue.isNullOrZero() + } else { + decimalCryptoValue.isNullOrZero() + } + return prevState.copy( + isPrimaryButtonEnabled = !isExceedBalance && !isZero, + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + keyboardType = KeyboardType.Number, + ), + ), + ) + } + + data class ReduceByData( + val reduceAmountBy: BigDecimal, + val reduceAmountByDiff: BigDecimal, + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt new file mode 100644 index 0000000000..aed8332b0b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -0,0 +1,57 @@ +package com.tangem.common.ui.amountScreen.converters + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.utils.checkExceedBalance +import com.tangem.common.ui.amountScreen.utils.getFiatValue +import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +/** + * Reduces amount to specific value + * + * @property cryptoCurrencyStatus current cryptocurrency status + * @property value reduced to value + */ +class AmountReduceToTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: BigDecimal, +) : Transformer { + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + + val amountTextField = prevState.amountTextField + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + + val cryptoValue = value.parseBigDecimal(cryptoDecimals) + val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = false, + decimals = fiatDecimals, + ) + + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isNullOrZero() + return prevState.copy( + isPrimaryButtonEnabled = !isExceedBalance && !isZero, + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = value), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isExceedBalance, value), + keyboardType = KeyboardType.Number, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt similarity index 61% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 6afdc1ada8..ad251378f6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -1,6 +1,11 @@ -package com.tangem.features.send.impl.presentation.state.amount +package com.tangem.common.ui.amountScreen.converters -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -9,23 +14,37 @@ import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf -internal class SendAmountStateConverter( +/** + * Converts initial [String] to [AmountState] + * + * @property clickIntents amount screen clicks + * @property appCurrencyProvider selected app currency provider + * @property userWalletProvider selected user wallet provider + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + * @property iconStateConverter currency icon converter + */ +class AmountStateConverter( + private val clickIntents: AmountScreenClickIntents, private val appCurrencyProvider: Provider, private val userWalletProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, private val iconStateConverter: CryptoCurrencyToIconStateConverter, - private val sendAmountFieldConverter: SendAmountFieldConverter, -) : Converter { +) : Converter { - override fun convert(value: String): SendStates.AmountState { + private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { + AmountFieldConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + ) + } + + override fun convert(value: String): AmountState { val userWallet = userWalletProvider() val appCurrency = appCurrencyProvider() val status = cryptoCurrencyStatusProvider() @@ -33,15 +52,15 @@ internal class SendAmountStateConverter( val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) val noFeeRate = status.value.fiatRate.isNullOrZero() - return SendStates.AmountState( + return AmountState.Data( walletName = userWallet.name, walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)), tokenIconState = iconStateConverter.convert(status), - amountTextField = sendAmountFieldConverter.convert(value), + amountTextField = amountFieldConverter.convert(value), isPrimaryButtonEnabled = false, appCurrencyCode = appCurrency.code, segmentedButtonConfig = persistentListOf( - SendAmountSegmentedButtonsConfig( + AmountSegmentedButtonsConfig( title = stringReference(status.currency.symbol), iconState = iconStateConverter.convertCustom( value = status, @@ -50,7 +69,7 @@ internal class SendAmountStateConverter( ), isFiat = false, ), - SendAmountSegmentedButtonsConfig( + AmountSegmentedButtonsConfig( title = stringReference(appCurrency.code), iconUrl = appCurrency.iconSmallUrl, isFiat = true, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt new file mode 100644 index 0000000000..f0be223891 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -0,0 +1,84 @@ +package com.tangem.common.ui.amountScreen.converters.field + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.utils.checkExceedBalance +import com.tangem.common.ui.amountScreen.utils.getCryptoValue +import com.tangem.common.ui.amountScreen.utils.getFiatValue +import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +/** + * Amount value change + * + * @property cryptoCurrencyStatus current cryptocurrency status + * @property value amount value + */ +class AmountFieldChangeTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: String, +) : Transformer { + + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + + val amountTextField = prevState.amountTextField + + if (value.isEmpty()) return prevState.emptyState() + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + + val trimmedValue = value.trim() + val cryptoValue = trimmedValue.getCryptoValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = amountTextField.isFiatValue, + decimals = cryptoDecimals, + ) + val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) + val (fiatValue, decimalFiatValue) = trimmedValue.getFiatValue( + fiatRate = cryptoCurrencyStatus.value.fiatRate, + isFiatValue = amountTextField.isFiatValue, + decimals = fiatDecimals, + ) + + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue + val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isZero = if (amountTextField.isFiatValue) { + decimalFiatValue.isNullOrZero() + } else { + decimalCryptoValue.isNullOrZero() + } + return prevState.copy( + isPrimaryButtonEnabled = !isExceedBalance && !isZero, + amountTextField = amountTextField.copy( + value = cryptoValue, + fiatValue = fiatValue, + isError = isExceedBalance, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + keyboardType = KeyboardType.Number, + ), + ), + ) + } + + private fun AmountState.Data.emptyState(): AmountState.Data { + return copy( + isPrimaryButtonEnabled = false, + amountTextField = amountTextField.copy( + value = "", + fiatValue = "", + cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), + fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), + isError = false, + ), + ) + } +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt similarity index 72% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index 8bf30c0b57..bfe5259127 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -1,11 +1,12 @@ -package com.tangem.features.send.impl.presentation.state.fields +package com.tangem.common.ui.amountScreen.converters.field import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import com.tangem.blockchain.extensions.toBigDecimalOrDefault -import com.tangem.common.extensions.isZero +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -13,26 +14,27 @@ import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.convertToAmount -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.StateRouter -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import java.math.BigDecimal -private const val FIAT_DECIMALS = 2 - -internal class SendAmountFieldConverter( - private val clickIntents: SendClickIntents, - private val stateRouterProvider: Provider, +/** + * Converts initial [String] to [AmountField] + * + * @property clickIntents amount screen clicks + * @property appCurrencyProvider selected app currency provider + * @property cryptoCurrencyStatusProvider current cryptocurrency status provider + */ +class AmountFieldConverter( + private val clickIntents: AmountScreenClickIntents, private val cryptoCurrencyStatusProvider: Provider, private val appCurrencyProvider: Provider, -) : Converter { +) : Converter { - override fun convert(value: String): SendTextField.AmountField { + override fun convert(value: String): AmountFieldModel { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val cryptoDecimal = value.toBigDecimalOrDefault() + val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) val fiatRate = cryptoCurrencyStatus.value.fiatRate val (fiatValue, fiatDecimal) = when { @@ -44,8 +46,8 @@ internal class SendAmountFieldConverter( fiatValue to fiatDecimal } } - val isDoneActionEnabled = !cryptoDecimal.isZero() - return SendTextField.AmountField( + val isDoneActionEnabled = !cryptoDecimal.isNullOrZero() + return AmountFieldModel( value = value, fiatValue = fiatValue, onValueChange = clickIntents::onAmountValueChange, @@ -54,7 +56,7 @@ internal class SendAmountFieldConverter( keyboardType = KeyboardType.Number, ), keyboardActions = KeyboardActions( - onDone = { clickIntents.onNextClick(stateRouterProvider().isEditState) }, + onDone = { clickIntents.onAmountNext() }, ), isFiatValue = false, cryptoAmount = cryptoAmount, @@ -73,4 +75,8 @@ internal class SendAmountFieldConverter( decimals = FIAT_DECIMALS, type = AmountType.FiatType(appCurrency.code), ) + + private companion object { + private const val FIAT_DECIMALS = 2 + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt new file mode 100644 index 0000000000..05a8705750 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.common.ui.amountScreen.converters.field + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.isNullOrZero +import com.tangem.utils.transformer.Transformer +import java.math.RoundingMode + +/** + * Selects maximum amount value + * + * @property cryptoCurrencyStatus current cryptocurrency status + */ +class AmountFieldMaxAmountTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + + val amountTextField = prevState.amountTextField + + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + val decimalCryptoValue = cryptoCurrencyStatus.value.amount + val decimalFiatValue = cryptoCurrencyStatus.value.fiatAmount + + if (decimalCryptoValue.isNullOrZero()) return prevState + + val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() + val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() + val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() + return prevState.copy( + isPrimaryButtonEnabled = true, + amountTextField = amountTextField.copy( + isValuePasted = true, + value = cryptoValue, + fiatValue = fiatValue, + isError = false, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, + keyboardType = KeyboardType.Number, + ), + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt new file mode 100644 index 0000000000..572239504d --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountFieldModel.kt @@ -0,0 +1,39 @@ +package com.tangem.common.ui.amountScreen.models + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.tokens.model.Amount + +/** + * Model for amount field + * + * @param value entered value + * @param onValueChange on value change + * @param keyboardOptions keyboard options + * @param keyboardActions keyboard actions + * @param cryptoAmount value as amount + * @param fiatAmount value in fiat as amount + * @param isFiatValue indicates if app currency or crypto currency is selected + * @param fiatValue value in fiat + * @param isFiatUnavailable indicates if fiat rates are unavailable + * @param isValuePasted indicated if value was pasted + * @param onValuePastedTriggerDismiss on value pasted action + * @param isError indicates is value invalid + * @param error error text + */ +data class AmountFieldModel( + val value: String, + val onValueChange: (String) -> Unit, + val keyboardOptions: KeyboardOptions, + val keyboardActions: KeyboardActions, + val cryptoAmount: Amount, + val fiatAmount: Amount, + val isFiatValue: Boolean, + val fiatValue: String, + val isFiatUnavailable: Boolean, + val isValuePasted: Boolean, + val onValuePastedTriggerDismiss: () -> Unit, + val isError: Boolean, + val error: TextReference, +) \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt similarity index 61% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt index eca219a09d..23469c4c03 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountSegmentedButtonsConfig.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt @@ -1,7 +1,7 @@ -package com.tangem.features.send.impl.presentation.state.amount +package com.tangem.common.ui.amountScreen.models import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference /** @@ -13,9 +13,9 @@ import com.tangem.core.ui.extensions.TextReference * @param isFiat is fiat currency */ @Immutable -internal data class SendAmountSegmentedButtonsConfig( +data class AmountSegmentedButtonsConfig( val title: TextReference, - val iconState: TokenIconState? = null, + val iconState: CurrencyIconState? = null, val iconUrl: String? = null, val isFiat: Boolean, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt new file mode 100644 index 0000000000..d23a2892bf --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -0,0 +1,40 @@ +package com.tangem.common.ui.amountScreen.models + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.PersistentList + +/** Model for amount state */ +@Stable +sealed class AmountState { + + abstract val isPrimaryButtonEnabled: Boolean + + /** + * @param isPrimaryButtonEnabled indicates if next state button enabled + * @param walletName user wallet name + * @param walletBalance user crypto currency balance in wallet + * @param tokenIconState crypto currency icon state + * @param segmentedButtonConfig currency switcher config + * @param selectedButton selected currency index + * @param isSegmentedButtonsEnabled indicates if currency switches is enabled + * @param amountTextField amount field state + * @param appCurrencyCode app currency code + */ + data class Data( + override val isPrimaryButtonEnabled: Boolean, + val walletName: String, + val walletBalance: TextReference, + val tokenIconState: CurrencyIconState, + val segmentedButtonConfig: PersistentList, + val selectedButton: Int, + val isSegmentedButtonsEnabled: Boolean, + val amountTextField: AmountFieldModel, + val appCurrencyCode: String, + ) : AmountState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : AmountState() +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountScreenClickIntentsStub.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountScreenClickIntentsStub.kt new file mode 100644 index 0000000000..9509a15aed --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountScreenClickIntentsStub.kt @@ -0,0 +1,16 @@ +package com.tangem.common.ui.amountScreen.preview + +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents + +object AmountScreenClickIntentsStub : AmountScreenClickIntents { + + override fun onAmountValueChange(value: String) {} + + override fun onCurrencyChangeClick(isFiat: Boolean) {} + + override fun onMaxValueClick() {} + + override fun onAmountPasteTriggerDismiss() {} + + override fun onAmountNext() {} +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt similarity index 75% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index 7b2017d6e7..91951b4220 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -1,40 +1,38 @@ -package com.tangem.features.send.impl.presentation.state.previewdata +package com.tangem.common.ui.amountScreen.preview import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.SendUiStateType -import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig -import com.tangem.features.send.impl.presentation.state.fields.SendTextField import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal -internal object AmountStatePreviewData { +object AmountStatePreviewData { - val amountState = SendStates.AmountState( - type = SendUiStateType.Amount, + val amountState = AmountState.Data( isPrimaryButtonEnabled = false, walletName = "Family Wallet", walletBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), - tokenIconState = TokenIconState.Loading, + tokenIconState = CurrencyIconState.Loading, segmentedButtonConfig = persistentListOf( - SendAmountSegmentedButtonsConfig( + AmountSegmentedButtonsConfig( title = stringReference("USDT"), - iconState = TokenIconState.Locked, + iconState = CurrencyIconState.Locked, isFiat = false, ), - SendAmountSegmentedButtonsConfig( + AmountSegmentedButtonsConfig( title = stringReference("USD"), isFiat = true, ), ), appCurrencyCode = "usd", - amountTextField = SendTextField.AmountField( + amountTextField = AmountFieldModel( value = "", onValueChange = {}, keyboardOptions = KeyboardOptions.Default, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt similarity index 83% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index a4912c792e..b2903b20cc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui.send +package com.tangem.common.ui.amountScreen.ui import android.content.res.Configuration import androidx.compose.foundation.background @@ -15,21 +15,17 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.core.ui.components.ResizableText -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.features.send.impl.presentation.state.SendStates -import com.tangem.features.send.impl.presentation.state.previewdata.AmountStatePreviewData @Composable -internal fun AmountBlock( - amountState: SendStates.AmountState, - isClickDisabled: Boolean, - isEditingDisabled: Boolean, - onClick: () -> Unit, -) { +fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit) { + if (amountState !is AmountState.Data) return val amount = amountState.amountTextField val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) @@ -58,7 +54,7 @@ internal fun AmountBlock( .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing16), ) { - TokenIcon(state = amountState.tokenIconState) + CurrencyIcon(state = amountState.tokenIconState) ResizableText( text = firstAmount, style = TangemTheme.typography.h2, @@ -85,7 +81,7 @@ internal fun AmountBlock( @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: SendStates.AmountState) { +private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::class) value: AmountState) { TangemThemePreview { AmountBlock( amountState = value, @@ -96,8 +92,8 @@ private fun AmountBlockPreview(@PreviewParameter(AmountBlockPreviewProvider::cla } } -private class AmountBlockPreviewProvider : PreviewParameterProvider { - override val values: Sequence +private class AmountBlockPreviewProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( AmountStatePreviewData.amountState, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt similarity index 85% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt index 19325703a5..b279e47f6a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountButtons.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui.amount +package com.tangem.common.ui.amountScreen.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -13,22 +13,22 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.currency.fiaticon.FiatIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.R -import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig -import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.PersistentList private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" internal fun LazyListScope.buttons( - segmentedButtonConfig: PersistentList, - clickIntents: SendClickIntents, + segmentedButtonConfig: PersistentList, + clickIntents: AmountScreenClickIntents, isSegmentedButtonsEnabled: Boolean, selectedButton: Int, ) { @@ -53,7 +53,7 @@ internal fun LazyListScope.buttons( initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton), isEnabled = isSegmentedButtonsEnabled, ) { - SendAmountCurrencyButton( + AmountCurrencyButton( button = it, isSegmentedButtonsEnabled = isSegmentedButtonsEnabled, ) @@ -84,7 +84,7 @@ internal fun LazyListScope.buttons( } @Composable -private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) { +private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) { Row( modifier = Modifier .fillMaxSize() @@ -94,7 +94,8 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig, i horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - val iconModifier = Modifier.size(TangemTheme.dimens.size18) + val iconModifier = Modifier + .size(TangemTheme.dimens.size18) .padding(horizontal = TangemTheme.dimens.spacing1) if (button.isFiat) { FiatIcon( @@ -104,7 +105,7 @@ private fun SendAmountCurrencyButton(button: SendAmountSegmentedButtonsConfig, i modifier = iconModifier, ) } else if (button.iconState != null) { - TokenIcon( + CurrencyIcon( state = button.iconState, shouldDisplayNetwork = false, modifier = iconModifier, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt similarity index 78% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index 8f7a69c8c1..1144bf4bcd 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui.amount +package com.tangem.common.ui.amountScreen.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -7,13 +7,16 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment.Companion.BottomCenter import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDirection +import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference @@ -21,18 +24,17 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.rememberDecimalFormat -import com.tangem.features.send.impl.presentation.state.fields.SendTextField import kotlinx.coroutines.delay @Composable -internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: String) { +internal fun AmountField(amountField: AmountFieldModel, appCurrencyCode: String) { val decimalFormat = rememberDecimalFormat() - val isFiatValue = sendField.isFiatValue + val isFiatValue = amountField.isFiatValue val currencyCode = if (isFiatValue) appCurrencyCode else null val (primaryAmount, primaryValue) = if (isFiatValue) { - sendField.fiatAmount to sendField.fiatValue + amountField.fiatAmount to amountField.fiatValue } else { - sendField.cryptoAmount to sendField.value + amountField.cryptoAmount to amountField.value } val requester = remember { FocusRequester() } @@ -45,16 +47,16 @@ internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: currencyCode = currencyCode, decimalFormat = decimalFormat, ), - onValueChange = sendField.onValueChange, - keyboardOptions = sendField.keyboardOptions, - keyboardActions = sendField.keyboardActions, + onValueChange = amountField.onValueChange, + keyboardOptions = amountField.keyboardOptions, + keyboardActions = amountField.keyboardActions, textStyle = TangemTheme.typography.h2.copy( color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ), isAutoResize = true, - isValuePasted = sendField.isValuePasted, - onValuePastedTriggerDismiss = sendField.onValuePastedTriggerDismiss, + isValuePasted = amountField.isValuePasted, + onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, modifier = Modifier .focusRequester(requester) .padding( @@ -70,12 +72,12 @@ internal fun AmountField(sendField: SendTextField.AmountField, appCurrencyCode: requester.requestFocus() } - AmountSecondary(sendField, appCurrencyCode) + AmountSecondary(amountField, appCurrencyCode) } @Composable -private fun AmountSecondary(sendField: SendTextField.AmountField, appCurrencyCode: String) { - val secondaryAmount = if (sendField.isFiatValue) sendField.cryptoAmount else sendField.fiatAmount +private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) { + val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount Box( modifier = Modifier .padding( @@ -84,7 +86,7 @@ private fun AmountSecondary(sendField: SendTextField.AmountField, appCurrencyCod end = TangemTheme.dimens.spacing12, ), ) { - val text = if (sendField.isFiatValue) { + val text = if (amountField.isFiatValue) { BigDecimalFormatter.formatCryptoAmount( cryptoAmount = secondaryAmount.value, cryptoCurrency = secondaryAmount.currencySymbol, @@ -107,8 +109,8 @@ private fun AmountSecondary(sendField: SendTextField.AmountField, appCurrencyCod .padding(bottom = TangemTheme.dimens.spacing32), ) AmountFieldError( - isError = sendField.isError, - error = sendField.error, + isError = amountField.isError, + error = amountField.error, modifier = Modifier .align(BottomCenter) .padding(bottom = TangemTheme.dimens.spacing12), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt similarity index 86% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index eb78c8d0a1..46fef9fbb0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/amount/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui.amount +package com.tangem.common.ui.amountScreen.ui import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background @@ -12,16 +12,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextAlign -import com.tangem.common.Strings.STARS -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.state.SendStates +import com.tangem.utils.Strings.STARS private const val AMOUNT_FIELD_KEY = "amountFieldKey" internal fun LazyListScope.amountField( - amountState: SendStates.AmountState, + amountState: AmountState.Data, isBalanceHiding: Boolean, modifier: Modifier = Modifier, ) { @@ -55,13 +55,13 @@ internal fun LazyListScope.amountField( .padding(top = TangemTheme.dimens.spacing2), ) } - TokenIcon( + CurrencyIcon( state = amountState.tokenIconState, modifier = Modifier .padding(top = TangemTheme.dimens.spacing32), ) AmountField( - sendField = amountState.amountTextField, + amountField = amountState.amountTextField, appCurrencyCode = amountState.appCurrencyCode, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt similarity index 86% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt index 3921429828..26492fa913 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt @@ -1,11 +1,11 @@ -package com.tangem.features.send.impl.presentation.state.amount +package com.tangem.common.ui.amountScreen.utils import androidx.compose.ui.text.input.ImeAction -import com.tangem.common.extensions.isZero +import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.send.impl.presentation.state.fields.SendTextField +import com.tangem.utils.isNullOrZero import java.math.BigDecimal import java.math.RoundingMode @@ -38,7 +38,7 @@ internal fun String.getFiatValue( internal fun String.checkExceedBalance( cryptoCurrencyStatus: CryptoCurrencyStatus, - amountTextField: SendTextField.AmountField, + amountTextField: AmountFieldModel, ): Boolean { val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO @@ -52,7 +52,7 @@ internal fun String.checkExceedBalance( } internal fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) = - if (!isExceedBalance && !decimalCryptoValue.isZero()) { + if (!isExceedBalance && !decimalCryptoValue.isNullOrZero()) { ImeAction.Done } else { ImeAction.None diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt similarity index 79% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt rename to common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt index 7c8a2becb6..67c6ed8cc9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/utils/FormatterUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/FormatterUtils.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.utils +package com.tangem.common.ui.amountScreen.utils import com.tangem.blockchain.common.Amount import com.tangem.core.ui.extensions.TextReference @@ -11,7 +11,7 @@ import java.math.BigDecimal private const val CRYPTO_FEE_DECIMALS = 6 -internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { +fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): TextReference? { if (amount == null) return null return combinedReference( if (isFeeApproximate) stringReference("${BigDecimalFormatter.CAN_BE_LOWER_SIGN} ") else TextReference.EMPTY, @@ -25,13 +25,13 @@ internal fun getCryptoReference(amount: Amount?, isFeeApproximate: Boolean): Tex ) } -internal fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { +fun getFiatReference(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): TextReference? { if (value == null || rate == null) return null val formattedFiat = getFiatString(value = value, rate = rate, appCurrency = appCurrency) return stringReference(formattedFiat) } -internal fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { +fun getFiatString(value: BigDecimal?, rate: BigDecimal?, appCurrency: AppCurrency): String { if (value == null || rate == null) return EMPTY_BALANCE_SIGN val feeValue = value.multiply(rate) return BigDecimalFormatter.formatFiatAmount( diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt new file mode 100644 index 0000000000..e65b7b86f1 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/GiveTxPermisssionBottomSheet.kt @@ -0,0 +1,340 @@ +package com.tangem.common.ui.bottomsheets + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.PopupProperties +import com.tangem.common.ui.R +import com.tangem.common.ui.bottomsheets.state.* +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.secondary, + ) { content: GiveTxPermissionBottomSheetConfig -> + GiveTxPermissionBottomSheetContent(content = content) + } +} + +@Composable +private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSheetConfig) { + var isPermissionAlertShow by remember { mutableStateOf(false) } + val data = content.data + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarWithAdditionalButtons( + text = resourceReference(id = R.string.give_permission_title), + iconColor = TangemTheme.colors.text.tertiary, + endButton = AdditionalButton( + iconRes = R.drawable.ic_information_24, + onIconClicked = { isPermissionAlertShow = true }, + ), + ) + Text( + text = stringResource( + id = R.string.give_permission_subtitle, + data.currency, + ), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24), + ) + + SpacerH16() + + ApprovalBottomSheetInfo(data) + + SpacerH(height = TangemTheme.dimens.spacing20) + + PrimaryButtonIconEnd( + text = stringResource(id = R.string.common_approve), + iconResId = R.drawable.ic_tangem_24, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = data.approveButton.onClick, + ) + + SpacerH12() + + SecondaryButton( + text = stringResource(id = R.string.common_cancel), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = content.onCancel, + ) + + SpacerH16() + + // region dialog + if (isPermissionAlertShow) { + BasicDialog( + message = stringResource(id = R.string.swapping_approve_information_text), + title = stringResource(id = R.string.swapping_approve_information_title), + confirmButton = DialogButton { isPermissionAlertShow = false }, + onDismissDialog = {}, + ) + } + } +} + +@Composable +private fun ApprovalBottomSheetInfo(data: GiveTxPermissionState.ReadyForRequest) { + FooterContainer( + footer = stringResource(id = R.string.give_permission_policy_type_footer), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + AmountItem( + currency = data.currency, + approveType = data.approveType, + onChangeApproveType = data.onChangeApproveType, + approveItems = data.approveItems, + ) + } + SpacerH16() + FooterContainer( + footer = stringResource(id = R.string.give_permission_fee_footer), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + FeeItem(fee = data.fee) + } +} + +@Composable +private fun FeeItem(fee: TextReference) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing16, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.common_network_fee_title), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + EllipsisText( + text = fee.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), + ) + } +} + +@Composable +private fun AmountItem( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, +) { + var isExpandSelector by remember { mutableStateOf(false) } + var amountSize by remember { mutableStateOf(IntSize.Zero) } + Box( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = { isExpandSelector = true }, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { amountSize = it } + .padding( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing16, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.give_permission_rows_amount, currency), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + when (approveType) { + ApproveType.LIMITED -> { + Text( + text = stringResource(id = R.string.give_permission_current_transaction), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + } + ApproveType.UNLIMITED -> { + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = R.drawable.ic_infinity_24), + ), + tint = TangemTheme.colors.text.tertiary, + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size20), + ) + } + } + } + DropdownSelector( + isExpanded = isExpandSelector, + onDismiss = { isExpandSelector = false }, + onItemClick = { approveType -> + isExpandSelector = false + onChangeApproveType.invoke(approveType) + }, + items = approveItems, + selectedType = approveType, + amountSize = amountSize, + ) + } +} + +@Suppress("LongParameterList") +@Composable +private fun DropdownSelector( + isExpanded: Boolean, + onDismiss: () -> Unit, + onItemClick: (ApproveType) -> Unit, + items: ImmutableList, + selectedType: ApproveType, + amountSize: IntSize, +) { + var dropDownWidth by remember { mutableStateOf(IntSize.Zero) } + val offsetY = amountSize.height.times(-1) + val offsetX = amountSize.width - dropDownWidth.width + + // Workaround to set color and shape of dropdown menu + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action), + shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)), + ) { + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismiss, + properties = PopupProperties(clippingEnabled = false), + offset = with(LocalDensity.current) { + DpOffset(x = offsetX.toDp(), y = offsetY.toDp()) + }, + modifier = Modifier + .wrapContentSize() + .background(TangemTheme.colors.background.action) + .onSizeChanged { dropDownWidth = it }, + ) { + items.forEach { item -> + val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent + + DropdownMenuItem( + modifier = Modifier.fillMaxWidth(), + text = { + Row { + Text( + text = when (item) { + ApproveType.LIMITED -> stringResource( + id = R.string.give_permission_current_transaction, + ) + ApproveType.UNLIMITED -> stringResource(id = R.string.give_permission_unlimited) + }, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + SpacerWMax() + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = R.drawable.ic_check_24), + ), + tint = color, + contentDescription = null, + modifier = Modifier.padding(start = TangemTheme.dimens.size20), + ) + } + }, + onClick = { + onItemClick.invoke(item) + }, + ) + } + } + } +} + +// region preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_AgreementBottomSheet() { + TangemThemePreview { + GiveTxPermissionBottomSheetContent(content = previewData) + } +} + +private val previewData = GiveTxPermissionBottomSheetConfig( + data = GiveTxPermissionState.ReadyForRequest( + currency = "DAI", + amount = "1", + walletAddress = "", + spenderAddress = "", + fee = TextReference.Str("2,14$"), + approveType = ApproveType.UNLIMITED, + approveButton = ApprovePermissionButton(true) {}, + cancelButton = CancelPermissionButton(true), + onChangeApproveType = { ApproveType.UNLIMITED }, + ), + onCancel = {}, +) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt new file mode 100644 index 0000000000..ad5b970ea9 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionBottomSheetConfig.kt @@ -0,0 +1,8 @@ +package com.tangem.common.ui.bottomsheets.state + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +data class GiveTxPermissionBottomSheetConfig( + val data: GiveTxPermissionState.ReadyForRequest, + val onCancel: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt similarity index 62% rename from features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt rename to common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt index 4192106336..9b07504849 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheets/state/GiveTxPermissionState.kt @@ -1,15 +1,14 @@ -package com.tangem.feature.swap.models +package com.tangem.common.ui.bottomsheets.state import com.tangem.core.ui.extensions.TextReference -import com.tangem.feature.swap.domain.models.domain.SwapApproveType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -sealed class SwapPermissionState { +sealed class GiveTxPermissionState { - object InProgress : SwapPermissionState() + data object InProgress : GiveTxPermissionState() - object Empty : SwapPermissionState() + data object Empty : GiveTxPermissionState() data class ReadyForRequest( val currency: String, @@ -18,24 +17,17 @@ sealed class SwapPermissionState { val spenderAddress: String, val fee: TextReference, val approveType: ApproveType, - val approveItems: ImmutableList = ApproveType.values().toList().toImmutableList(), + val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), val approveButton: ApprovePermissionButton, val cancelButton: CancelPermissionButton, val onChangeApproveType: (ApproveType) -> Unit, - ) : SwapPermissionState() + ) : GiveTxPermissionState() } enum class ApproveType { LIMITED, UNLIMITED } -fun ApproveType.toDomainApproveType(): SwapApproveType { - return when (this) { - ApproveType.LIMITED -> SwapApproveType.LIMITED - ApproveType.UNLIMITED -> SwapApproveType.UNLIMITED - } -} - data class ApprovePermissionButton( val enabled: Boolean, val loading: Boolean = false, diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 4deb2329d1..b706481bfe 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.staking) // TODO staking create staking models module /** Tangem libraries */ implementation(deps.tangem.blockchain) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt index f61191a104..2e6adf8fe7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/UnknownEnumMoshiAdapter.kt @@ -1,13 +1,43 @@ package com.tangem.datasource.api.common.adapter -import com.squareup.moshi.* +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi import com.squareup.moshi.adapters.EnumJsonAdapter +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO /** * Object to create a adapter for enum types with support for unknown enum values. */ object UnknownEnumMoshiAdapter { - fun > create(enumType: Class, defaultValue: T): JsonAdapter = - EnumJsonAdapter.create(enumType).withUnknownFallback(defaultValue) + @Suppress("UNCHECKED_CAST") + fun > create(enumType: Class>, defaultValue: Enum<*>): JsonAdapter> { + return EnumJsonAdapter.create(enumType as Class).withUnknownFallback(defaultValue as T) + } +} + +fun Moshi.Builder.addStakeKitEnumFallbackAdapters(): Moshi.Builder { + val map = mapOf( + NetworkTypeDTO::class.java to NetworkTypeDTO.UNKNOWN, + StakingActionTypeDTO::class.java to StakingActionTypeDTO.UNKNOWN, + YieldDTO.RewardTypeDTO::class.java to YieldDTO.RewardTypeDTO.UNKNOWN, + BalanceDTO.BalanceType::class.java to BalanceDTO.BalanceType.UNKNOWN, + StakingTransactionTypeDTO::class.java to StakingTransactionTypeDTO.UNKNOWN, + StakingTransactionStatusDTO::class.java to StakingTransactionStatusDTO.UNKNOWN, + StakingActionStatusDTO::class.java to StakingActionStatusDTO.UNKNOWN, + ) + + return apply { + map.forEach { entry -> + val enumClass = entry.key + val unknownValue = entry.value + add(enumClass, UnknownEnumMoshiAdapter.create(enumClass, unknownValue)) + } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt new file mode 100644 index 0000000000..0ded78dbab --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -0,0 +1,41 @@ +package com.tangem.datasource.api.markets + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.markets.models.response.* +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface TangemTechMarketsApi { + + @Suppress("LongParameterList") + @GET("coins/list") + suspend fun getCoinsList( + @Query("currency") currency: String, + @Query("interval") interval: String, + @Query("offset") offset: Int, + @Query("limit") limit: Int, + @Query("order") order: String, + @Query("general_coins") generalCoins: Boolean, + @Query("search") search: String?, + ): ApiResponse + + @GET("coins/{coin_id}") + suspend fun getCoinMarketData( + @Path("coin_id") coinId: String, + @Query("currency") currency: String, + ): ApiResponse + + @GET("coins/{coin_id}/history") + suspend fun getCoinChart( + @Query("currency") currency: String, + @Query("interval") interval: String, + ): ApiResponse + + @GET("coins/history_preview") + suspend fun getCoinsListCharts( + @Query("coin_ids") coinIds: List, + @Query("currency") currency: String, + @Query("interval") interval: String, + ): ApiResponse +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt new file mode 100644 index 0000000000..0d1667ee24 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.api.markets.models.response + +import com.squareup.moshi.Json + +class TokenMarketChartListResponse( + @Json(name = "tokens") + val tokens: Map, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt new file mode 100644 index 0000000000..777bbc1228 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartResponse.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.markets.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class TokenMarketChartResponse( + @Json(name = "prices") + val prices: Map, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt new file mode 100644 index 0000000000..227496a66e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketDetailsResponse.kt @@ -0,0 +1,132 @@ +package com.tangem.datasource.api.markets.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class TokenMarketDetailsResponse( + @Json(name = "id") + val id: String, + @Json(name = "name") + val name: String, + @Json(name = "symbol") + val symbol: String, + @Json(name = "active") + val active: Boolean, + @Json(name = "current_price") + val currentPrice: BigDecimal, + @Json(name = "price_change_percentage") + val priceChangePercentage: PriceChangePercentage, + @Json(name = "networks") + val networks: List, + @Json(name = "short_description") + val shortDescription: String?, + @Json(name = "full_description") + val fullDescription: String?, + @Json(name = "insights") + val insights: List?, + @Json(name = "metrics") + val metrics: Metrics, + @Json(name = "links") + val links: Links, + @Json(name = "price_performance") + val pricePerformance: PricePerformance, +) { + data class PriceChangePercentage( + @Json(name = "24h") + val h24: BigDecimal, + @Json(name = "1w") + val week1: BigDecimal, + @Json(name = "1m") + val month1: BigDecimal, + @Json(name = "3m") + val month3: BigDecimal, + @Json(name = "6m") + val month6: BigDecimal, + @Json(name = "1y") + val year1: BigDecimal, + @Json(name = "all_time") + val allTime: BigDecimal, + ) + + data class Network( + @Json(name = "network_id") + val networkId: String, + @Json(name = "exchangeable") + val exchangeable: Boolean, + @Json(name = "contract_address") + val contractAddress: String, + @Json(name = "decimalCount") + val decimalCount: Int, + ) + + data class Insight( + @Json(name = "holders_change") + val holdersChange: Change, + @Json(name = "liquidity_change") + val liquidityChange: Change, + @Json(name = "buy_pressure_change") + val buyPressureChange: Change, + @Json(name = "experienced_buyer_change") + val experiencedBuyerChange: Change, + ) { + data class Change( + @Json(name = "1d") + val day1: Int, + @Json(name = "1w") + val week1: Int, + @Json(name = "1m") + val month1: Int, + ) + } + + data class Metrics( + @Json(name = "market_rating") + val marketRating: Int, + @Json(name = "circulating_supply") + val circulatingSupply: BigDecimal, + @Json(name = "market_cap") + val marketCap: BigDecimal, + @Json(name = "volume_24h") + val volume24h: BigDecimal, + @Json(name = "total_supply") + val totalSupply: BigDecimal, + @Json(name = "fully_diluted_valuation") + val fullyDilutedValuation: BigDecimal, + ) + + data class Links( + @Json(name = "official_links") + val officialLinks: List = emptyList(), + @Json(name = "social") + val social: List = emptyList(), + @Json(name = "repository") + val repository: List = emptyList(), + @Json(name = "blockchain_site") + val blockchainSite: List = emptyList(), + ) + + data class Link( + @Json(name = "title") + val title: String?, + @Json(name = "id") + val id: String, + @Json(name = "link") + val url: String, + ) + + data class PricePerformance( + @Json(name = "high_price") + val highPrice: Price, + @Json(name = "low_price") + val lowPrice: Price, + ) { + data class Price( + @Json(name = "24h") + val h24: BigDecimal, + @Json(name = "1m") + val month1: BigDecimal, + @Json(name = "all_time") + val allTime: BigDecimal, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt new file mode 100644 index 0000000000..4157bd2bd6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -0,0 +1,43 @@ +package com.tangem.datasource.api.markets.models.response + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class TokenMarketListResponse( + @Json(name = "imageHost") + val imageHost: String, + @Json(name = "tokens") + val tokens: List, + @Json(name = "total") + val total: Int, + @Json(name = "limit") + val limit: Int, + @Json(name = "offset") + val offset: Int, +) { + data class Token( + @Json(name = "id") + val id: String, + @Json(name = "name") + val name: String, + @Json(name = "symbol") + val symbol: String, + @Json(name = "current_price") + val currentPrice: BigDecimal, + @Json(name = "price_change_percentage") + val priceChangePercentage: PriceChangePercentage, + @Json(name = "market_rating") + val marketRating: Int?, + @Json(name = "market_cap") + val marketCap: BigDecimal?, + ) { + data class PriceChangePercentage( + @Json(name = "24h") + val h24: BigDecimal, + @Json(name = "1w") + val week1: BigDecimal, + @Json(name = "30d") + val day30: BigDecimal, + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index c7d564c69a..17e656903f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -1,48 +1,61 @@ package com.tangem.datasource.api.stakekit import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody -import com.tangem.datasource.api.stakekit.models.request.RevenueOption -import com.tangem.datasource.api.stakekit.models.request.YieldType +import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse -import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYield -import com.tangem.datasource.api.stakekit.models.response.model.Yield -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapper -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.Path -import retrofit2.http.Query +import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import retrofit2.http.* @Suppress("LongParameterList") interface StakeKitApi { @GET("yields/enabled") suspend fun getMultipleYields( - @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean, - @Query("type") type: YieldType, - @Query("revenueOption") revenueOption: RevenueOption, - @Query("page") page: Int, - @Query("network") network: String, - @Query("limit") limit: Int, + @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean? = null, + @Query("type") type: YieldType? = null, + @Query("revenueOption") revenueOption: RevenueOption? = null, + @Query("page") page: Int? = null, + @Query("network") network: String? = null, + @Query("limit") limit: Int? = null, ): ApiResponse @GET("yields/{integrationId}") suspend fun getSingleYield( @Path("integrationId") integrationId: String, @Query("ledgerWalletAPICompatible") ledgerWalletAPICompatible: Boolean = false, - ): ApiResponse + ): ApiResponse - @GET("yields/balances") + @POST("yields/balances") suspend fun getMultipleYieldBalances( @Body body: List, - ): ApiResponse> + ): ApiResponse> - @GET("yields/{integrationId}/balances") + @POST("yields/{integrationId}/balances") suspend fun getSingleYieldBalance( @Path("integrationId") integrationId: String, @Body body: YieldBalanceRequestBody, - ): ApiResponse + ): ApiResponse> @GET("tokens") - suspend fun getTokens(): ApiResponse> + suspend fun getTokens(): ApiResponse> + + @POST("actions/enter") + suspend fun createEnterAction(@Body body: EnterActionRequestBody): ApiResponse + + @PATCH("transactions/{transactionId}") + suspend fun constructTransaction( + @Path("transactionId") transactionId: String, + @Body body: ConstructTransactionRequestBody, + ): ApiResponse + + @POST("transactions/{transactionId}/submit_hash") + suspend fun submitTransactionHash( + @Path("transactionId") transactionId: String, + @Body body: SubmitTransactionHashRequestBody, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt new file mode 100644 index 0000000000..03921fa39c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/Address.kt @@ -0,0 +1,39 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json + +data class Address( + @Json(name = "address") + val address: String, + @Json(name = "additionalAddresses") + val additionalAddresses: AdditionalAddresses? = null, + @Json(name = "explorerUrl") + val explorerUrl: String? = null, +) { + + data class AdditionalAddresses( + // cosmos-specific + @Json(name = "cosmosPubKey") + val cosmosPubKey: String? = null, + + // binance-specific + @Json(name = "binanceBeaconAddress") + val binanceBeaconAddress: String? = null, + + // solana-specific + @Json(name = "stakeAccounts") + val stakeAccounts: List? = null, + @Json(name = "lidoStakeAccounts") + val lidoStakeAccounts: List? = null, + + // tezos-specific + @Json(name = "tezosPubKey") + val tezosPubKey: String? = null, + + // avalanche-specific + @Json(name = "cAddressBech") + val cAddressBech: String? = null, + @Json(name = "pAddressBech") + val pAddressBech: String? = null, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt new file mode 100644 index 0000000000..60ee34b310 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ConstructTransactionRequestBody.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json +import java.math.BigDecimal + +data class ConstructTransactionRequestBody( + @Json(name = "gasArgs") + val gasArgs: GasArgs? = null, + @Json(name = "ledgerWalletAPICompatible") + val ledgerWalletAPICompatible: Boolean? = null, +) { + data class GasArgs( + // cosmos-specific + @Json(name = "gasPrice") + val gasPrice: BigDecimal? = null, + // EVM eip 1559 specific + @Json(name = "type") + val type: Int? = null, + @Json(name = "maxFeePerGas") + val maxFeePerGas: BigDecimal? = null, + @Json(name = "maxPriorityFeePerGas") + val maxPriorityFeePerGas: BigDecimal? = null, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt new file mode 100644 index 0000000000..6d06f46f5c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/EnterActionRequestBody.kt @@ -0,0 +1,47 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO + +data class EnterActionRequestBody( + @Json(name = "integrationId") + val integrationId: String, + @Json(name = "addresses") + val addresses: Address, + @Json(name = "args") + val args: EnterActionRequestBodyArgs, + @Json(name = "referralCode") + val referralCode: String? = null, +) { + + data class EnterActionRequestBodyArgs( + @Json(name = "amount") + val amount: String, + @Json(name = "validatorAddress") + val validatorAddress: String? = null, + @Json(name = "validatorAddresses") + val validatorAddresses: List? = null, + @Json(name = "providerId") + val providerId: String? = null, + @Json(name = "duration") + val duration: String? = null, + @Json(name = "nfts") + val nfts: List? = null, + @Json(name = "ledgerWalletAPICompatible") + val ledgerWalletAPICompatible: Boolean? = null, + @Json(name = "tronResource") + val tronResource: String? = null, + @Json(name = "signatureVerification") + val signatureVerification: SignatureVerification? = null, + @Json(name = "inputToken") + val inputToken: TokenDTO? = null, + ) + + data class SignatureVerification( + @Json(name = "message") + val message: String, + @Json(name = "signed") + val signed: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt new file mode 100644 index 0000000000..c07a103fe1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/SubmitTransactionHashRequestBody.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.api.stakekit.models.request + +import com.squareup.moshi.Json + +data class SubmitTransactionHashRequestBody( + @Json(name = "hash") + val hash: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt index 7433386e73..b950b05390 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/YieldBalanceRequestBody.kt @@ -5,26 +5,9 @@ import com.squareup.moshi.Json data class YieldBalanceRequestBody( @Json(name = "addresses") val addresses: Address, @Json(name = "args") val args: YieldBalanceRequestArgs, - @Json(name = "integrationId") val integrationId: String? = null, + @Json(name = "integrationId") val integrationId: String, ) { - data class Address( - @Json(name = "address") val address: String, - @Json(name = "additionalAddresses") val additionalAddresses: AdditionalAddresses? = null, - @Json(name = "explorerUrl") val explorerUrl: String, - ) { - - data class AdditionalAddresses( - @Json(name = "cosmosPubKey") val cosmosPubKey: String? = null, - @Json(name = "binanceBeaconAddress") val binanceBeaconAddress: String? = null, - @Json(name = "stakeAccounts") val stakeAccounts: List? = null, - @Json(name = "lidoStakeAccounts") val lidoStakeAccounts: List? = null, - @Json(name = "tezosPubKey") val tezosPubKey: String? = null, - @Json(name = "cAddressBech") val cAddressBech: String? = null, - @Json(name = "pAddressBech") val pAddressBech: String? = null, - ) - } - data class YieldBalanceRequestArgs( @Json(name = "validatorAddresses") val validatorAddresses: List, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt index c4a958c435..15605bc27c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnabledYieldsResponse.kt @@ -2,12 +2,12 @@ package com.tangem.datasource.api.stakekit.models.response import com.squareup.moshi.Json import com.squareup.moshi.JsonClass -import com.tangem.datasource.api.stakekit.models.response.model.Yield +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO @JsonClass(generateAdapter = true) data class EnabledYieldsResponse( @Json(name = "data") - val data: List, + val data: List, @Json(name = "hasNextPage") val hasNextPage: Boolean, @Json(name = "limit") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt new file mode 100644 index 0000000000..9413c43398 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt @@ -0,0 +1,33 @@ +package com.tangem.datasource.api.stakekit.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO +import org.joda.time.DateTime +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class EnterActionResponse( + @Json(name = "id") + val id: String, + @Json(name = "integrationId") + val integrationId: String, + @Json(name = "status") + val status: StakingActionStatusDTO, + @Json(name = "type") + val type: StakingActionTypeDTO, + @Json(name = "currentStepIndex") + val currentStepIndex: Int, + @Json(name = "amount") + val amount: BigDecimal, + @Json(name = "validatorAddress") + val validatorAddress: String?, + @Json(name = "validatorAddresses") + val validatorAddresses: List?, + @Json(name = "transactions") + val transactions: List?, + @Json(name = "createdAt") + val createdAt: DateTime, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt similarity index 77% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt index 301b7b5805..7a4f33b288 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgument.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/AddressArgumentDTO.kt @@ -4,13 +4,13 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class AddressArgument( +data class AddressArgumentDTO( @Json(name = "required") val required: Boolean, @Json(name = "network") val network: String? = null, @Json(name = "minimum") - val minimum: Int? = null, + val minimum: Double? = null, @Json(name = "maximum") - val maximum: Int? = null, + val maximum: Double? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt new file mode 100644 index 0000000000..c7fedd5ec8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/NetworkTypeDTO.kt @@ -0,0 +1,205 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json + +enum class NetworkTypeDTO { + @Json(name = "avalanche-c") + AVALANCHE_C, + + @Json(name = "avalanche-atomic") + AVALANCHE_ATOMIC, + + @Json(name = "avalanche-p") + AVALANCHE_P, + + @Json(name = "arbitrum") + ARBITRUM, + + @Json(name = "binance") + BINANCE, + + @Json(name = "celo") + CELO, + + @Json(name = "ethereum") + ETHEREUM, + + @Json(name = "ethereum-goerli") + ETHEREUM_GOERLI, + + @Json(name = "ethereum-holesky") + ETHEREUM_HOLESKY, + + @Json(name = "fantom") + FANTOM, + + @Json(name = "harmony") + HARMONY, + + @Json(name = "optimism") + OPTIMISM, + + @Json(name = "polygon") + POLYGON, + + @Json(name = "gnosis") + GNOSIS, + + @Json(name = "moonriver") + MOONRIVER, + + @Json(name = "okc") + OKC, + + @Json(name = "zksync") + ZKSYNC, + + @Json(name = "viction") + VICTION, + + @Json(name = "agoric") + AGORIC, + + @Json(name = "akash") + AKASH, + + @Json(name = "axelar") + AXELAR, + + @Json(name = "band-protocol") + BAND_PROTOCOL, + + @Json(name = "bitsong") + BITSONG, + + @Json(name = "canto") + CANTO, + + @Json(name = "chihuahua") + CHIHUAHUA, + + @Json(name = "comdex") + COMDEX, + + @Json(name = "coreum") + COREUM, + + @Json(name = "cosmos") + COSMOS, + + @Json(name = "crescent") + CRESCENT, + + @Json(name = "cronos") + CRONOS, + + @Json(name = "cudos") + CUDOS, + + @Json(name = "desmos") + DESMOS, + + @Json(name = "dydx") + DYDX, + + @Json(name = "evmos") + EVMOS, + + @Json(name = "fetch-ai") + FETCH_AI, + + @Json(name = "gravity-bridge") + GRAVITY_BRIDGE, + + @Json(name = "injective") + INJECTIVE, + + @Json(name = "irisnet") + IRISNET, + + @Json(name = "juno") + JUNO, + + @Json(name = "kava") + KAVA, + + @Json(name = "ki-network") + KI_NETWORK, + + @Json(name = "mars-protocol") + MARS_PROTOCOL, + + @Json(name = "nym") + NYM, + + @Json(name = "okex-chain") + OKEX_CHAIN, + + @Json(name = "onomy") + ONOMY, + + @Json(name = "osmosis") + OSMOSIS, + + @Json(name = "persistence") + PERSISTENCE, + + @Json(name = "quicksilver") + QUICKSILVER, + + @Json(name = "regen") + REGEN, + + @Json(name = "secret") + SECRET, + + @Json(name = "sentinel") + SENTINEL, + + @Json(name = "sommelier") + SOMMELIER, + + @Json(name = "stafi") + STAFI, + + @Json(name = "stargaze") + STARGAZE, + + @Json(name = "stride") + STRIDE, + + @Json(name = "teritori") + TERITORI, + + @Json(name = "tgrade") + TGRADE, + + @Json(name = "umee") + UMEE, + + @Json(name = "polkadot") + POLKADOT, + + @Json(name = "kusama") + KUSAMA, + + @Json(name = "westend") + WESTEND, + + @Json(name = "binancebeacon") + BINANCEBEACON, + + @Json(name = "near") + NEAR, + + @Json(name = "solana") + SOLANA, + + @Json(name = "tezos") + TEZOS, + + @Json(name = "tron") + TRON, + + UNKNOWN, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt deleted file mode 100644 index 88d5d12b59..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Token.kt +++ /dev/null @@ -1,218 +0,0 @@ -package com.tangem.datasource.api.stakekit.models.response.model - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class Token( - @Json(name = "name") val name: String, - @Json(name = "network") val network: NetworkType, - @Json(name = "symbol") val symbol: String, - @Json(name = "decimals") val decimals: Int, - @Json(name = "address") val address: String?, - @Json(name = "coinGeckoId") val coinGeckoId: String?, - @Json(name = "logoURI") val logoURI: String?, - @Json(name = "isPoints") val isPoints: Boolean?, -) { - enum class NetworkType { - @Json(name = "avalanche-c") - AVALANCHE_C, - - @Json(name = "avalanche-atomic") - AVALANCHE_ATOMIC, - - @Json(name = "avalanche-p") - AVALANCHE_P, - - @Json(name = "arbitrum") - ARBITRUM, - - @Json(name = "binance") - BINANCE, - - @Json(name = "celo") - CELO, - - @Json(name = "ethereum") - ETHEREUM, - - @Json(name = "ethereum-goerli") - ETHEREUM_GOERLI, - - @Json(name = "ethereum-holesky") - ETHEREUM_HOLESKY, - - @Json(name = "fantom") - FANTOM, - - @Json(name = "harmony") - HARMONY, - - @Json(name = "optimism") - OPTIMISM, - - @Json(name = "polygon") - POLYGON, - - @Json(name = "gnosis") - GNOSIS, - - @Json(name = "moonriver") - MOONRIVER, - - @Json(name = "okc") - OKC, - - @Json(name = "zksync") - ZKSYNC, - - @Json(name = "viction") - VICTION, - - @Json(name = "agoric") - AGORIC, - - @Json(name = "akash") - AKASH, - - @Json(name = "axelar") - AXELAR, - - @Json(name = "band-protocol") - BAND_PROTOCOL, - - @Json(name = "bitsong") - BITSONG, - - @Json(name = "canto") - CANTO, - - @Json(name = "chihuahua") - CHIHUAHUA, - - @Json(name = "comdex") - COMDEX, - - @Json(name = "coreum") - COREUM, - - @Json(name = "cosmos") - COSMOS, - - @Json(name = "crescent") - CRESCENT, - - @Json(name = "cronos") - CRONOS, - - @Json(name = "cudos") - CUDOS, - - @Json(name = "desmos") - DESMOS, - - @Json(name = "dydx") - DYDX, - - @Json(name = "evmos") - EVMOS, - - @Json(name = "fetch-ai") - FETCH_AI, - - @Json(name = "gravity-bridge") - GRAVITY_BRIDGE, - - @Json(name = "injective") - INJECTIVE, - - @Json(name = "irisnet") - IRISNET, - - @Json(name = "juno") - JUNO, - - @Json(name = "kava") - KAVA, - - @Json(name = "ki-network") - KI_NETWORK, - - @Json(name = "mars-protocol") - MARS_PROTOCOL, - - @Json(name = "nym") - NYM, - - @Json(name = "okex-chain") - OKEX_CHAIN, - - @Json(name = "onomy") - ONOMY, - - @Json(name = "osmosis") - OSMOSIS, - - @Json(name = "persistence") - PERSISTENCE, - - @Json(name = "quicksilver") - QUICKSILVER, - - @Json(name = "regen") - REGEN, - - @Json(name = "secret") - SECRET, - - @Json(name = "sentinel") - SENTINEL, - - @Json(name = "sommelier") - SOMMELIER, - - @Json(name = "stafi") - STAFI, - - @Json(name = "stargaze") - STARGAZE, - - @Json(name = "stride") - STRIDE, - - @Json(name = "teritori") - TERITORI, - - @Json(name = "tgrade") - TGRADE, - - @Json(name = "umee") - UMEE, - - @Json(name = "polkadot") - POLKADOT, - - @Json(name = "kusama") - KUSAMA, - - @Json(name = "westend") - WESTEND, - - @Json(name = "binancebeacon") - BINANCEBEACON, - - @Json(name = "near") - NEAR, - - @Json(name = "solana") - SOLANA, - - @Json(name = "tezos") - TEZOS, - - @Json(name = "tron") - TRON, - - UNKNOWN, - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt new file mode 100644 index 0000000000..62eb0a4c95 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenDTO.kt @@ -0,0 +1,24 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TokenDTO( + @Json(name = "name") + val name: String, + @Json(name = "network") + val network: NetworkTypeDTO, + @Json(name = "symbol") + val symbol: String, + @Json(name = "decimals") + val decimals: Int, + @Json(name = "address") + val address: String?, + @Json(name = "coinGeckoId") + val coinGeckoId: String?, + @Json(name = "logoURI") + val logoURI: String?, + @Json(name = "isPoints") + val isPoints: Boolean?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt similarity index 75% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt index 2bc30185af..15182ef389 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/TokenWithYieldDTO.kt @@ -4,7 +4,7 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class TokenWithYield( - @Json(name = "token") val token: Token, +data class TokenWithYieldDTO( + @Json(name = "token") val token: TokenDTO, @Json(name = "availableYields") val availableYieldIds: List, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt deleted file mode 100644 index 01f0e26c16..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapper.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.datasource.api.stakekit.models.response.model - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.joda.time.DateTime -import java.math.BigDecimal - -@JsonClass(generateAdapter = true) -data class YieldBalanceWrapper( - @Json(name = "balances") - val balances: List, - @Json(name = "integrationId") - val integrationId: String?, -) { - - @JsonClass(generateAdapter = true) - data class Balance( - @Json(name = "groupId") - val groupId: String, - @Json(name = "type") - val type: BalanceType, - @Json(name = "amount") - val amount: BigDecimal, - @Json(name = "date") - val date: DateTime?, - @Json(name = "pricePerShare") - val pricePerShare: BigDecimal, - @Json(name = "pendingActions") - val pendingActions: List, - @Json(name = "token") - val token: Token, - @Json(name = "validatorAddress") - val validatorAddress: String?, - @Json(name = "validatorAddresses") - val validatorAddresses: List?, - @Json(name = "providerId") - val providerId: String?, - ) { - - enum class BalanceType { - @Json(name = "available") - AVAILABLE, - - @Json(name = "staked") - STAKED, - - @Json(name = "unstaking") - UNSTAKING, - - @Json(name = "unstaked") - UNSTAKED, - - @Json(name = "preparing") - PREPARING, - - @Json(name = "rewards") - REWARDS, - - @Json(name = "locked") - LOCKED, - - @Json(name = "unlocking") - UNLOCKING, - } - - @JsonClass(generateAdapter = true) - data class PendingAction( - @Json(name = "type") - val type: StakingActionType, - @Json(name = "passthrough") - val passthrough: String, - @Json(name = "args") - val args: PendingActionArgs?, - ) { - @JsonClass(generateAdapter = true) - data class PendingActionArgs( - @Json(name = "amount") - val amount: Amount?, - @Json(name = "duration") - val duration: Duration?, - @Json(name = "validatorAddress") - val validatorAddress: Required?, - @Json(name = "validatorAddresses") - val validatorAddresses: Required?, - @Json(name = "nfts") - val nfts: List?, - @Json(name = "tronResource") - val tronResource: TronResource?, - @Json(name = "signatureVerification") - val signatureVerification: Required?, - ) { - @JsonClass(generateAdapter = true) - data class Amount( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: BigDecimal?, - @Json(name = "maximum") - val maximum: BigDecimal?, - ) - - @JsonClass(generateAdapter = true) - data class Duration( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: Int?, - @Json(name = "maximum") - val maximum: Int?, - ) - - @JsonClass(generateAdapter = true) - data class Nft( - @Json(name = "baycId") - val baycId: Required?, - @Json(name = "maycId") - val maycId: Required?, - @Json(name = "bakcId") - val bakcId: Required?, - ) - - @JsonClass(generateAdapter = true) - data class TronResource( - @Json(name = "required") - val required: Boolean, - @Json(name = "options") - val options: List, - ) - } - } - - @JsonClass(generateAdapter = true) - data class Required( - @Json(name = "required") - val required: Boolean, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt new file mode 100644 index 0000000000..5be46fc967 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalanceWrapperDTO.kt @@ -0,0 +1,140 @@ +package com.tangem.datasource.api.stakekit.models.response.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO +import org.joda.time.DateTime +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class YieldBalanceWrapperDTO( + @Json(name = "balances") + val balances: List, + @Json(name = "integrationId") + val integrationId: String?, +) + +@JsonClass(generateAdapter = true) +data class BalanceDTO( + @Json(name = "groupId") + val groupId: String, + @Json(name = "type") + val type: BalanceType, + @Json(name = "amount") + val amount: BigDecimal, + @Json(name = "date") + val date: DateTime?, + @Json(name = "pricePerShare") + val pricePerShare: BigDecimal, + @Json(name = "pendingActions") + val pendingActions: List, + @Json(name = "token") + val tokenDTO: TokenDTO, + @Json(name = "validatorAddress") + val validatorAddress: String?, + @Json(name = "validatorAddresses") + val validatorAddresses: List?, + @Json(name = "providerId") + val providerId: String?, +) { + + enum class BalanceType { + @Json(name = "available") + AVAILABLE, + + @Json(name = "staked") + STAKED, + + @Json(name = "unstaking") + UNSTAKING, + + @Json(name = "unstaked") + UNSTAKED, + + @Json(name = "preparing") + PREPARING, + + @Json(name = "rewards") + REWARDS, + + @Json(name = "locked") + LOCKED, + + @Json(name = "unlocking") + UNLOCKING, + + UNKNOWN, + } + + @JsonClass(generateAdapter = true) + data class PendingAction( + @Json(name = "type") + val type: StakingActionTypeDTO, + @Json(name = "passthrough") + val passthrough: String, + @Json(name = "args") + val args: PendingActionArgs?, + ) { + @JsonClass(generateAdapter = true) + data class PendingActionArgs( + @Json(name = "amount") + val amount: Amount?, + @Json(name = "duration") + val duration: Duration?, + @Json(name = "validatorAddress") + val validatorAddress: Required?, + @Json(name = "validatorAddresses") + val validatorAddresses: Required?, + @Json(name = "nfts") + val nfts: List?, + @Json(name = "tronResource") + val tronResource: TronResource?, + @Json(name = "signatureVerification") + val signatureVerification: Required?, + ) { + @JsonClass(generateAdapter = true) + data class Amount( + @Json(name = "required") + val required: Boolean, + @Json(name = "minimum") + val minimum: BigDecimal?, + @Json(name = "maximum") + val maximum: BigDecimal?, + ) + + @JsonClass(generateAdapter = true) + data class Duration( + @Json(name = "required") + val required: Boolean, + @Json(name = "minimum") + val minimum: Int?, + @Json(name = "maximum") + val maximum: Int?, + ) + + @JsonClass(generateAdapter = true) + data class Nft( + @Json(name = "baycId") + val baycId: Required?, + @Json(name = "maycId") + val maycId: Required?, + @Json(name = "bakcId") + val bakcId: Required?, + ) + + @JsonClass(generateAdapter = true) + data class TronResource( + @Json(name = "required") + val required: Boolean, + @Json(name = "options") + val options: List, + ) + } + } + + @JsonClass(generateAdapter = true) + data class Required( + @Json(name = "required") + val required: Boolean, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt deleted file mode 100644 index 98637444a4..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldBalances.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.datasource.api.stakekit.models.response.model - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import org.joda.time.DateTime -import java.math.BigDecimal - -@JsonClass(generateAdapter = true) -data class YieldBalances( - @Json(name = "balances") - val balances: List, - @Json(name = "integrationId") - val integrationId: String, -) { - - @JsonClass(generateAdapter = true) - data class Balance( - @Json(name = "groupId") - val groupId: String, - @Json(name = "type") - val type: BalanceType, - @Json(name = "amount") - val amount: BigDecimal, - @Json(name = "date") - val date: DateTime?, - @Json(name = "pricePerShare") - val pricePerShare: BigDecimal, - @Json(name = "pendingActions") - val pendingActions: List, - @Json(name = "token") - val token: Token, - @Json(name = "validatorAddress") - val validatorAddress: String?, - @Json(name = "validatorAddresses") - val validatorAddresses: List?, - @Json(name = "providerId") - val providerId: String?, - ) { - - enum class BalanceType { - @Json(name = "available") - AVAILABLE, - - @Json(name = "staked") - STAKED, - - @Json(name = "unstaking") - UNSTAKING, - - @Json(name = "unstaked") - UNSTAKED, - - @Json(name = "preparing") - PREPARING, - - @Json(name = "rewards") - REWARDS, - - @Json(name = "locked") - LOCKED, - - @Json(name = "unlocking") - UNLOCKING, - } - - @JsonClass(generateAdapter = true) - data class PendingAction( - @Json(name = "type") - val type: StakingActionType, - @Json(name = "passthrough") - val passthrough: String, - @Json(name = "args") - val args: PendingActionArgs?, - ) { - @JsonClass(generateAdapter = true) - data class PendingActionArgs( - @Json(name = "amount") - val amount: Amount?, - @Json(name = "duration") - val duration: Duration?, - @Json(name = "validatorAddress") - val validatorAddress: Required?, - @Json(name = "validatorAddresses") - val validatorAddresses: Required?, - @Json(name = "nfts") - val nfts: List?, - @Json(name = "tronResource") - val tronResource: TronResource?, - @Json(name = "signatureVerification") - val signatureVerification: Required?, - ) { - @JsonClass(generateAdapter = true) - data class Amount( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: BigDecimal?, - @Json(name = "maximum") - val maximum: BigDecimal?, - ) - - @JsonClass(generateAdapter = true) - data class Duration( - @Json(name = "required") - val required: Boolean, - @Json(name = "minimum") - val minimum: Int?, - @Json(name = "maximum") - val maximum: Int?, - ) - - @JsonClass(generateAdapter = true) - data class Nft( - @Json(name = "baycId") - val baycId: Required?, - @Json(name = "maycId") - val maycId: Required?, - @Json(name = "bakcId") - val bakcId: Required?, - ) - - @JsonClass(generateAdapter = true) - data class TronResource( - @Json(name = "required") - val required: Boolean, - @Json(name = "options") - val options: List, - ) - } - } - - @JsonClass(generateAdapter = true) - data class Required( - @Json(name = "required") - val required: Boolean, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt similarity index 78% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index c122022402..bf8956f11f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/Yield.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -5,33 +5,33 @@ import com.squareup.moshi.JsonClass import java.math.BigDecimal @JsonClass(generateAdapter = true) -data class Yield( +data class YieldDTO( @Json(name = "id") val id: String, @Json(name = "token") - val token: Token, + val token: TokenDTO, @Json(name = "tokens") - val tokens: List, + val tokens: List, @Json(name = "args") - val args: Args, + val args: ArgsDTO, @Json(name = "status") - val status: Status, + val status: StatusDTO, @Json(name = "apy") val apy: BigDecimal, @Json(name = "rewardRate") val rewardRate: Double, @Json(name = "rewardType") - val rewardType: RewardType, + val rewardType: RewardTypeDTO, @Json(name = "metadata") - val metadata: Metadata, + val metadata: MetadataDTO, @Json(name = "validators") - val validators: List, + val validators: List, @Json(name = "isAvailable") val isAvailable: Boolean, ) { @JsonClass(generateAdapter = true) - data class Status( + data class StatusDTO( @Json(name = "enter") val enter: Boolean, @Json(name = "exit") @@ -39,7 +39,7 @@ data class Yield( ) @JsonClass(generateAdapter = true) - data class Args( + data class ArgsDTO( @Json(name = "enter") val enter: Enter, @Json(name = "exit") @@ -50,20 +50,20 @@ data class Yield( @Json(name = "addresses") val addresses: Addresses, @Json(name = "args") - val args: Map, + val args: Map, ) { @JsonClass(generateAdapter = true) data class Addresses( @Json(name = "address") - val address: AddressArgument, + val address: AddressArgumentDTO, @Json(name = "additionalAddresses") - val additionalAddresses: Map? = null, + val additionalAddresses: Map? = null, ) } } @JsonClass(generateAdapter = true) - data class Validator( + data class ValidatorDTO( @Json(name = "address") val address: String, @Json(name = "status") @@ -75,7 +75,7 @@ data class Yield( @Json(name = "website") val website: String?, @Json(name = "apr") - val apr: Double?, + val apr: BigDecimal?, @Json(name = "commission") val commission: Double?, @Json(name = "stakedBalance") @@ -87,7 +87,7 @@ data class Yield( ) @JsonClass(generateAdapter = true) - data class Metadata( + data class MetadataDTO( @Json(name = "name") val name: String, @Json(name = "logoURI") @@ -97,19 +97,19 @@ data class Yield( @Json(name = "documentation") val documentation: String?, @Json(name = "gasFeeToken") - val gasFeeToken: Token, + val gasFeeTokenDTO: TokenDTO, @Json(name = "token") - val token: Token, + val tokenDTO: TokenDTO, @Json(name = "tokens") - val tokens: List, + val tokensDTO: List, @Json(name = "type") val type: String, @Json(name = "rewardSchedule") val rewardSchedule: String, @Json(name = "cooldownPeriod") - val cooldownPeriod: Period, + val cooldownPeriod: PeriodDTO, @Json(name = "warmupPeriod") - val warmupPeriod: Period, + val warmupPeriod: PeriodDTO, @Json(name = "rewardClaiming") val rewardClaiming: String, @Json(name = "defaultValidator") @@ -119,28 +119,30 @@ data class Yield( @Json(name = "supportsMultipleValidators") val supportsMultipleValidators: Boolean, @Json(name = "revshare") - val revshare: Enabled, + val revshare: EnabledDTO, @Json(name = "fee") - val fee: Enabled, + val fee: EnabledDTO, ) { @JsonClass(generateAdapter = true) - data class Period( + data class PeriodDTO( @Json(name = "days") val days: Int, ) @JsonClass(generateAdapter = true) - data class Enabled( + data class EnabledDTO( @Json(name = "enabled") val enabled: Boolean, ) } - enum class RewardType { + enum class RewardTypeDTO { @Json(name = "apy") - APY, // auto + APY, // compound rate @Json(name = "apr") - APR, // manual + APR, // simple rate, + + UNKNOWN, } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt new file mode 100644 index 0000000000..040cab01ea --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionStatusDTO.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.api.stakekit.models.response.model.action + +import com.squareup.moshi.Json + +enum class StakingActionStatusDTO { + @Json(name = "CANCELED") + CANCELED, + + @Json(name = "CREATED") + CREATED, + + @Json(name = "WAITING_FOR_NEXT") + WAITING_FOR_NEXT, + + @Json(name = "PROCESSING") + PROCESSING, + + @Json(name = "FAILED") + FAILED, + + @Json(name = "SUCCESS") + SUCCESS, + + UNKNOWN, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt similarity index 93% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt index 5e60da84d9..3ba035dc64 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/StakingActionType.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/action/StakingActionTypeDTO.kt @@ -1,8 +1,8 @@ -package com.tangem.datasource.api.stakekit.models.response.model +package com.tangem.datasource.api.stakekit.models.response.model.action import com.squareup.moshi.Json -enum class StakingActionType { +enum class StakingActionTypeDTO { @Json(name = "STAKE") STAKE, @@ -47,4 +47,6 @@ enum class StakingActionType { @Json(name = "MIGRATE") MIGRATE, + + UNKNOWN, } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt new file mode 100644 index 0000000000..93ee18f790 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingGasEstimateDTO.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import java.math.BigDecimal + +@JsonClass(generateAdapter = true) +data class StakingGasEstimateDTO( + @Json(name = "amount") + val amount: BigDecimal, + @Json(name = "token") + val token: TokenDTO, + @Json(name = "gasLimit") + val gasLimit: String?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionDTO.kt new file mode 100644 index 0000000000..0a28baaeee --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionDTO.kt @@ -0,0 +1,37 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO + +@JsonClass(generateAdapter = true) +data class StakingTransactionDTO( + @Json(name = "id") + val id: String, + @Json(name = "network") + val network: NetworkTypeDTO, + @Json(name = "status") + val status: StakingTransactionStatusDTO, + @Json(name = "type") + val type: StakingTransactionTypeDTO, + @Json(name = "hash") + val hash: String?, + @Json(name = "signedTransaction") + val signedTransaction: String?, + @Json(name = "unsignedTransaction") + val unsignedTransaction: String?, + @Json(name = "stepIndex") + val stepIndex: Int, + @Json(name = "error") + val error: String?, + @Json(name = "gasEstimate") + val gasEstimate: StakingGasEstimateDTO?, + @Json(name = "stakeId") + val stakeId: String?, + @Json(name = "explorerUrl") + val explorerUrl: String?, + @Json(name = "ledgerHwAppId") + val ledgerHwAppId: String?, + @Json(name = "isMessage") + val isMessage: Boolean, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt new file mode 100644 index 0000000000..f7a4f6d76d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionStatusDTO.kt @@ -0,0 +1,37 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction + +import com.squareup.moshi.Json + +enum class StakingTransactionStatusDTO { + @Json(name = "NOT_FOUND") + NOT_FOUND, + + @Json(name = "CREATED") + CREATED, + + @Json(name = "BLOCKED") + BLOCKED, + + @Json(name = "WAITING_FOR_SIGNATURE") + WAITING_FOR_SIGNATURE, + + @Json(name = "SIGNED") + SIGNED, + + @Json(name = "BROADCASTED") + BROADCASTED, + + @Json(name = "PENDING") + PENDING, + + @Json(name = "CONFIRMED") + CONFIRMED, + + @Json(name = "FAILED") + FAILED, + + @Json(name = "SKIPPED") + SKIPPED, + + UNKNOWN, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt new file mode 100644 index 0000000000..e79e28e7bb --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/transaction/StakingTransactionTypeDTO.kt @@ -0,0 +1,124 @@ +package com.tangem.datasource.api.stakekit.models.response.model.transaction + +import com.squareup.moshi.Json + +enum class StakingTransactionTypeDTO { + @Json(name = "SWAP") + SWAP, + + @Json(name = "DEPOSIT") + DEPOSIT, + + @Json(name = "APPROVAL") + APPROVAL, + + @Json(name = "STAKE") + STAKE, + + @Json(name = "CLAIM_UNSTAKED") + CLAIM_UNSTAKED, + + @Json(name = "CLAIM_REWARDS") + CLAIM_REWARDS, + + @Json(name = "RESTAKE_REWARDS") + RESTAKE_REWARDS, + + @Json(name = "UNSTAKE") + UNSTAKE, + + @Json(name = "SPLIT") + SPLIT, + + @Json(name = "MERGE") + MERGE, + + @Json(name = "LOCK") + LOCK, + + @Json(name = "UNLOCK") + UNLOCK, + + @Json(name = "SUPPLY") + SUPPLY, + + @Json(name = "BRIDGE") + BRIDGE, + + @Json(name = "VOTE") + VOTE, + + @Json(name = "REVOKE") + REVOKE, + + @Json(name = "RESTAKE") + RESTAKE, + + @Json(name = "REBOND") + REBOND, + + @Json(name = "WITHDRAW") + WITHDRAW, + + @Json(name = "CREATE_ACCOUNT") + CREATE_ACCOUNT, + + @Json(name = "REVEAL") + REVEAL, + + @Json(name = "MIGRATE") + MIGRATE, + + @Json(name = "UTXO_P_TO_C_IMPORT") + UTXO_P_TO_C_IMPORT, + + @Json(name = "UTXO_C_TO_P_IMPORT") + UTXO_C_TO_P_IMPORT, + + @Json(name = "UNFREEZE_LEGACY") + UNFREEZE_LEGACY, + + @Json(name = "UNFREEZE_LEGACY_BANDWIDTH") + UNFREEZE_LEGACY_BANDWIDTH, + + @Json(name = "UNFREEZE_LEGACY_ENERGY") + UNFREEZE_LEGACY_ENERGY, + + @Json(name = "UNFREEZE_BANDWIDTH") + UNFREEZE_BANDWIDTH, + + @Json(name = "UNFREEZE_ENERGY") + UNFREEZE_ENERGY, + + @Json(name = "FREEZE_BANDWIDTH") + FREEZE_BANDWIDTH, + + @Json(name = "FREEZE_ENERGY") + FREEZE_ENERGY, + + @Json(name = "UNDELEGATE_BANDWIDTH") + UNDELEGATE_BANDWIDTH, + + @Json(name = "UNDELEGATE_ENERGY") + UNDELEGATE_ENERGY, + + @Json(name = "P2P_NODE_REQUEST") + P2P_NODE_REQUEST, + + @Json(name = "LUGANODES_PROVISION") + LUGANODES_PROVISION, + + @Json(name = "LUGANODES_EXIT_REQUEST") + LUGANODES_EXIT_REQUEST, + + @Json(name = "INFSTONES_PROVISION") + INFSTONES_PROVISION, + + @Json(name = "INFSTONES_EXIT_REQUEST") + INFSTONES_EXIT_REQUEST, + + @Json(name = "INFSTONES_CLAIM_REQUEST") + INFSTONES_CLAIM_REQUEST, + + UNKNOWN, +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt index 2155d0bf77..e1e9e07e40 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt @@ -12,6 +12,10 @@ data class QuotesResponse( @Json(name = "price") val price: BigDecimal?, @Json(name = "priceChange24h") - val priceChange: BigDecimal?, + val priceChange24h: BigDecimal?, + @Json(name = "priceChange1w") + val priceChange1w: BigDecimal?, + @Json(name = "priceChange30d") + val priceChange30d: BigDecimal?, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 6bda8dd093..36bba25d83 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -103,6 +103,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { polygonScanApiKey = configValues.polygonScanApiKey, bittensorDwellirApiKey = configValues.bittensorDwellirApiKey, bittensorOnfinalityApiKey = configValues.bittensorOnfinalityKey, + koinosProApiKey = configValues.koinosProApiKey, ), amplitudeApiKey = configValues.amplitudeApiKey, sprinklr = configValues.sprinklr, diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index 685b4e3ffe..a6929291e1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -46,6 +46,7 @@ class ConfigValueModel( val stakeKitApiKey: String?, @Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?, @Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?, + @Json(name = "koinosProApiKey") val koinosProApiKey: String?, ) @JsonClass(generateAdapter = true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt similarity index 52% rename from core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt rename to core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt index 43b8e93f5c..cf51b2b006 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/AssetsStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ExpressAssetsStoreModule.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.token.DefaultAssetsStore -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.DefaultExpressAssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,11 +11,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object AssetsStoreModule { +internal object ExpressAssetsStoreModule { @Provides @Singleton - fun provideAssetsStore(): AssetsStore { - return DefaultAssetsStore(dataStore = RuntimeDataStore()) + fun provideExpressAssetsStore(): ExpressAssetsStore { + return DefaultExpressAssetsStore(dataStore = RuntimeDataStore()) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index ddd3386e58..e04042a8b4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -4,11 +4,10 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.adapters.PolymorphicJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter +import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.api.common.adapter.BigDecimalAdapter import com.tangem.datasource.api.common.adapter.DateTimeAdapter import com.tangem.datasource.api.common.adapter.LocalDateAdapter -import com.tangem.datasource.api.common.adapter.UnknownEnumMoshiAdapter -import com.tangem.datasource.api.stakekit.models.response.model.Token import com.tangem.datasource.config.models.ProviderModel import dagger.Module import dagger.Provides @@ -35,10 +34,7 @@ class MoshiModule { .add(LocalDateAdapter()) .add(DateTimeAdapter()) .add(KotlinJsonAdapterFactory()) - .add( - Token.NetworkType::class.java, - UnknownEnumMoshiAdapter.create(Token.NetworkType::class.java, Token.NetworkType.UNKNOWN), - ) + .addStakeKitEnumFallbackAdapters() .build() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index ed7b6b5621..d2b395d5f9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -5,6 +5,7 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi +import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApiV2 @@ -128,6 +129,23 @@ class NetworkModule { ) } + @Provides + @DevTangemApi + @Singleton + fun provideCoinMarketsApi( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appVersionProvider: AppVersionProvider, + ): TangemTechMarketsApi { + return provideTangemTechApiInternal( + moshi = moshi, + context = context, + appVersionProvider = appVersionProvider, + baseUrl = DEV_V1_TANGEM_TECH_BASE_URL, + requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)), + ) + } + private inline fun provideTangemTechApiInternal( moshi: Moshi, context: Context, diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt new file mode 100644 index 0000000000..21d78318c1 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.DefaultStakingBalanceStore +import com.tangem.datasource.local.token.StakingBalanceStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object StakingBalanceStoreModule { + + @Provides + @Singleton + fun provideStakingBalanceStore(): StakingBalanceStore { + return DefaultStakingBalanceStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt new file mode 100644 index 0000000000..45e280e949 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.DefaultStakingYieldsStore +import com.tangem.datasource.local.token.StakingYieldsStore +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object StakingTokensStoreModule { + + @Provides + @Singleton + fun provideStakingTokensStore(): StakingYieldsStore { + return DefaultStakingYieldsStore(dataStore = RuntimeDataStore()) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt index 757c01cef5..b2997661fb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/UserTokensStoreModule.kt @@ -1,11 +1,11 @@ package com.tangem.datasource.di -import com.squareup.moshi.Moshi -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.files.FileReader -import com.tangem.datasource.local.datastore.FileDataStore -import com.tangem.datasource.local.token.DefaultUserTokensStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.AppPreferencesUserTokensStore import com.tangem.datasource.local.token.UserTokensStore +import com.tangem.datasource.local.token.UserTokensStoreMigrationRunner +import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,9 +18,17 @@ internal object UserTokensStoreModule { @Provides @Singleton - fun provideUserTokensStore(fileReader: FileReader, @NetworkMoshi moshi: Moshi): UserTokensStore { - return DefaultUserTokensStore( - dataStore = FileDataStore(fileReader, moshi.adapter(UserTokensResponse::class.java)), + fun provideUserTokensStore( + appPreferencesStore: AppPreferencesStore, + userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, + userWalletsStore: UserWalletsStore, + dispatchers: CoroutineDispatcherProvider, + ): UserTokensStore { + return AppPreferencesUserTokensStore( + appPreferencesStore = appPreferencesStore, + userTokensStoreMigrationRunner = userTokensStoreMigrationRunner, + userWalletsStore = userWalletsStore, + dispatchers = dispatchers, ) } } \ No newline at end of file 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 52b037fd6d..722573fc37 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 @@ -94,6 +94,22 @@ object PreferencesKeys { } fun getStart2CoinTOSAcceptedKey(region: String?) = booleanPreferencesKey(name = "start2Coin_tos_accepted_$region") + + // region Permission + fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission") + + fun getShouldShowInitialPermissionScreen(permission: String) = + booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission") + + fun getIsFirstTimeAskingPermission(permission: String) = + booleanPreferencesKey("shouldAskInitialPushPermission_$permission") + + fun getPermissionLaunchCount(permission: String) = intPreferencesKey("pushPermissionLaunchCount_$permission") + + fun getPermissionDaysCount(permission: String) = longPreferencesKey("pushPermissionDaysCount_$permission") + // endregion + + fun getUserTokensKey(userWalletId: String) = stringPreferencesKey(name = "user_tokens_$userWalletId") } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt new file mode 100644 index 0000000000..80d70825bf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/AppPreferencesUserTokensStore.kt @@ -0,0 +1,63 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +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.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +/** + * Implementation of [UserTokensStore] that based on [appPreferencesStore] + * + * @property appPreferencesStore application preference store + * +[REDACTED_AUTHOR] + */ +internal class AppPreferencesUserTokensStore( + private val appPreferencesStore: AppPreferencesStore, + private val userTokensStoreMigrationRunner: UserTokensStoreMigrationRunner, + private val userWalletsStore: UserWalletsStore, + private val dispatchers: CoroutineDispatcherProvider, +) : UserTokensStore { + + init { + runUserTokensMigrations() + } + + override fun get(key: UserWalletId): Flow { + return appPreferencesStore + .getObject(PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue)) + .filterNotNull() + } + + override suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? { + return appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), + ) + } + + override suspend fun store(key: UserWalletId, value: UserTokensResponse) { + appPreferencesStore.storeObject( + key = PreferencesKeys.getUserTokensKey(userWalletId = key.stringValue), + value = value, + ) + } + + // TODO: delete in 5.15 (Mobile Sprint 161) [REDACTED_JIRA] + private fun runUserTokensMigrations() { + userWalletsStore.userWallets + .filter { it.isNotEmpty() } + .take(1) + .onEach { userWallets -> + userTokensStoreMigrationRunner.run(ids = userWallets.map { it.walletId.stringValue }) + } + .flowOn(dispatchers.io) + .launchIn(CoroutineScope(dispatchers.io)) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt similarity index 89% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt index 9177ec70b7..c67abc06e5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultAssetsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultExpressAssetsStore.kt @@ -4,9 +4,9 @@ import com.tangem.datasource.api.express.models.response.Asset import com.tangem.datasource.local.datastore.core.StringKeyDataStore import com.tangem.domain.wallets.models.UserWalletId -internal class DefaultAssetsStore( +internal class DefaultExpressAssetsStore( private val dataStore: StringKeyDataStore>, -) : AssetsStore { +) : ExpressAssetsStore { override suspend fun getSyncOrNull(userWalletId: UserWalletId): List? { return dataStore.getSyncOrNull(userWalletId.stringValue) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt new file mode 100644 index 0000000000..c88970b238 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -0,0 +1,51 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.utils.extensions.addOrReplace +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class DefaultStakingBalanceStore( + private val dataStore: StringKeyDataStore>, +) : StakingBalanceStore { + + override fun get(): Flow> { + return dataStore.get(STAKING_BALANCE_KEY) + } + + override suspend fun getSyncOrNull(): List? { + return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + } + + override suspend fun store(items: List) { + return dataStore.store(STAKING_BALANCE_KEY, items) + } + + override fun get(integrationId: String): Flow> { + return dataStore.get(STAKING_BALANCE_KEY) + .map { balances -> + balances.filter { it.integrationId == integrationId } + .flatMap { it.balances } + } + } + + override suspend fun getSyncOrNull(integrationId: String): List? { + return dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + ?.firstOrNull { it.integrationId == integrationId }?.balances + } + + override suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) { + val balances = dataStore.getSyncOrNull(STAKING_BALANCE_KEY) + ?.toMutableList() + ?.addOrReplace(item) { item.integrationId == integrationId } + ?: listOf(item) + + return dataStore.store(STAKING_BALANCE_KEY, balances) + } + + companion object { + private const val STAKING_BALANCE_KEY = "STAKING_BALANCE_KEY" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt new file mode 100644 index 0000000000..a74afc914e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingTokensStore.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.staking.model.StakingTokenWithYield + +internal class DefaultStakingTokensStore( + private val dataStore: StringKeyDataStore>, +) : StakingTokensStore { + + override suspend fun getSyncOrNull(): List? { + return dataStore.getSyncOrNull(STAKING_TOKENS_KEY) + } + + override suspend fun store(items: List) { + dataStore.store(STAKING_TOKENS_KEY, items) + } + + companion object { + private const val STAKING_TOKENS_KEY = "STAKING_TOKENS_KEY" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt new file mode 100644 index 0000000000..1cdb6c381c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.local.datastore.core.StringKeyDataStore + +internal class DefaultStakingYieldsStore( + private val dataStore: StringKeyDataStore>, +) : StakingYieldsStore { + + override suspend fun getSyncOrNull(): List? { + return dataStore.getSyncOrNull(STAKING_YIELDS_KEY) + } + + override suspend fun store(items: List) { + dataStore.store(STAKING_YIELDS_KEY, items) + } + + companion object { + private const val STAKING_YIELDS_KEY = "STAKING_YIELDS_KEY" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt similarity index 90% rename from core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt index 11d6101843..353a15c7e8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/AssetsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/ExpressAssetsStore.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.express.models.response.Asset import com.tangem.domain.wallets.models.UserWalletId -interface AssetsStore { +interface ExpressAssetsStore { suspend fun getSyncOrNull(userWalletId: UserWalletId): List? diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt new file mode 100644 index 0000000000..0a9ea06c9e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -0,0 +1,20 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import kotlinx.coroutines.flow.Flow + +interface StakingBalanceStore { + + fun get(): Flow> + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) + + fun get(integrationId: String): Flow> + + suspend fun getSyncOrNull(integrationId: String): List? + + suspend fun store(integrationId: String, item: YieldBalanceWrapperDTO) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt new file mode 100644 index 0000000000..586674756b --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingTokensStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.token + +import com.tangem.domain.staking.model.StakingTokenWithYield + +interface StakingTokensStore { + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt new file mode 100644 index 0000000000..61f46cf15d --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO + +interface StakingYieldsStore { + + suspend fun getSyncOrNull(): List? + + suspend fun store(items: List) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt index 08fcb31438..f3f12f026b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStore.kt @@ -4,11 +4,43 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +@Deprecated( + message = "Use AppPreferencesStore", + replaceWith = ReplaceWith( + expression = "AppPreferencesStore", + imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), + ), + level = DeprecationLevel.WARNING, +) interface UserTokensStore { + @Deprecated( + message = "Use getObject", + replaceWith = ReplaceWith( + expression = "appPreferencesStore.getObject(userWalletId)", + imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), + ), + level = DeprecationLevel.WARNING, + ) fun get(key: UserWalletId): Flow + @Deprecated( + message = "Use getObjectSyncOrNull", + replaceWith = ReplaceWith( + expression = "appPreferencesStore.getObjectSyncOrNull(userWalletId)", + imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), + ), + level = DeprecationLevel.WARNING, + ) suspend fun getSyncOrNull(key: UserWalletId): UserTokensResponse? + @Deprecated( + message = "Use storeObject", + replaceWith = ReplaceWith( + expression = "appPreferencesStore.storeObject(userWalletId, response)", + imports = arrayOf("com.tangem.datasource.local.preferences.AppPreferencesStore"), + ), + level = DeprecationLevel.WARNING, + ) suspend fun store(key: UserWalletId, value: UserTokensResponse) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt new file mode 100644 index 0000000000..dae09c0616 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigration.kt @@ -0,0 +1,56 @@ +package com.tangem.datasource.local.token + +import androidx.datastore.core.DataMigration +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull +import com.tangem.datasource.local.preferences.utils.storeObject + +/** + * Migration of saving [UserTokensResponse] from file to [AppPreferencesStore] + * + * @param userWalletId user wallet id + * @param moshi moshi + * @property fileReader file reader + * +[REDACTED_AUTHOR] + */ +internal class UserTokensStoreMigration( + userWalletId: String, + moshi: Moshi, + private val fileReader: FileReader, +) : DataMigration { + + private val legacyFileName = "user_tokens_$userWalletId" + private val keyName = PreferencesKeys.getUserTokensKey(userWalletId = userWalletId) + + @OptIn(ExperimentalStdlibApi::class) + private val adapter = moshi.adapter() + + override suspend fun shouldMigrate(currentData: AppPreferencesStore): Boolean = true + + override suspend fun migrate(currentData: AppPreferencesStore): AppPreferencesStore { + val currentKey = currentData.getObjectSyncOrNull(key = keyName) + + if (currentKey != null) return currentData + + val value = runCatching { + val json = fileReader.readFile(legacyFileName) + adapter.fromJson(json) + }.getOrNull() + + if (value != null) { + currentData.storeObject(key = keyName, value = value) + } + + return currentData + } + + override suspend fun cleanUp() { + fileReader.removeFile(legacyFileName) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt new file mode 100644 index 0000000000..eafb84aa79 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/UserTokensStoreMigrationRunner.kt @@ -0,0 +1,50 @@ +package com.tangem.datasource.local.token + +import com.squareup.moshi.Moshi +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.files.FileReader +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Runner that launch migrations of saving user tokens store + * + * @property appPreferencesStore application preference store + * @property fileReader file reader + * @property moshi moshi + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +@Singleton +class UserTokensStoreMigrationRunner @Inject constructor( + private val appPreferencesStore: AppPreferencesStore, + private val fileReader: FileReader, + @NetworkMoshi private val moshi: Moshi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend fun run(ids: List) { + ids.forEach { id -> + coroutineScope { run(id) } + } + } + + private suspend fun run(id: String) { + withContext(dispatchers.io) { + val migration = UserTokensStoreMigration( + userWalletId = id, + moshi = moshi, + fileReader = fileReader, + ) + + migration.migrate(appPreferencesStore) + + migration.cleanUp() + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index bfee20e460..6aa2933ba2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -2,11 +2,14 @@ package com.tangem.datasource.local.userwallet import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow interface UserWalletsStore { val selectedUserWalletOrNull: UserWallet? + val userWallets: Flow> + suspend fun getSyncOrNull(key: UserWalletId): UserWallet? suspend fun getAllSyncOrNull(): List? diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt index c9fa81608f..215b56e9f8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/RequestHeader.kt @@ -35,5 +35,6 @@ sealed class RequestHeader(vararg pairs: Pair String>) { class StakeKit(stakeKitAuthProvider: StakeKitAuthProvider) : RequestHeader( "X-API-KEY" to { stakeKitAuthProvider.getApiKey() }, + "accept" to { "application/json" }, ) } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt index b5df451866..74fba5a48e 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/di/DecomposeComponent.kt @@ -1,5 +1,6 @@ package com.tangem.core.decompose.di +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import dagger.BindsInstance @@ -37,6 +38,14 @@ interface DecomposeComponent { */ fun uiMessageSender(@BindsInstance uiMessageSender: UiMessageSender): Builder + /** + * Sets the parameters container for the component. + * + * @param container The parameters container to set. + * @return The builder instance. + */ + fun paramsContainer(@BindsInstance container: ParamsContainer): Builder + /** * Builds the Decompose component. * diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt new file mode 100644 index 0000000000..20c1159324 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/factory/ComponentFactory.kt @@ -0,0 +1,8 @@ +package com.tangem.core.decompose.factory + +import com.tangem.core.decompose.context.AppComponentContext + +interface ComponentFactory

{ + + fun create(context: AppComponentContext, params: P): C +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt index 58cad4cf7a..286678b4c3 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ModelEntryPoint.kt @@ -22,24 +22,43 @@ interface ModelsEntryPoint { } /** - * Gets or creates a component's [Model]. + * Gets or creates a component's [Model] with no parameters. */ -inline fun AppComponentContext.getOrCreateModel(): M { - val modelKey = "model_${M::class.simpleName}" +inline fun AppComponentContext.getOrCreateModel(): M = getOrCreateModel(params = null) +/** + * Gets or creates a component's [Model]. + * + * Be careful with objects you pass in parameters as they will be used in [Model] lifecycle. + * If you pass a object with different lifecycle than the [Model] then you can face memory leaks. + * + * @param params The parameters to store in the [ParamsContainer], + + */ +inline fun AppComponentContext.getOrCreateModel(params: P?): M { val entryPoint = instanceKeeper.getOrCreateSimple(key = "modelsEntryPoint") { val hiltComponent = hiltComponentBuilder .router(router) .uiMessageSender(messageSender) + .let { builder -> + if (params != null) { + val container = MutableParamsContainer(params) + + builder.paramsContainer(container) + } else { + builder + } + } .build() EntryPoints.get(hiltComponent, ModelsEntryPoint::class.java) } + val modelKey = "model_${M::class.simpleName}" val model = instanceKeeper.getOrCreate(modelKey) { requireNotNull(entryPoint.models()[M::class.java]?.get()) { "Model ${M::class.simpleName} is not provided" - } + } as M } val isModelExist = tags.getOrElse(modelKey) { false } as Boolean @@ -47,5 +66,5 @@ inline fun AppComponentContext.getOrCreateModel(): M { tags[modelKey] = true } - return model as M + return model } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ParamsContainer.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ParamsContainer.kt new file mode 100644 index 0000000000..910764d661 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/model/ParamsContainer.kt @@ -0,0 +1,45 @@ +package com.tangem.core.decompose.model + +/** + * Lazy container for [Model] params. + * + * This contrainer can be accessed by DI because it's provided via [com.tangem.core.decompose.di.DecomposeComponent]. + * */ +interface ParamsContainer { + + /** Returns stored value if it is of type [T], otherwise returns null. */ + fun get(): T? + + /** Returns stored value if it is of type [T], otherwise throws an exception. */ + fun require(): T +} + +/** + * Mutable implementation of [ParamsContainer]. + * + * ***You should not use this class directly in other modules, use immutable [ParamsContainer] instead.*** + * */ +class MutableParamsContainer private constructor() : ParamsContainer { + + private var value: Any? = null + + /** Stores [value] inside a container, replaces any previous stored value. */ + fun set(value: Any) { + this.value = value + } + + @Suppress("UNCHECKED_CAST") + override fun get(): T? = value as? T + + override fun require(): T = get() ?: error("Contrainer is empty or contains a value of a different type.") + + companion object { + + /** Creates a new [MutableParamsContainer] and stores [value] inside it. */ + operator fun invoke(value: T): MutableParamsContainer { + return MutableParamsContainer().apply { + set(value) + } + } + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt index 50e3840442..86c43706a6 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DefaultRouter.kt @@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.popWhile import com.arkivanov.decompose.router.stack.pushNew import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import kotlin.reflect.KClass internal class DefaultRouter( private val navigationProvider: AppNavigationProvider, @@ -19,6 +20,15 @@ internal class DefaultRouter( navigation.pushNew(route, onComplete) } + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { + val newRoutes = routes.toList() + + navigation.navigate( + transformer = { newRoutes }, + onComplete = { newStack, _ -> onComplete(newStack.size == newRoutes.size) }, + ) + } + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { navigation.pop(onComplete) } @@ -29,4 +39,11 @@ internal class DefaultRouter( onComplete = onComplete, ) } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + navigation.popWhile( + predicate = { it::class != routeClass }, + onComplete = onComplete, + ) + } } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt new file mode 100644 index 0000000000..77e7a8f59d --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/DummyRouter.kt @@ -0,0 +1,26 @@ +package com.tangem.core.decompose.navigation + +import kotlin.reflect.KClass + +class DummyRouter : Router { + + override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } + + override fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit) { + onComplete(true) + } +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt index 3a9b7d93ca..da7f859b8f 100644 --- a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/Router.kt @@ -1,8 +1,10 @@ package com.tangem.core.decompose.navigation +import kotlin.reflect.KClass + /** * Interface for a router in the application. - * It provides methods for navigating through the application. + * It provides methods for navigating through the application by stack. */ interface Router { @@ -14,6 +16,14 @@ interface Router { */ fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + /** + * Replaces ***all*** routes in the navigation stack with the specified [routes]. + * + * @param routes The routes to replace the current stack with. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + /** * Pops the top route from the navigation stack. * @@ -22,10 +32,18 @@ interface Router { fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) /** - * Pops routes from the navigation stack until the specified route is found. + * Pops routes from the navigation stack until the specified [route] is found. * * @param route The route to pop to. * @param onComplete The callback to be invoked when the operation is complete. */ fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit = {}) + + /** + * Pops routes from the navigation stack until the ***first*** specified [routeClass] is found. + * + * @param routeClass The route class to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ + fun popTo(routeClass: KClass, onComplete: (isSuccess: Boolean) -> Unit = {}) } \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt new file mode 100644 index 0000000000..777cc78199 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/RouterExt.kt @@ -0,0 +1,11 @@ +package com.tangem.core.decompose.navigation + +/** + * Pops routes from the navigation stack until the specified route [R] is found. + * + * @param R The route to pop to. + * @param onComplete The callback to be invoked when the operation is complete. + */ +inline fun Router.popTo(noinline onComplete: (isSuccess: Boolean) -> Unit = {}) { + popTo(R::class, onComplete) +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index a4472cf056..1a01fb3200 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -3,10 +3,6 @@ "name": "NEW_CARD_SCANNING_ENABLED", "version": "undefined" }, - { - "name": "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED", - "version": "undefined" - }, { "name": "REDESIGNED_SEND_SCREEN_ENABLED", "version": "5.10.0" @@ -41,6 +37,10 @@ }, { "name": "DETAILS_REDESIGN_ENABLED", - "version": "undefined" + "version": "5.13.0" + }, + { + "name": "PUSH_NOTIFICATIONS_ENABLED", + "version": "5.13.0" } ] diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt deleted file mode 100644 index 676958ef92..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationAction.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.core.navigation - -import android.net.Uri -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import org.rekotlin.Action -import java.lang.ref.WeakReference - -sealed class NavigationAction : Action { - - data class NavigateTo( - val screen: AppScreen, - val fragmentShareTransition: FragmentShareTransition? = null, - val addToBackstack: Boolean = true, - val bundle: Bundle? = null, - ) : NavigationAction() - - data class PopBackTo(val screen: AppScreen? = null, val inclusive: Boolean = false) : NavigationAction() - - data class OpenUrl(val url: String) : NavigationAction() - - data class OpenDocument(val url: Uri) : NavigationAction() - - object OpenBiometricsSettings : NavigationAction() - - data class OpenDialog(val stateDialog: StateDialog) : NavigationAction() - - data class Share(val data: String) : NavigationAction() - - data class ActivityCreated(val activity: WeakReference) : NavigationAction() - - data class ActivityDestroyed(val activity: WeakReference) : NavigationAction() -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt b/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt deleted file mode 100644 index a0a6d64f25..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/NavigationState.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.core.navigation - -import androidx.appcompat.app.AppCompatActivity -import org.rekotlin.StateType -import java.lang.ref.WeakReference - -data class NavigationState( - val backStack: List = emptyList(), - val activity: WeakReference? = null, -) : StateType - -enum class AppScreen(val isDialogFragment: Boolean = false) { - Home, - Disclaimer, - OnboardingNote, - OnboardingWallet, - OnboardingTwins, - OnboardingOther, - Wallet, - WalletDetails, - Send(isDialogFragment = true), - Details, - DetailsSecurity, - CardSettings, - AppSettings, - ResetToFactory, - AccessCodeRecovery, - ManageTokens, - AddCustomToken, - WalletConnectSessions, - QrScanning, - ReferralProgram, - Swap, - Welcome, - SaveWallet(isDialogFragment = true), - AppCurrencySelector, - ModalNotification(isDialogFragment = true), -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt b/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt deleted file mode 100644 index 20e37f3958..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/ReduxNavController.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.core.navigation - -/** - * Navigation controller that based on redux actions - * -[REDACTED_AUTHOR] - */ -interface ReduxNavController { - - /** Navigate by [action] */ - fun navigate(action: NavigationAction) - - fun popBackStack(screen: AppScreen? = null) - - fun getBackStack(): List -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/FragmentShareTransition.kt b/core/navigation/src/main/java/com/tangem/core/navigation/ShareElement.kt similarity index 74% rename from core/navigation/src/main/java/com/tangem/core/navigation/FragmentShareTransition.kt rename to core/navigation/src/main/java/com/tangem/core/navigation/ShareElement.kt index 2cb2132eec..b3fd049583 100644 --- a/core/navigation/src/main/java/com/tangem/core/navigation/FragmentShareTransition.kt +++ b/core/navigation/src/main/java/com/tangem/core/navigation/ShareElement.kt @@ -1,18 +1,8 @@ package com.tangem.core.navigation import android.view.View -import androidx.transition.TransitionSet import java.lang.ref.WeakReference -/** -[REDACTED_AUTHOR] - */ -data class FragmentShareTransition( - val shareElements: List, - val enterTransitionSet: TransitionSet, - val exitTransitionSet: TransitionSet, -) - /** * For ease of use, the name is used as transitionName\name into the FragmentTransaction.addSharedElement */ diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt b/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt deleted file mode 100644 index 75aa1f0579..0000000000 --- a/core/navigation/src/main/java/com/tangem/core/navigation/StateDialog.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.core.navigation - -interface StateDialog { - - data class ScanFailsDialog(val source: ScanFailsSource) : StateDialog - - enum class ScanFailsSource { - MAIN, SIGN_IN, SETTINGS, INTRO; - } -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt b/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt new file mode 100644 index 0000000000..54785819e1 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/finisher/AppFinisher.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.finisher + +interface AppFinisher { + + fun finish() + + fun restart() +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt new file mode 100644 index 0000000000..25c4706fb9 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/settings/DummySettingsManager.kt @@ -0,0 +1,5 @@ +package com.tangem.core.navigation.settings + +class DummySettingsManager : SettingsManager { + override fun openSettings() { /* no-op */ } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt new file mode 100644 index 0000000000..4be904c9cb --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/settings/SettingsManager.kt @@ -0,0 +1,5 @@ +package com.tangem.core.navigation.settings + +interface SettingsManager { + fun openSettings() +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt new file mode 100644 index 0000000000..20769b6995 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/share/DummyShareManager.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.share + +class DummyShareManager : ShareManager { + + override fun shareText(text: String) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt new file mode 100644 index 0000000000..81146f78f4 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/share/ShareManager.kt @@ -0,0 +1,6 @@ +package com.tangem.core.navigation.share + +interface ShareManager { + + fun shareText(text: String) +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt b/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt new file mode 100644 index 0000000000..cff4946b40 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/url/DummyUrlOpener.kt @@ -0,0 +1,8 @@ +package com.tangem.core.navigation.url + +class DummyUrlOpener : UrlOpener { + + override fun openUrl(url: String) { + /* no-op */ + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt b/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt new file mode 100644 index 0000000000..d78ba1b1eb --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/url/UrlOpener.kt @@ -0,0 +1,6 @@ +package com.tangem.core.navigation.url + +interface UrlOpener { + + fun openUrl(url: String) +} \ No newline at end of file diff --git a/core/pagination/.gitignore b/core/pagination/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/core/pagination/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/core/pagination/build.gradle.kts b/core/pagination/build.gradle.kts new file mode 100644 index 0000000000..c7b5d3d97a --- /dev/null +++ b/core/pagination/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + // region Coroutines + implementation(deps.kotlin.coroutines) + // endregion +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/Batch.kt b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt new file mode 100644 index 0000000000..0e262776a2 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/Batch.kt @@ -0,0 +1,13 @@ +package com.tangem.pagination + +/** + * Represents a batch of data with a key. + * Used in [BatchListState]. + * + * @param TKey type of the key. + * @param TData type of the data. + */ +data class Batch( + val key: TKey, + val data: TData, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt new file mode 100644 index 0000000000..691941752a --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchAction.kt @@ -0,0 +1,61 @@ +package com.tangem.pagination + +/** + * Action that can be dispatched to [BatchListSource]. + * + * @param TRequestParams type of the request params to load batches. + * @param TKey type of the key of the batch. + * @param TUpdate type of the update request. + */ +sealed class BatchAction { + + /** + * Action to load the first batch. + * + * @param requestParams request params to load the first batch. + */ + data class Reload( + val requestParams: TRequestParams, + ) : BatchAction() + + /** + * Action to load the next batch. + * + * @param requestParams request params to load the next batch with new request. + * If null, the last request will be used. + * Will be saved in the state and used for future LoadMore actions with request = null. + */ + data class LoadMore( + val requestParams: TRequestParams? = null, + ) : BatchAction() + + /** + * Action to update the batch. + * + * @param keys keys of the batches to update. + * @param updateRequest request to update the batches. + */ + class UpdateBatches( + val keys: Set, + val updateRequest: TUpdate, + ) : BatchAction() + + /** + * Action to cancel the current batch loading. + */ + data object CancelBatchLoading : BatchAction() + + /** + * Action to cancel all update requests. + */ + data object CancelAllUpdates : BatchAction() + + /** + * Action to cancel update requests that satisfy the predicate. + * + * @param predicate predicate to check if the update request should be cancelled. + */ + class CancelUpdates( + val predicate: (UpdateBatches) -> Boolean, + ) : BatchAction() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt new file mode 100644 index 0000000000..a358df8bf2 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchFetchResult.kt @@ -0,0 +1,30 @@ +package com.tangem.pagination + +/** + * Represents a result of a batch fetch request. + * Used in [BatchListState]. + * + * @param TData type of the data. + */ +sealed class BatchFetchResult { + + /** + * Represents a successful result of a batch fetch request. + * + * @param data fetched data. + * @param last indicates if this is the last batch for the request. + */ + data class Success( + val data: TData, + val last: Boolean, + ) : BatchFetchResult() + + /** + * Represents an error result of a batch fetch request. + * Also used for unexpected exceptions that occurred in fetch method in BatchFetcher. + * + * @param throwable throwable that occurred during the request. + * @see com.tangem.pagination.fetcher.BatchFetcher + */ + class Error(val throwable: Throwable) : BatchFetchResult() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt new file mode 100644 index 0000000000..77e58eafcf --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSource.kt @@ -0,0 +1,332 @@ +package com.tangem.pagination + +import com.tangem.pagination.fetcher.BatchFetcher +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.* + +/** + * Source for paginated data. The starting point for pagination. + * + * @param TKey Type of the key that identifies a batch. + * @param TData Type of the data in the batch. Generally, it' a list of items. + * @param TUpdate Type of the update request. + * @property state State of the paginated data. + * @property updateResults Flow of results of update requests. + */ +interface BatchListSource { + val state: StateFlow> + val updateResults: SharedFlow>> +} + +/** + * Creates a new [BatchListSource] with the provided configuration. + * + * @param fetchDispatcher Dispatcher for fetch operations. + * @param context Context for batching. + * @param generateNewKey Function to generate a new key for a batch. + * @param batchFetcher Function to fetch a batch of data. + * + * @return New instance of [BatchListSource]. + */ +@Suppress("FunctionNaming") +fun BatchListSource( + fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, + context: BatchingContext, + generateNewKey: suspend (List) -> TKey, + batchFetcher: BatchFetcher, +): BatchListSource = + DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, null) + +/** + * Creates a new [BatchListSource] with the provided configuration. + * + * @param fetchDispatcher Dispatcher for fetch operations. + * @param context Context for batching. + * @param generateNewKey Function to generate a new key for a batch. + * @param batchFetcher Function to fetch a batch of data. + * @param updateFetcher Function to fetch updates for batches. + * + * @return New instance of [BatchListSource]. + */ +@Suppress("FunctionNaming") +fun BatchListSource( + fetchDispatcher: CoroutineDispatcher = Dispatchers.IO, + context: BatchingContext, + generateNewKey: suspend (List) -> TKey, + batchFetcher: BatchFetcher, + updateFetcher: BatchUpdateFetcher, +): BatchListSource = + DefaultBatchListSource(fetchDispatcher, context, generateNewKey, batchFetcher, updateFetcher) + +private class DefaultBatchListSource( + private val fetchDispatcher: CoroutineDispatcher, + private val context: BatchingContext, + private val generateNewKey: suspend (List) -> TKey, + private val batchFetcher: BatchFetcher, + private val updateFetcher: BatchUpdateFetcher? = null, +) : BatchListSource { + + override val state = MutableStateFlow(BatchListState(emptyList(), PaginationStatus.None)) + override val updateResults = MutableSharedFlow>>( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + private val scope = context.coroutineScope + private val updateJobs = MutableStateFlow, Job>>>(emptyList()) + private val waitingUpdateJobs = + MutableStateFlow, Job>>>(emptyList()) + + private val lastRequestResult = MutableStateFlow?>(null) + private var reloadActionJob: Job? = null + private var loadMoreActionJob: Job? = null + + init { + scope.launch { + try { + awaitCancellation() + } finally { + withContext(NonCancellable) { + loadMoreActionJob = null + loadMoreActionJob = null + lastRequestResult.value = null + stopAllUpdates() + state.value = BatchListState(emptyList(), PaginationStatus.None) + } + } + } + + scope.launch { + context.actionsFlow.collect { action -> + collectActions(action) + } + } + } + + private fun collectActions(action: BatchAction) { + when (action) { + is BatchAction.Reload -> { + // Stop all tasks + loadMoreActionJob?.cancel() + reloadActionJob?.cancel() + stopAllUpdates() + reloadActionJob = scope.launch(fetchDispatcher) { + reloadTask(action) + } + } + is BatchAction.LoadMore -> { + if (loadMoreActionJob?.isActive == true) { + return + } + + loadMoreActionJob = scope.launch(fetchDispatcher) { + reloadActionJob?.join() + loadMoreTask(action) + } + } + is BatchAction.UpdateBatches -> { + if (updateFetcher == null) return + + scope.launch(fetchDispatcher) { + // Lazily start a job so we can avoid batch update collisions + // by waiting for other tasks with the same keys to complete + val job = launch(start = CoroutineStart.LAZY) { + updateBatchesTask(action) + } + + val actionJob = action to job + + waitingUpdateJobs.update { it + actionJob } + + // Wait for other update tasks that mutate batches with the same keys + updateJobs.first { workingJobs -> + action.keys.intersect(workingJobs.map { it.first.keys }.flatten().toSet()).isEmpty() + } + + waitingUpdateJobs.update { it - actionJob } + + // No other task are mutating batches with the same keys, so we can start a job + val started = job.start() + + if (started) { + updateJobs.update { it + actionJob } + + job.invokeOnCompletion { cause -> + // If the job was cancelled it is up to a canceller to remove job from the updateJobs list + if (cause !is CancellationException) { + updateJobs.update { it - actionJob } + } + } + } + } + } + BatchAction.CancelAllUpdates -> { + if (updateFetcher == null) return + stopAllUpdates() + } + is BatchAction.CancelUpdates -> { + if (updateFetcher == null) return + stopUpdates(action.predicate) + } + BatchAction.CancelBatchLoading -> { + loadMoreActionJob?.cancel() + reloadActionJob?.cancel() + } + } + } + + private suspend fun reloadTask(action: BatchAction.Reload) { + state.value = BatchListState( + data = emptyList(), + status = PaginationStatus.InitialLoading, + ) + + val res = runCatching { + batchFetcher.fetchFirst(action.requestParams) + }.getOrElse { BatchFetchResult.Error(it) } + + state.value = when (res) { + is BatchFetchResult.Success -> { + val key = generateNewKey(listOf()) + val batch = Batch( + key = key, + data = res.data, + ) + BatchListState( + data = listOf(batch), + status = if (res.last) { + PaginationStatus.EndOfPagination + } else { + PaginationStatus.Paginating(res) + }, + ) + } + is BatchFetchResult.Error -> { + BatchListState( + data = emptyList(), + status = PaginationStatus.InitialLoadingError( + throwable = res.throwable, + ), + ) + } + } + + lastRequestResult.value = res + } + + private suspend fun loadMoreTask(action: BatchAction.LoadMore) { + val status = state.value.status + + // Skip the action if the state is not ready to continue pagination. + // Two options are acceptable: + // 1. The Source is ready to load next page with the same or different request params. + // 2. The Source has reached the end of pagination, but there is another request + // that can possibly load the next page and continue the pagination + + if (status !is PaginationStatus.Paginating && status !is PaginationStatus.EndOfPagination) return + if (status is PaginationStatus.EndOfPagination && action.requestParams == null) return + + val lastResult = lastRequestResult.value ?: return + + state.update { it.copy(status = PaginationStatus.NextBatchLoading) } + + val res = runCatching { + batchFetcher.fetchNext(action.requestParams, lastResult) + }.getOrElse { BatchFetchResult.Error(it) } + + lastRequestResult.value = lastResult + + state.update { currentState -> + when (res) { + is BatchFetchResult.Success -> { + val newBatch = Batch( + key = generateNewKey(currentState.data.map { it.key }), + data = res.data, + ) + + currentState.copy( + data = currentState.data + newBatch, + status = if (res.last) { + PaginationStatus.EndOfPagination + } else { + PaginationStatus.Paginating(res) + }, + ) + } + is BatchFetchResult.Error -> { + currentState.copy( + status = PaginationStatus.Paginating(res), + ) + } + } + } + } + + private suspend fun updateBatchesTask(action: BatchAction.UpdateBatches) { + if (updateFetcher == null) return + + val batches = state.value.data + val batchesToUpdate = batches.filter { action.keys.contains(it.key) } + + val result = try { + updateFetcher.fetchUpdate( + toUpdate = batchesToUpdate, + updateRequest = action.updateRequest, + ) + } catch (t: Throwable) { + BatchUpdateResult.Error(t) + } + + if (result is BatchUpdateResult.Success) { + state.update { currentState -> + val resMap = result.data.associateBy { it.key } + currentState.copy( + data = currentState.data.map { + resMap[it.key] ?: it + }, + ) + } + } + + updateResults.emit(action.updateRequest to result) + } + + private fun stopAllUpdates() { + updateJobs.update { actionJobs -> + waitingUpdateJobs.update { waitingActionJobs -> + waitingActionJobs.forEach { + it.second.cancel() + } + emptyList() + } + actionJobs.forEach { + it.second.cancel() + } + emptyList() + } + } + + private fun stopUpdates(predicate: (BatchAction.UpdateBatches) -> Boolean) { + updateJobs.update { actionJobs -> + waitingUpdateJobs.update { waitingActionJobs -> + waitingActionJobs.mapNotNull { + if (predicate(it.first)) { + it.second.cancel() + null + } else { + it + } + } + } + actionJobs.mapNotNull { + if (predicate(it.first)) { + it.second.cancel() + null + } else { + it + } + } + } + } +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt new file mode 100644 index 0000000000..258a20b8d0 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListSourceFlow.kt @@ -0,0 +1,17 @@ +package com.tangem.pagination + +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +fun BatchListSource.toBatchFlow() = + object : BatchFlow { + override val state: StateFlow> + get() = this@toBatchFlow.state + override val updateResults: SharedFlow>> + get() = this@toBatchFlow.updateResults + } + +interface BatchFlow { + val state: StateFlow> + val updateResults: SharedFlow>> +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt new file mode 100644 index 0000000000..b53ba23dc0 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchListState.kt @@ -0,0 +1,17 @@ +package com.tangem.pagination + +/** + * State that is used for listening the current state of a pagination. + * + * @param TKey type of the key of the batch. + * @param TData type of the data. + * + * @property data list of loaded batches. + * @property status current status of the pagination. + * + * @see BatchListSource + */ +data class BatchListState( + val data: List>, + val status: PaginationStatus, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt new file mode 100644 index 0000000000..704c416316 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateFetcher.kt @@ -0,0 +1,23 @@ +package com.tangem.pagination + +/** + * Interface for fetching updates for a batch of data. + * Used in [BatchListState]. + * + * @param TKey type of the key. + * @param TData type of the data. + * @param TUpdate type of the update request. + */ +fun interface BatchUpdateFetcher { + + /** + * Fetches updates for a batch of data. + * Note that the result batch key as a result of executing the method must be presented in the [toUpdate] list, + * otherwise, updates will not be performed + * + * @param toUpdate list of batches to update. + * @param updateRequest request to update the data. + * @return result of the update operation. + */ + suspend fun fetchUpdate(toUpdate: List>, updateRequest: TUpdate): BatchUpdateResult +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt new file mode 100644 index 0000000000..c2e00fad9c --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchUpdateResult.kt @@ -0,0 +1,27 @@ +package com.tangem.pagination + +/** + * Represents the result of a batch fetch operation. + * Used in [BatchListState] and [BatchUpdateFetcher]. + * + * @param TKey type of the key. + * @param TData type of the data. + */ +sealed class BatchUpdateResult { + + /** + * Represents a successful result of a batch update operation. + * + * @param data fetched data. + */ + data class Success( + val data: List>, + ) : BatchUpdateResult() + + /** + * Represents an error result of a batch update operation. + * + * @param error error that occurred during the operation. + */ + class Error(val throwable: Throwable) : BatchUpdateResult() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt new file mode 100644 index 0000000000..252d6f1680 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/BatchingContext.kt @@ -0,0 +1,22 @@ +package com.tangem.pagination + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow + +/** + * Context for working with [BatchListSource]. + * + * @param TRequestParams type of the request. + * @param TKey type of the key. + * @param TUpdate type of the update request. + * + * @property actionsFlow flow of [BatchAction]s that would be dispatched to [BatchListSource]. + * @property coroutineScope scope for the [BatchListSource] to launch coroutines. When it is cancelled, + * all the operations and requests launched in the [BatchListSource] would be cancelled and all data would be cleared. + * + * @see BatchListSource + */ +class BatchingContext( + val actionsFlow: Flow>, + val coroutineScope: CoroutineScope, +) \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt new file mode 100644 index 0000000000..5e30875c6a --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/PaginationStatus.kt @@ -0,0 +1,55 @@ +package com.tangem.pagination + +/** + * Status of the pagination. + * + * @param TData type of the data. + * + * @see BatchListState + */ +sealed class PaginationStatus { + + /** + * Represents that there is no data. Used when the list of batches is empty. + * The initial state of the pagination. + */ + data object None : PaginationStatus() + + /** + * Represents that the batch is loading for the first time. + * Used when the pagination is empty and the first batch is being loaded. + */ + data object InitialLoading : PaginationStatus() + + /** + * Represents that the first batch was loaded with an error. + * + * @param error error that occurred during the initial loading. + */ + data class InitialLoadingError( + val throwable: Throwable, + ) : PaginationStatus() + + /** + * Represents that the last batch was loaded and + * the source is ready to load the next one or reload previous if [lastResult] is an error. + * For the first batch, [lastResult] is always [BatchFetchResult.Success] + * + * @param lastResult result of the last batch fetch. + */ + data class Paginating( + val lastResult: BatchFetchResult, + ) : PaginationStatus() + + /** + * Represents that the next batch is loading. + * Used when the next batch is being loaded. + */ + data object NextBatchLoading : PaginationStatus() + + /** + * Represents that the source has no more batches to load. + * The next [BatchAction.LoadMore] with [BatchAction.LoadMore.requestParams] = null will be ignored. + */ + data object EndOfPagination : PaginationStatus() +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt b/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt new file mode 100644 index 0000000000..f2cc69767a --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/exception/EndOfPaginationException.kt @@ -0,0 +1,6 @@ +package com.tangem.pagination.exception + +/** + * Exception that is thrown when there are no more items to fetch. + */ +class EndOfPaginationException : IllegalStateException() \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt new file mode 100644 index 0000000000..818620dab1 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/BatchFetcher.kt @@ -0,0 +1,36 @@ +package com.tangem.pagination.fetcher + +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.BatchListState + +/** + * Interface for fetching a batch of data. Used in [BatchListState]. + * + * @param TRequestParams type of the request. + * @param TData type of the data. + * + * @see BatchListState + */ +interface BatchFetcher { + + /** + * Fetches the first batch of data. + * + * @param requestParams initial request params. Will be saved to be used in [fetchNext] requests. + * @return result of the fetch operation. + */ + suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult + + /** + * Fetches the next batch of data. + * + * @param overrideRequestParams overrides current remembered request, even if that fetch fails. + * If null, the last request should be used. + * @param lastResult result of the last fetch operation. + * @return result of the fetch operation. + */ + suspend fun fetchNext( + overrideRequestParams: TRequestParams?, + lastResult: BatchFetchResult, + ): BatchFetchResult +} \ No newline at end of file diff --git a/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt new file mode 100644 index 0000000000..3dd04c24c7 --- /dev/null +++ b/core/pagination/src/main/java/com/tangem/pagination/fetcher/LimitOffsetBatchFetcher.kt @@ -0,0 +1,74 @@ +package com.tangem.pagination.fetcher + +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.exception.EndOfPaginationException +import kotlinx.coroutines.flow.MutableStateFlow + +/** + * Fetcher that uses limit and offset to fetch data. + * + * @param TRequestParams type of the request params. + * @param TData type of the data. + * + * @property prefetchDistance number of items to fetch for the first batch. + * @property batchSize size of the batch. + * @property fetch function that fetches the data. + */ +class LimitOffsetBatchFetcher( + private val prefetchDistance: Int, + private val batchSize: Int, + private val fetch: suspend (request: Request) -> BatchFetchResult, +) : BatchFetcher { + + data class Request( + val limit: Int, + val offset: Int, + val request: TRequest, + ) + + private val lastRequest = MutableStateFlow?>(null) + + override suspend fun fetchFirst(requestParams: TRequestParams): BatchFetchResult { + val req = Request( + offset = 0, + limit = prefetchDistance, + request = requestParams, + ) + + val res = runCatching { + fetch(req) + }.getOrElse { BatchFetchResult.Error(it) } + + lastRequest.value = req + return res + } + + override suspend fun fetchNext( + overrideRequestParams: TRequestParams?, + lastResult: BatchFetchResult, + ): BatchFetchResult { + val last = lastRequest.value + requireNotNull(last) + + val req = if (lastResult is BatchFetchResult.Success) { + if (lastResult.last && overrideRequestParams == null) { + return BatchFetchResult.Error(EndOfPaginationException()) + } + + Request( + offset = last.offset + last.limit, + limit = batchSize, + request = overrideRequestParams ?: last.request, + ) + } else { + last + } + + val res = runCatching { + fetch(req) + }.getOrElse { BatchFetchResult.Error(it) } + + lastRequest.value = req + return res + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index e70467d63f..015132e02d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,9 +1,12 @@ + Выберите сеть Добавить токен Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. + Как сканировать Обратиться в поддержку + Попробовать снова Эта функция недоступна в демонстрационном режиме Причина: %s Не могу отправить транзакцию @@ -67,6 +70,7 @@ Доступ запрещен Применить Одобрение + Подтвердить Внимание Баланс: %s Баланс @@ -81,6 +85,7 @@ Копировать Скопировать адрес Создать + Свое Удалить Отключено Готово @@ -92,7 +97,6 @@ Обозреватель Комиссия Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s - Свое Быстро По рынку Медленно @@ -108,6 +112,7 @@ Далее Нет Нет адреса + Сейчас OK Основная карта Парольная фраза @@ -117,6 +122,7 @@ Отклонить Перезагрузить Переименовать + Сохранить Сохранить изменения Искать Поиск токенов @@ -128,12 +134,14 @@ Поделиться Подписать Подписать и отправить + Стейкинг Начать Отправить Успешно Поддержка Обмен условия участия + Сегодня Ошибка транзакции Транзакции Перевод @@ -169,6 +177,7 @@ Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить Остерегайтесь мошеннических токенов, они могут ничего не стоить Токены могут быть созданы кем угодно + Купить кошелек Tangem Чат Код доступа Перед сканированием карты вам нужно будет ввести правильный код доступа. @@ -186,6 +195,7 @@ Скрывать балансы жестом переворота Эмитент Подписано + Отправить отзыв Подробности Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования @@ -254,6 +264,13 @@ Обращение в поддержку Обращение в поддержку Tangem Не могу отправить транзакцию + Текущая транзакция + Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. + Укажите лимит доступа к выбранному токену + Количество %s + Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s + Дать разрешение + Безлимитно Купить Сканировать Чтобы изменить код доступа, приложите карту как показано выше и не убирайте до окончания операции @@ -297,11 +314,15 @@ %1$d из %2$d кошельков %1$d из %2$d кошельков + Удалить например Bitcoin + Ваш портфель был обновлен Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление. Голосовать Выберите кошелек Кошелёк не поддерживает более одной сети + Ссылки + Метрики Вам необходимо установить единый код доступа для защиты всех ваших карт Защита Позже вы сможете установить индивидуальный код доступа для каждой карты @@ -377,6 +398,7 @@ Активация карты Резервная карта #%d Нет резервных карт + Уведомления Добавлена ​​одна резервная карта Подготовьте свою карту Добавлены две резервные карты @@ -537,9 +559,22 @@ Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время Неверный адрес Транзакция отправлена + Подготовьтесь к сканированию карты, которую вы хотите настроить. Забыть кошелек Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя + APY + Годовой процентный доход, который вы можете получить от участия в стейкинге. + Доступно + Способ возраграждения + Способ получения вознаграждений за стейкинг. Он может быть автоматическим или ручным. + Период возрагражения + Это период, определяющий, когда участники стейкинга получат свои вознаграждения. + Стейкинг %s + Период вывода + Период, который необходимо подождать после запроса на вывод средств из стейкинга, прежде чем токены станут доступны. + Период прогрева + Время, необходимое для начала процесса стейкинга. Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Революционный аппаратный кошелек До трех карт с одним кошельком @@ -559,17 +594,9 @@ Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. Подтвердить Вы отправляете - Дать разрешение Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств - Подтвердить - Текущая транзакция - Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. Дать разрешение - Укажите лимит доступа к выбранному токену - Количество %s - Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s - Безлимитно В процессе Обменять Вы получите @@ -583,9 +610,10 @@ У вас нет средств для продажи. Пополните счет, чтобы иметь возможность продать с него средства. У вас нет средств для отправки. Пополните счет, чтобы иметь возможность отправить с него средства. В данный момент обмен монеты %s недоступен. Следите за нашими обновлениями. - Продажа средств станет доступной после завершения транзакции(-ий) в сети %s + Продажа средств станет доступной после завершения транзакции(-ий) в сети %s Отправка средств станет доступной после завершения транзакции(-ий) в сети %s В данный момент продажа %s недоступна. Следите за нашими обновлениями. + В данный момент стейкинг монеты %s недоступен. Следите за нашими обновлениями. Сгенерировать XPUB Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами. @@ -662,6 +690,7 @@ Сессии WalletConnect Подключение к dApps WalletConnect + Подключение может занять несколько секунд Рыночная цена %s за 24 часа Сеть %s diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index e0b8c69381..28f111ab8b 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -277,13 +277,9 @@ 兼容Web3.0 批准被視為所有去中心化交易所的行業標準,並保護您的錢包在未經您許可的情況下不被智能合約訪問。按照設計,智能合約無法訪問您的代幣,除非您從您的終端批准訪問。通過“解鎖”您的代幣,您將獲得 1inch 智能合約使用您的資產的權限。網絡的礦工將獲得Gas Fee(由您支付)作為補償,以在區塊鏈上記錄此操作。一旦獲得許可,您就可以交易您的代幣。 批准 - 賦予權限 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量 餘額不足 - 允許 賦予權限 - 數量 %s - 要繼續,您需要允許 1inch 智能合約使用您的 %s 進行中 交易 選擇代幣 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 40b0b45e01..b38d48131f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,9 +1,12 @@ + Choose network Add custom token Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. + How to scan Request support + Try again This feature is disabled in Demo mode Reason: %s Can\'t send a transaction @@ -64,8 +67,11 @@ Not enough ADA Accept Access denied + All + Allow Apply Approval + Approve Attention Balance: %s Balance @@ -80,6 +86,7 @@ Copy Copy address Create + Custom Delete Disabled Done @@ -91,7 +98,6 @@ Explorer Fee Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s - Custom Fast Market Slow @@ -107,15 +113,18 @@ Next No No address + Now OK Primary Card Passphrase Paste + %1$s-%2$s Read more Receive Reject Reload Rename + Save Save changes Search Search tokens @@ -127,12 +136,15 @@ Share Sign Sign and send + Stake + Staking Start Submit Success Support Swap terms and conditions + Today Transaction failed Transactions Transfer @@ -168,6 +180,7 @@ Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing. Be aware of adding scam tokens, they can cost nothing Note that tokens can be created by anyone + Buy Tangem Wallet Chat Access code You will have to submit the correct access code before scanning the card @@ -185,6 +198,7 @@ Flip-to-Hide Balances Issuer Signed + Send feedback Details Check your internet connection or switch to a different network Terms of service @@ -253,6 +267,13 @@ Feedback Tangem feedback Can\'t send a transaction + Current transaction + The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. + Specify the approve limit for the selected token + Amount %s + To continue, grant 1inch smart contracts permission to use your %s + Give Permission + Unlimited Order card Scan card To change the access code tap the card as shown above and do not remove until the end of the operation @@ -296,11 +317,25 @@ %1$d of %2$d wallet %1$d of %2$d wallets + Remove e.g. BTC I trust, hodl I must + Your portfolio has been updated The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. Upvote Choose wallet The wallet doesn\'t support more than one network + To start buying, exchanging or receiving this asset, add this token to at least 1 network + This asset is not available + Add to portfolio + My portfolio + Market + Select wallet + Sort By + Insights + Links + Metrics + Price performance + Security score You have to set up a single access code to protect all your cards Protect You can set up an individual access code on each card later @@ -372,6 +407,7 @@ Activating card Backup card #%d No backup cards + Notifications One backup card added Prepare your card Two backup cards added @@ -530,9 +566,41 @@ Invalid address %1$s (%2$s) Transaction sent + Prepare to scan card you want to setup. Forget wallet This will remove the wallet from the application. The wallet itself can be added again. Name + Active + To unstake your assets, click here. + APR + APY + The annual percentage return you can earn from participating in staking. + Available + Average Reward Rate + %s est. profit + Market rating + Metrics + Minimum Requirement + No rewards to claim + On stake + Reward claiming + A way to receive staking rewards. It can be claimed automatically or manually. + Reward schedule + This is a schedule that determines when participants in staking receive their rewards. + Rewards to claim: %s + Staking %s + Unbonding Period + The period you must wait after requesting to withdraw funds from staking before the tokens become available. + Warmup period + The allocated time for activating participation in staking. + Native staking + Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days. + Earn staking rewards + Rewards + Stake more + Unstacked + Check unstaked to claim your assets + Validator Store your crypto assets secure while keeping private keys contained in your card Revolutionary Hardware Wallet Up to 3 physical cards to one wallet @@ -552,17 +620,9 @@ All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve You swap - Give Permission Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds - Approve - Current transaction - The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Give Permission - Specify the approve limit for the selected token - Amount %s - To continue, grant 1inch smart contracts permission to use your %s - Unlimited In progress Swap You receive @@ -579,6 +639,7 @@ Selling funds will be available once the pending transaction(s) in network %s is complete Sending funds will be available once the pending transaction(s) in network %s is complete Selling %s is not available at the moment. Please check our updates. + Staking %s is not available at the moment. Please check our updates. Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. @@ -613,6 +674,9 @@ This action is irreversible. You will not have access to the old wallet. Tap the twin card with number %s and do not remove until the end of the operation Use %s or scan a card to have an access to your wallet + Stay up to date with the latest features and news + Be able to get important market notifications + Would you like to use\nPush-notifications? Add new wallet Are you sure you want to delete this wallet? An error has occurred, please scan your card to log in @@ -655,6 +719,7 @@ WalletConnect Sessions Connect to dApps WalletConnect + Connecting may take a few seconds %s Market Price last 24h %s network diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index e7c5bfc4e7..cb8f7f15ee 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -9,9 +9,6 @@ android { } dependencies { - /** Project - Common */ - implementation(projects.common) - /** Project - Domain */ implementation(projects.domain.tokens.models) implementation(projects.domain.appTheme.models) @@ -25,6 +22,12 @@ dependencies { implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.paging.runtime) implementation(deps.androidx.palette) + implementation(deps.androidx.windowManager) { + exclude( + deps.kotlin.coroutines.android.get().module.group, + deps.kotlin.coroutines.android.get().module.name + ) + } /** Compose */ implementation(deps.compose.constraintLayout) @@ -40,11 +43,12 @@ dependencies { /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.permission) implementation(deps.material) implementation(deps.compose.shimmer) implementation(deps.kotlin.immutable.collections) implementation(deps.zxing.qrCore) - implementation(deps.jodatime) + api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt index 62e19d4af6..49a90c5af6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/UiDependencies.kt @@ -1,11 +1,19 @@ package com.tangem.core.ui +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Stable import com.tangem.core.ui.haptic.HapticManager +import com.tangem.core.ui.message.EventMessageHandler import com.tangem.core.ui.theme.AppThemeModeHolder +@Stable interface UiDependencies { val hapticManager: HapticManager val appThemeModeHolder: AppThemeModeHolder + + val globalSnackbarHostState: SnackbarHostState + + val eventMessageHandler: EventMessageHandler } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt new file mode 100644 index 0000000000..fbb4ae896b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt @@ -0,0 +1,33 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.res.TangemTheme + +/** + * A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating + * elements and floating button at the bottom of the screen. + */ +@Composable +fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size100 + bottomBarHeight) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index bb17fcf7b6..7ef56f015e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -86,6 +86,7 @@ fun PrimaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, size: TangemButtonSize = TangemButtonSize.Default, + colors: ButtonColors = TangemButtonsDefaults.primaryButtonColors, showProgress: Boolean = false, enabled: Boolean = true, ) { @@ -94,7 +95,7 @@ fun PrimaryButton( text = text, icon = TangemButtonIconPosition.None, onClick = onClick, - colors = TangemButtonsDefaults.primaryButtonColors, + colors = colors, enabled = enabled, showProgress = showProgress, size = size, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt index 18ca78091a..97a3b692cc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Dialogs.kt @@ -27,8 +27,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.SelctorDialogParamsProvider.SelectorDialogParams import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.fields.SimpleDialogTextField -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -100,7 +100,7 @@ fun TextInputDialog( confirmButton: DialogButton, onDismissDialog: () -> Unit, onValueChange: (TextFieldValue) -> Unit, - textFieldParams: AdditionalTextInputDialogParams, + textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, title: String? = null, dismissButton: DialogButton? = null, isDismissable: Boolean = true, @@ -128,7 +128,7 @@ fun TextInputDialog( confirmButton: DialogButton, onDismissDialog: () -> Unit, onValueChange: (String) -> Unit, - textFieldParams: AdditionalTextInputDialogParams, + textFieldParams: AdditionalTextInputDialogParams = remember { AdditionalTextInputDialogParams() }, title: String? = null, dismissButton: DialogButton? = null, isDismissable: Boolean = true, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt deleted file mode 100644 index ea843b7a7a..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsEffect.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.core.ui.components - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import com.google.accompanist.systemuicontroller.SystemUiController -import com.google.accompanist.systemuicontroller.rememberSystemUiController - -@Composable -fun SystemBarsEffect(block: SystemUiController.() -> Unit) { - val systemUiController = rememberSystemUiController() - SideEffect { block(systemUiController) } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt new file mode 100644 index 0000000000..0e40554581 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/SystemBarsUtils.kt @@ -0,0 +1,61 @@ +package com.tangem.core.ui.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.core.ui.res.LocalIsInDarkTheme + +/** + * Provides the ability to set a scrim for 3-button navigation + * + * Automatically makes navigation bar transparent when the composable is disposed. + * + * Usage example: [com.tangem.feature.tokendetails.presentation.TokenDetailsFragment] + */ +@Composable +fun NavigationBar3ButtonsScrim() { + val systemUiController = rememberSystemUiController() + val isDarkTheme = LocalIsInDarkTheme.current + SideEffect { + systemUiController.isNavigationBarContrastEnforced = true + } + LaunchedEffect(systemUiController.isNavigationBarContrastEnforced) { + if (systemUiController.isNavigationBarContrastEnforced.not()) { + systemUiController.isNavigationBarContrastEnforced = true + } + } + DisposableEffect(isDarkTheme) { + onDispose { + systemUiController.isNavigationBarContrastEnforced = false + } + } +} + +/** + * Provides the ability to set dark/light icons in cases where the darkness of the screen differs from that + * provided in [TangemTheme]. + * + * Automatically returns the icon colors to their original state when the composable is disposed. + * + * Usage example: [com.tangem.feature.qrscanning.QrScanningFragment] + */ +@Composable +fun SystemBarsIconsDisposable(darkIcons: Boolean, isNavigationBarContrastEnforced: Boolean = false) { + val systemUiController = rememberSystemUiController() + + SideEffect { + systemUiController.systemBarsDarkContentEnabled = darkIcons + systemUiController.isNavigationBarContrastEnforced = isNavigationBarContrastEnforced + } + + val isDarkTheme = LocalIsInDarkTheme.current + + DisposableEffect(isDarkTheme) { + onDispose { + systemUiController.systemBarsDarkContentEnabled = !isDarkTheme + systemUiController.isNavigationBarContrastEnforced = false + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt new file mode 100644 index 0000000000..80afe74fd7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBar.kt @@ -0,0 +1,55 @@ +package com.tangem.core.ui.components.appbar + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * App bar without buttons + * + * @param text title + * @param modifier modifier + * + * @see Figma component + */ +@Composable +fun AppBar(text: TextReference, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxWidth()) { + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.primary1, + maxLines = 1, + style = TangemTheme.typography.subtitle1, + modifier = Modifier + .align(Alignment.Center) + .padding(vertical = TangemTheme.dimens.spacing10), + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun AppBar_Preview() { + TangemThemePreview { + AppBar( + text = stringReference("Tangem"), + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt index 685358db13..07f48d4787 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithAdditionalButtons.kt @@ -1,23 +1,57 @@ package com.tangem.core.ui.components.appbar import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.models.AdditionalButton -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * App bar with title and two additional buttons + * + * @param startButton button information attached to the left edge + * @param endButton button information attached to the right edge + * + * @see + * Figma Component + */ +@Composable +fun AppBarWithAdditionalButtons( + text: TextReference, + modifier: Modifier = Modifier, + startButton: AdditionalButton? = null, + endButton: AdditionalButton? = null, + textColor: Color = TangemTheme.colors.text.primary1, + iconColor: Color = TangemTheme.colors.icon.primary1, +) { + AppBarWithAdditionalButtons( + text = text.resolveReference(), + startButton = startButton, + endButton = endButton, + modifier = modifier, + textColor = textColor, + iconColor = iconColor, + ) +} /** * App bar with title and two additional buttons @@ -31,43 +65,63 @@ import com.tangem.core.ui.res.TangemTheme @Composable fun AppBarWithAdditionalButtons( text: String, + modifier: Modifier = Modifier, startButton: AdditionalButton? = null, endButton: AdditionalButton? = null, + textColor: Color = TangemTheme.colors.text.primary1, + iconColor: Color = TangemTheme.colors.icon.primary1, ) { Box( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .heightIn(TangemTheme.dimens.size56) .padding(all = TangemTheme.dimens.spacing16), ) { if (startButton != null) { - IconButton(modifier = Modifier.align(Alignment.CenterStart), onClick = startButton.onIconClicked) { - Icon( - painter = painterResource(id = startButton.iconRes), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.primary1, - ) - } + Icon( + painter = painterResource(id = startButton.iconRes), + contentDescription = null, + tint = iconColor, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .align(Alignment.CenterStart) + .clickable( + onClick = startButton.onIconClicked, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size24 / 2, + ), + ), + ) } Text( text = text, modifier = Modifier.align(Alignment.Center), - color = TangemTheme.colors.text.primary1, + color = textColor, maxLines = 1, style = TangemTheme.typography.subtitle1, ) if (endButton != null) { - IconButton(modifier = Modifier.align(Alignment.CenterEnd), onClick = endButton.onIconClicked) { - Icon( - painter = painterResource(id = endButton.iconRes), - contentDescription = null, - modifier = Modifier.size(TangemTheme.dimens.size24), - tint = TangemTheme.colors.icon.primary1, - ) - } + Icon( + painter = painterResource(id = endButton.iconRes), + contentDescription = null, + tint = iconColor, + modifier = Modifier + .size(TangemTheme.dimens.size24) + .align(Alignment.CenterEnd) + .clickable( + onClick = endButton.onIconClicked, + role = Role.Button, + interactionSource = remember { MutableInteractionSource() }, + indication = androidx.compose.material.ripple.rememberRipple( + bounded = false, + radius = TangemTheme.dimens.size24 / 2, + ), + ), + ) } } } @@ -87,6 +141,7 @@ private fun Preview_AppBarWithAdditionalButtons() { iconRes = R.drawable.ic_more_vertical_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } @@ -102,6 +157,7 @@ private fun Preview_AppBarWithOnlyStartButtons() { iconRes = R.drawable.ic_scan_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } @@ -117,6 +173,7 @@ private fun Preview_AppBarWithOnlyEndButtons() { iconRes = R.drawable.ic_more_vertical_24, onIconClicked = {}, ), + modifier = Modifier.background(TangemTheme.colors.background.secondary), ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt new file mode 100644 index 0000000000..d50a8f5c75 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/models/TopAppBarMedium.kt @@ -0,0 +1,75 @@ +package com.tangem.core.ui.components.appbar.models + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBarMedium( + title: TextReference, + scrollBehavior: TopAppBarScrollBehavior, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + navigationIconResId: Int = R.drawable.ic_back_24, + colors: TopAppBarColors = TangemTopAppBarColors, +) { + MediumTopAppBar( + modifier = modifier, + scrollBehavior = scrollBehavior, + colors = colors, + title = { + val collapsedStyle = TangemTheme.typography.subtitle1 + val expandedStyle = TangemTheme.typography.h1 + val style by remember(scrollBehavior.state.collapsedFraction) { + derivedStateOf { + if (scrollBehavior.state.collapsedFraction >= COLLAPSED_APP_BAR_THRESHOLD) { + collapsedStyle + } else { + expandedStyle + } + } + } + + Text( + text = title.resolveReference(), + style = style, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + }, + navigationIcon = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onBackClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = navigationIconResId), + contentDescription = null, + ) + } + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +internal val TangemTopAppBarColors: TopAppBarColors + @Composable + @ReadOnlyComposable + get() = TopAppBarColors( + containerColor = TangemTheme.colors.background.secondary, + scrolledContainerColor = TangemTheme.colors.background.secondary, + navigationIconContentColor = TangemTheme.colors.icon.primary1, + titleContentColor = TangemTheme.colors.text.primary1, + actionIconContentColor = TangemTheme.colors.icon.primary1, + ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt new file mode 100644 index 0000000000..777062e8bd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/atoms/radiobutton/TangemRadioButton.kt @@ -0,0 +1,85 @@ +package com.tangem.core.ui.components.atoms.radiobutton + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * [Radio button](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=101-119&t=FH84ljLBk1vmGAei-4) + * + * @param isSelected Whether the radio button is selected + * @param onClick Called when the user clicks the button + * @param modifier Modifier to be applied to the button + * @param isEnabled Whether the button click is enabled + */ +@Composable +fun TangemRadioButton( + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + Box( + modifier = modifier.clickable( + enabled = isEnabled, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16), + onClick = onClick, + ), + ) { + val color = TangemTheme.colors.stroke.secondary + val radius = with(LocalDensity.current) { TangemTheme.dimens.size9.toPx() } + val width = with(LocalDensity.current) { TangemTheme.dimens.size2.toPx() } + Canvas( + modifier = Modifier + .size(TangemTheme.dimens.size24) + .padding(TangemTheme.dimens.spacing2), + ) { + drawCircle( + color = color, + radius = radius, + style = Stroke(width), + ) + } + AnimatedVisibility( + visible = isSelected, + label = "Radio button animation", + modifier = modifier + .size(TangemTheme.dimens.size24), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_circle_24), + contentDescription = null, + tint = TangemTheme.colors.control.checked, + ) + } + } +} + +// region Preview +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TangemRadioButton_Preview() { + var isSelected by remember { mutableStateOf(false) } + TangemThemePreview { + TangemRadioButton(isSelected, onClick = { isSelected = !isSelected }) + } +} +// endregion \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt similarity index 85% rename from features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt index e11a41c41e..24f64babdc 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockCard.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockCard.kt @@ -1,4 +1,4 @@ -package com.tangem.features.details.ui +package com.tangem.core.ui.components.block import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material3.Card @@ -9,9 +9,10 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.res.TangemTheme @Composable -internal fun BlockCard( +fun BlockCard( modifier: Modifier = Modifier, enabled: Boolean = true, + colors: CardColors = TangemBlockCardColors, onClick: () -> Unit = {}, content: @Composable ColumnScope.() -> Unit = {}, ) { @@ -19,13 +20,13 @@ internal fun BlockCard( modifier = modifier, onClick = onClick, shape = TangemTheme.shapes.roundedCornersXMedium, - colors = BlockColors, + colors = colors, enabled = enabled, content = content, ) } -private val BlockColors: CardColors +val TangemBlockCardColors: CardColors @Composable @ReadOnlyComposable get() = CardColors( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt similarity index 61% rename from features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt index 80d99b3b20..d729408a97 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/BlockItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/BlockItem.kt @@ -1,4 +1,4 @@ -package com.tangem.features.details.ui +package com.tangem.core.ui.components.block import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -11,12 +11,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.details.entity.DetailsItemUM @Composable -internal fun BlockItem(model: DetailsItemUM.Basic.Item, modifier: Modifier = Modifier) { +fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) { BlockCard( modifier = modifier, onClick = model.onClick, @@ -29,14 +29,22 @@ internal fun BlockItem(model: DetailsItemUM.Basic.Item, modifier: Modifier = Mod Icon( modifier = Modifier.size(TangemTheme.dimens.size24), painter = painterResource(id = model.iconRes), - tint = TangemTheme.colors.icon.secondary, + tint = when (model.accentType) { + BlockUM.AccentType.NONE -> TangemTheme.colors.icon.secondary + BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent + BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning + }, contentDescription = null, ) Text( - text = model.title.resolveReference(), + text = model.text.resolveReference(), style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, + color = when (model.accentType) { + BlockUM.AccentType.NONE -> TangemTheme.colors.text.primary1 + BlockUM.AccentType.ACCENT -> TangemTheme.colors.text.accent + BlockUM.AccentType.WARNING -> TangemTheme.colors.text.warning + }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt new file mode 100644 index 0000000000..027109f071 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlock.kt @@ -0,0 +1,228 @@ +package com.tangem.core.ui.components.block.information + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.text.TooltipText +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.persistentListOf + +@Immutable +class InformationBlockContentScope(val scope: BoxScope) : BoxScope by scope + +@Composable +fun InformationBlock( + title: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, + action: (@Composable BoxScope.() -> Unit)? = null, + content: (@Composable InformationBlockContentScope.() -> Unit)? = null, +) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.action), + horizontalAlignment = Alignment.Start, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = TangemTheme.dimens.size40) + .padding( + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing6, + ) + .padding(horizontal = TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + modifier = Modifier + .weight(weight = 1f) + .heightIn(min = TangemTheme.dimens.size20), + contentAlignment = Alignment.CenterStart, + content = title, + ) + if (action != null) { + Spacer(modifier = Modifier.size(TangemTheme.dimens.spacing8)) + Box( + modifier = Modifier + .weight(weight = 1f) + .heightIn(min = TangemTheme.dimens.size24), + contentAlignment = Alignment.CenterEnd, + content = action, + ) + } + } + + if (content != null) { + Box( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) { + val scope = InformationBlockContentScope(scope = this) + content(scope) + } + } + } +} + +// region Previews +@Composable +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_Grid() { + TangemThemePreview { + InformationBlock( + title = { + TooltipText( + text = stringReference("Grid title"), + onInfoClick = { }, + ) + }, + content = { + GridItems( + itemPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing4), + items = persistentListOf( + stringReference("Fist item"), + stringReference("Second item"), + ), + itemContent = { + PreviewItem(text = it) + }, + horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) + }, + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_List() { + TangemThemePreview { + InformationBlock( + title = { + Text( + text = "List", + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + }, + action = { + SecondarySmallButton( + config = SmallButtonConfig( + text = stringReference("Add token"), + icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), + onClick = {}, + ), + ) + }, + content = { + ListItems( + items = persistentListOf( + stringReference("Fist item"), + stringReference("Second item"), + ), + itemContent = { + PreviewItem(it) + }, + verticalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) + }, + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_Plain() { + TangemThemePreview { + InformationBlock( + title = { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + TooltipText( + text = stringReference("Title"), + onInfoClick = { }, + ) + + Text( + text = "Subtitle", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + }, + action = { + PreviewItem(text = stringReference("Action")) + }, + ) + } +} + +@Composable +@Preview(showBackground = true, widthDp = 328) +@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_Tree() { + TangemThemePreview { + InformationBlock( + title = { + TooltipText( + text = stringReference("Tree title"), + onInfoClick = { }, + ) + }, + content = { + ArrowRowItems( + itemPadding = PaddingValues(vertical = TangemTheme.dimens.spacing4), + items = persistentListOf( + stringReference("Fist item"), + stringReference("Second item"), + stringReference("Third item"), + ), + rootContent = { + PreviewItem(stringReference("Root")) + }, + itemContent = { + PreviewItem(it) + }, + ) + }, + ) + } +} + +@Composable +private fun PreviewItem(text: TextReference) { + Box( + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors.background.secondary) + .padding(all = TangemTheme.dimens.spacing12), + ) { + Text( + text = text.resolveReference(), + color = TangemTheme.colors.text.primary1, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt new file mode 100644 index 0000000000..1a40b55863 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/information/InformationBlockContent.kt @@ -0,0 +1,113 @@ +package com.tangem.core.ui.components.block.information + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.rows.ArrowRow +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +inline fun InformationBlockContentScope.ListItems( + items: ImmutableList, + itemContent: @Composable BoxScope.(T) -> Unit, + modifier: Modifier = Modifier, + itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), + horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, + verticalArragement: Arrangement.Vertical = Arrangement.Top, +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = horizontalAlignment, + verticalArrangement = verticalArragement, + ) { + items.fastForEach { item -> + Box( + modifier = Modifier + .padding(itemPadding) + .fillMaxWidth(), + ) { + itemContent(item) + } + } + } +} + +@Composable +inline fun InformationBlockContentScope.GridItems( + items: ImmutableList, + itemContent: @Composable BoxScope.(T) -> Unit, + modifier: Modifier = Modifier, + itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), + verticalAlignment: Alignment.Vertical = Alignment.Top, + horizontalArragement: Arrangement.Horizontal = Arrangement.Start, +) { + val rowItems by remember(items) { + derivedStateOf { + items.asSequence() + .windowed(size = 2, step = 2, partialWindows = true) + .map { it.toImmutableList() } + .toImmutableList() + } + } + + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + ) { + rowItems.fastForEach { row -> + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = verticalAlignment, + horizontalArrangement = horizontalArragement, + ) { + row.fastForEach { item -> + Box( + modifier = Modifier + .padding(itemPadding) + .weight(1f), + contentAlignment = Alignment.Center, + ) { + itemContent(item) + } + } + } + } + } +} + +@Composable +inline fun InformationBlockContentScope.ArrowRowItems( + items: ImmutableList, + rootContent: @Composable BoxScope.() -> Unit, + itemContent: @Composable BoxScope.(T) -> Unit, + modifier: Modifier = Modifier, + itemPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart, + content = rootContent, + ) + + items.forEachIndexed { index, item -> + ArrowRow( + modifier = Modifier.fillMaxWidth(), + content = { itemContent(item) }, + contentPadding = itemPadding, + isLastItem = index == items.size - 1, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt new file mode 100644 index 0000000000..df7e04af33 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/block/model/BlockUM.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.components.block.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +data class BlockUM( + val text: TextReference, + @DrawableRes val iconRes: Int, + val onClick: () -> Unit, + val accentType: AccentType = AccentType.NONE, +) { + + enum class AccentType { + NONE, ACCENT, WARNING, + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt index 3f934a0f9b..ec10ddfa0a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/TangemBottomSheet.kt @@ -1,13 +1,14 @@ package com.tangem.core.ui.components.bottomsheets -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch @@ -15,6 +16,7 @@ import kotlinx.coroutines.launch * Tangem bottom sheet with custom draggable header and config * * @param config data model containing logic and ui models + * @param * @param content custom bottom sheet content */ @OptIn(ExperimentalMaterial3Api::class) @@ -22,20 +24,33 @@ import kotlinx.coroutines.launch inline fun TangemBottomSheet( config: TangemBottomSheetConfig, containerColor: Color = TangemTheme.colors.background.primary, + addBottomInsets: Boolean = true, crossinline content: @Composable ColumnScope.(T) -> Unit, ) { var isVisible by remember { mutableStateOf(value = config.isShow) } - + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) if (isVisible && config.content is T) { ModalBottomSheet( + // FIXME temporary solution to fix height of the bottom sheet + modifier = Modifier.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight), onDismissRequest = config.onDismissRequest, sheetState = sheetState, containerColor = containerColor, shape = TangemTheme.shapes.bottomSheetLarge, + windowInsets = WindowInsetsZero, dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) }, ) { - content(config.content) + if (addBottomInsets) { + Column( + // FIXME temporary solution to fix height of the bottom sheet + modifier = Modifier.navigationBarsPadding(), + ) { + content(config.content) + } + } else { + content(config.content) + } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt index 15f640bdbf..e02ae97109 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/SmallButton.kt @@ -6,17 +6,21 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Small button config @@ -28,6 +32,7 @@ import com.tangem.core.ui.res.TangemTheme data class SmallButtonConfig( val text: TextReference, val onClick: () -> Unit, + val icon: TangemButtonIconPosition = TangemButtonIconPosition.None, ) /** @@ -61,13 +66,12 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: label = "Update background color", ) - val textColor by animateColorAsState( - targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1, - label = "Update text color", - ) - Box( + Row( modifier = modifier - .defaultMinSize(minWidth = TangemTheme.dimens.size46, minHeight = TangemTheme.dimens.size24) + .defaultMinSize( + minWidth = TangemTheme.dimens.size46, + minHeight = TangemTheme.dimens.size24, + ) .clip(shape) .background( color = backgroundColor, @@ -75,22 +79,68 @@ private fun SmallButton(config: SmallButtonConfig, isPrimary: Boolean, modifier: ) .clickable(enabled = true, onClick = config.onClick) .padding( - vertical = TangemTheme.dimens.spacing2, + paddingValues = when (config.icon) { + is TangemButtonIconPosition.None -> PaddingValues( + horizontal = TangemTheme.dimens.spacing12, + ) + is TangemButtonIconPosition.End -> PaddingValues( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing8, + ) + is TangemButtonIconPosition.Start -> PaddingValues( + start = TangemTheme.dimens.spacing8, + end = TangemTheme.dimens.spacing12, + ) + }, ), - contentAlignment = Alignment.Center, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, ) { - Text( - modifier = Modifier.padding( - horizontal = TangemTheme.dimens.spacing10, - ), - text = config.text.resolveReference(), - color = textColor, - maxLines = 1, - style = TangemTheme.typography.button, + ContentContainer( + iconPosition = config.icon, + text = { + val textColor by animateColorAsState( + targetValue = if (isPrimary) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1, + label = "Update text color", + ) + + Text( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing4), + text = config.text.resolveReference(), + color = textColor, + maxLines = 1, + style = TangemTheme.typography.button, + ) + }, + icon = { iconResId -> + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = iconResId), + tint = TangemTheme.colors.icon.secondary, + contentDescription = null, + ) + }, ) } } +@Composable +private fun RowScope.ContentContainer( + iconPosition: TangemButtonIconPosition, + text: @Composable RowScope.() -> Unit, + icon: @Composable RowScope.(Int) -> Unit, +) { + if (iconPosition is TangemButtonIconPosition.Start) { + icon(iconPosition.iconResId) + Spacer(modifier = Modifier.requiredWidth(TangemTheme.dimens.spacing4)) + } + text() + if (iconPosition is TangemButtonIconPosition.End) { + Spacer(modifier = Modifier.requiredWidth(TangemTheme.dimens.spacing4)) + icon(iconPosition.iconResId) + } +} + @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -112,5 +162,17 @@ private fun ButtonsSample() { ) PrimarySmallButton(config = config) SecondarySmallButton(config = config.copy(text = TextReference.Str(value = "Add"))) + SecondarySmallButton( + config = config.copy( + text = TextReference.Str(value = "Rating"), + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), + ), + ) + SecondarySmallButton( + config = config.copy( + text = TextReference.Str(value = "Add token"), + icon = TangemButtonIconPosition.Start(iconResId = R.drawable.ic_plus_24), + ), + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt index 344811dac2..f4235c7619 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonColors.kt @@ -6,7 +6,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.graphics.Color -class TangemButtonColors( +data class TangemButtonColors( private val backgroundColor: Color, private val contentColor: Color, private val disabledBackgroundColor: Color, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt index 0bfeb822a6..1101935641 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/segmentedbutton/SegmentedButton.kt @@ -8,10 +8,7 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -21,8 +18,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -42,7 +40,7 @@ import kotlinx.collections.immutable.persistentListOf */ @Composable inline fun SegmentedButtons( - config: PersistentList, + config: ImmutableList, crossinline onClick: (T) -> Unit, modifier: Modifier = Modifier, color: Color = TangemTheme.colors.background.tertiary, @@ -51,7 +49,7 @@ inline fun SegmentedButtons( showIndication: Boolean = true, initialSelectedItem: T? = null, isEnabled: Boolean = true, - crossinline buttonContent: @Composable (T) -> Unit, + crossinline buttonContent: @Composable BoxScope.(T) -> Unit, ) { if (config.isEmpty() || config.size == 1) return @@ -97,7 +95,7 @@ inline fun SegmentedButtons( onClick(config[index]) }, ) { - buttonContent.invoke(config[index]) + buttonContent.invoke(this, config[index]) } } } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt similarity index 92% rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt index 26b442e62a..5296313d28 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.impl.presentation.ui.common +package com.tangem.core.ui.components.containers import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column @@ -18,7 +18,7 @@ import com.tangem.core.ui.res.TangemTheme * @param content field content */ @Composable -internal fun FooterContainer( +fun FooterContainer( modifier: Modifier = Modifier, footer: String? = null, footerTopPadding: Dp = TangemTheme.dimens.spacing8, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt index 005053e2c8..bc9b5f2707 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/DefaultCurrencyIcon.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest -import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon +import com.tangem.core.ui.components.currency.icon.LoadingIcon import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ImageBackgroundContrastChecker import kotlinx.coroutines.launch diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt similarity index 90% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt index cdf96d7b53..089d050c7d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.currency.tokenicon +package com.tangem.core.ui.components.currency.icon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image @@ -18,20 +18,20 @@ import com.tangem.core.ui.res.TangemTheme @Composable internal fun ContentIcon( - icon: TokenIconState, + icon: CurrencyIconState, alpha: Float, colorFilter: ColorFilter?, modifier: Modifier = Modifier, ) { when (icon) { - is TokenIconState.CoinIcon -> CoinIcon( + is CurrencyIconState.CoinIcon -> CoinIcon( modifier = modifier, url = icon.url, fallbackResId = icon.fallbackResId, alpha = alpha, colorFilter = colorFilter, ) - is TokenIconState.TokenIcon -> TokenIcon( + is CurrencyIconState.TokenIcon -> TokenIcon( modifier = modifier, url = icon.url, alpha = alpha, @@ -45,20 +45,20 @@ internal fun ContentIcon( ) }, ) - is TokenIconState.CustomTokenIcon -> CustomTokenIcon( + is CurrencyIconState.CustomTokenIcon -> CustomTokenIcon( modifier = modifier, tint = icon.tint, background = icon.background, alpha = alpha, ) - TokenIconState.Loading, - TokenIconState.Locked, + CurrencyIconState.Loading, + CurrencyIconState.Locked, -> Unit } } @Composable -private fun CoinIcon( +fun CoinIcon( url: String?, @DrawableRes fallbackResId: Int, alpha: Float, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt similarity index 77% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 559bc7bde8..ce67d13f6f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.currency.tokenicon +package com.tangem.core.ui.components.currency.icon import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -26,23 +26,23 @@ import com.tangem.core.ui.utils.NORMAL_ALPHA * @param shouldDisplayNetwork specifies whether to display network badge */ @Composable -fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) { +fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true) { BaseContainer(modifier = modifier) { val iconModifier = Modifier .align(Alignment.Center) .size(TangemTheme.dimens.size36) when (state) { - is TokenIconState.Loading -> LoadingIcon(modifier = iconModifier) - is TokenIconState.Locked -> LockedIcon(modifier = iconModifier) - is TokenIconState.CoinIcon, - is TokenIconState.CustomTokenIcon, - is TokenIconState.TokenIcon, + is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier) + is CurrencyIconState.Locked -> LockedIcon(modifier = iconModifier) + is CurrencyIconState.CoinIcon, + is CurrencyIconState.CustomTokenIcon, + is CurrencyIconState.TokenIcon, -> { ContentIconContainer( icon = state, modifier = iconModifier, - shouldDisplayNetwork = shouldDisplayNetwork, + shouldShowTopBadge = shouldDisplayNetwork, ) } } @@ -70,9 +70,9 @@ private fun LockedIcon(modifier: Modifier = Modifier) { @Composable private fun BoxScope.ContentIconContainer( - icon: TokenIconState, + icon: CurrencyIconState, + shouldShowTopBadge: Boolean, modifier: Modifier = Modifier, - shouldDisplayNetwork: Boolean = true, ) { val networkBadgeOffset = TangemTheme.dimens.spacing4 val (alpha, colorFilter) = remember(icon.isGrayscale) { @@ -90,19 +90,21 @@ private fun BoxScope.ContentIconContainer( colorFilter = colorFilter, ) - if (icon.networkBadgeIconResId != null && shouldDisplayNetwork) { - NetworkBadge( + if (icon.topBadgeIconResId != null && shouldShowTopBadge) { + TopBadge( modifier = Modifier .offset(x = networkBadgeOffset, y = -networkBadgeOffset) .align(Alignment.TopEnd), - iconResId = requireNotNull(icon.networkBadgeIconResId), + iconResId = requireNotNull(icon.topBadgeIconResId), alpha = alpha, colorFilter = colorFilter, ) } if (icon.showCustomBadge) { - CustomBadge(modifier = Modifier.align(Alignment.BottomEnd)) + BottomBadge( + modifier = Modifier.align(Alignment.BottomEnd), + ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt similarity index 75% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 0f7ff350de..85e49532fc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/TokenIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.currency.tokenicon +package com.tangem.core.ui.components.currency.icon import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable @@ -10,11 +10,11 @@ import androidx.compose.ui.graphics.Color * [REDACTED_TODO_COMMENT] */ @Immutable -sealed class TokenIconState { +sealed class CurrencyIconState { abstract val isGrayscale: Boolean abstract val showCustomBadge: Boolean - abstract val networkBadgeIconResId: Int? + abstract val topBadgeIconResId: Int? /** * Represents a coin icon. @@ -29,16 +29,16 @@ sealed class TokenIconState { @DrawableRes val fallbackResId: Int, override val isGrayscale: Boolean, override val showCustomBadge: Boolean, - ) : TokenIconState() { + ) : CurrencyIconState() { - override val networkBadgeIconResId: Int? = null + override val topBadgeIconResId: Int? = null } /** * Represents a token icon. * * @property url The URL where the token icon can be fetched from. May be `null` if not found. - * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property topBadgeIconResId The drawable resource ID for the network badge. * @property isGrayscale Specifies whether to show the icon in grayscale. * @property showCustomBadge Specifies whether to show the custom token badge. * @property fallbackTint The color to be used for tinting the fallback icon. @@ -46,39 +46,39 @@ sealed class TokenIconState { */ data class TokenIcon( val url: String?, - @DrawableRes override val networkBadgeIconResId: Int, + @DrawableRes override val topBadgeIconResId: Int, override val isGrayscale: Boolean, override val showCustomBadge: Boolean, val fallbackTint: Color, val fallbackBackground: Color, - ) : TokenIconState() + ) : CurrencyIconState() /** * Represents a custom token icon. * * @property tint The color to be used for tinting the icon. * @property background The background color to be used for the icon. - * @property networkBadgeIconResId The drawable resource ID for the network badge. + * @property topBadgeIconResId The drawable resource ID for the network badge. * @property isGrayscale Specifies whether to show the icon in grayscale. * @property showCustomBadge Specifies whether to show the custom token badge. */ data class CustomTokenIcon( val tint: Color, val background: Color, - @DrawableRes override val networkBadgeIconResId: Int, + @DrawableRes override val topBadgeIconResId: Int, override val isGrayscale: Boolean, override val showCustomBadge: Boolean = true, - ) : TokenIconState() + ) : CurrencyIconState() - data object Loading : TokenIconState() { + data object Loading : CurrencyIconState() { override val isGrayscale: Boolean = false override val showCustomBadge: Boolean = false - override val networkBadgeIconResId: Int? = null + override val topBadgeIconResId: Int? = null } - data object Locked : TokenIconState() { + data object Locked : CurrencyIconState() { override val isGrayscale: Boolean = false override val showCustomBadge: Boolean = false - override val networkBadgeIconResId: Int? = null + override val topBadgeIconResId: Int? = null } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt similarity index 92% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt index e21ce3274c..09e597f7a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/IconBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/IconBadge.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.currency.tokenicon +package com.tangem.core.ui.components.currency.icon import androidx.annotation.DrawableRes import androidx.compose.foundation.Image @@ -14,7 +14,7 @@ import androidx.compose.ui.res.painterResource import com.tangem.core.ui.res.TangemTheme @Composable -internal fun NetworkBadge( +internal fun TopBadge( @DrawableRes iconResId: Int, alpha: Float, colorFilter: ColorFilter?, @@ -41,7 +41,7 @@ internal fun NetworkBadge( } @Composable -internal fun CustomBadge(modifier: Modifier = Modifier) { +internal fun BottomBadge(modifier: Modifier = Modifier) { Box( modifier = modifier .size(TangemTheme.dimens.size12) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt similarity index 81% rename from core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt index 3e62e98992..c331fc8a1d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/tokenicon/converter/CryptoCurrencyToIconStateConverter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt @@ -1,6 +1,6 @@ -package com.tangem.core.ui.components.currency.tokenicon.converter +package com.tangem.core.ui.components.currency.icon.converter -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.getTintForTokenIcon import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon @@ -9,11 +9,11 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.converter.Converter /** - * Converts [CryptoCurrencyStatus] to [TokenIconState] + * Converts [CryptoCurrencyStatus] to [CurrencyIconState] */ -class CryptoCurrencyToIconStateConverter : Converter { +class CryptoCurrencyToIconStateConverter : Converter { - override fun convert(value: CryptoCurrencyStatus): TokenIconState { + override fun convert(value: CryptoCurrencyStatus): CurrencyIconState { return when (val currency = value.currency) { is CryptoCurrency.Coin -> getIconStateForCoin(currency, value.value.isError) is CryptoCurrency.Token -> getIconStateForToken(currency, value.value.isError) @@ -24,7 +24,7 @@ class CryptoCurrencyToIconStateConverter : Converter getIconStateForCoin( coin = currency, @@ -41,7 +41,7 @@ class CryptoCurrencyToIconStateConverter : Converter getIconStateForCoin(currency, isUnreachable = false) is CryptoCurrency.Token -> getIconStateForToken(currency, isErrorStatus = false) @@ -53,8 +53,8 @@ class CryptoCurrencyToIconStateConverter : Converter( + collection = listOf( + SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "BTC", + onQueryChange = {}, + isActive = true, + onActiveChange = {}, + ), + SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), + ), +) +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt new file mode 100644 index 0000000000..d2ac8fcc8b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt @@ -0,0 +1,11 @@ +package com.tangem.core.ui.components.fields.entity + +import com.tangem.core.ui.extensions.TextReference + +data class SearchBarUM( + val placeholderText: TextReference, + val query: String, + val onQueryChange: (String) -> Unit, + val isActive: Boolean, + val onActiveChange: (Boolean) -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt index 2bd8311866..4a2a746428 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowApprox.kt @@ -12,13 +12,13 @@ import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * [Input Row Approx](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2207-810&mode=design&t=fM1ZU6zQF6g3CaTv-4) @@ -35,10 +35,10 @@ import com.tangem.core.ui.res.TangemTheme @Suppress("LongParameterList") @Composable fun InputRowApprox( - leftIcon: TokenIconState, + leftIcon: CurrencyIconState, leftTitle: TextReference, leftSubtitle: TextReference, - rightIcon: TokenIconState, + rightIcon: CurrencyIconState, rightTitle: TextReference, rightSubtitle: TextReference, modifier: Modifier = Modifier, @@ -86,7 +86,7 @@ fun InputRowApprox( @Composable private fun InputRowApproxItem( - iconState: TokenIconState, + iconState: CurrencyIconState, title: TextReference, subtitle: TextReference, modifier: Modifier = Modifier, @@ -95,7 +95,7 @@ private fun InputRowApproxItem( Row( modifier = modifier, ) { - TokenIcon( + CurrencyIcon( state = iconState, modifier = Modifier .size(TangemTheme.dimens.size36), @@ -131,11 +131,11 @@ private fun InputRowApproxPreview() { TangemThemePreview { Column { InputRowApprox( - leftIcon = TokenIconState.Loading, + leftIcon = CurrencyIconState.Loading, leftTitle = TextReference.Str("Left title USD"), leftSubtitle = TextReference.Str("Left subtitle USD"), leftTitleEllipsisOffset = 3, - rightIcon = TokenIconState.Loading, + rightIcon = CurrencyIconState.Loading, rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"), rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"), rightTitleEllipsisOffset = 3, @@ -143,11 +143,11 @@ private fun InputRowApproxPreview() { .background(TangemTheme.colors.background.action), ) InputRowApprox( - leftIcon = TokenIconState.Loading, + leftIcon = CurrencyIconState.Loading, leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"), leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"), leftTitleEllipsisOffset = 3, - rightIcon = TokenIconState.Loading, + rightIcon = CurrencyIconState.Loading, rightTitle = TextReference.Str("Right title USD"), rightSubtitle = TextReference.Str("Right subtitle USD"), rightTitleEllipsisOffset = 3, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt index 3f711de362..0a17c85b13 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowBestRate.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon @@ -14,18 +13,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview @@ -63,7 +59,7 @@ fun InputRowBestRate( modifier = Modifier .padding(TangemTheme.dimens.spacing12), ) { - InnerIcon(imageUrl = imageUrl) + InputRowAsyncImage(imageUrl = imageUrl, modifier = Modifier.size(TangemTheme.dimens.spacing40)) Column( modifier = Modifier .padding(start = TangemTheme.dimens.spacing12), @@ -131,29 +127,6 @@ private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag: } } -@Composable -private fun InnerIcon(imageUrl: String) { - SubcomposeAsyncImage( - modifier = Modifier.size(TangemTheme.dimens.size40), - model = ImageRequest.Builder(context = LocalContext.current) - .data(imageUrl) - .crossfade(enable = true) - .allowHardware(enable = false) - .build(), - loading = { LoadingIcon() }, - error = { - Box( - modifier = Modifier - .background( - color = TangemTheme.colors.background.tertiary, - shape = CircleShape, - ), - ) - }, - contentDescription = null, - ) -} - //region preview @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt index 4222d238c0..1eff068d9f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt @@ -4,12 +4,16 @@ import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -20,8 +24,8 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4) @@ -64,7 +68,7 @@ fun InputRowDefault( ) { Text( text = title.resolveReference(), - style = TangemTheme.typography.caption2, + style = TangemTheme.typography.subtitle2, color = titleColor, ) Text( @@ -80,11 +84,13 @@ fun InputRowDefault( contentDescription = null, tint = iconTint, modifier = Modifier + .align(CenterVertically) .padding( top = TangemTheme.dimens.spacing10, bottom = TangemTheme.dimens.spacing10, ) .clickable( + enabled = onIconClick != null, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), ) { onIconClick?.invoke() }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt index 35ba18ad8a..58a61bb00e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImage.kt @@ -18,13 +18,13 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.inputrow.inner.DividerContainer -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * [Input Row Image](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-813&mode=design&t=IQ5lBJEkFGU4WSvi-4) @@ -32,7 +32,7 @@ import com.tangem.core.ui.res.TangemTheme * @param title title reference * @param subtitle subtitle reference * @param caption caption reference - * @param tokenIconState token icon state [TokenIconState] + * @param tokenIconState token icon state [CurrencyIconState] * @param modifier modifier * @param titleColor title color * @param subtitleColor subtitle color @@ -48,7 +48,7 @@ fun InputRowImage( title: TextReference, subtitle: TextReference, caption: TextReference, - tokenIconState: TokenIconState, + tokenIconState: CurrencyIconState, modifier: Modifier = Modifier, titleColor: Color = TangemTheme.colors.text.secondary, subtitleColor: Color = TangemTheme.colors.text.primary1, @@ -79,7 +79,7 @@ fun InputRowImage( top = TangemTheme.dimens.spacing6, ), ) { - TokenIcon( + CurrencyIcon( state = tokenIconState, shouldDisplayNetwork = showNetworkIcon, modifier = Modifier @@ -146,7 +146,7 @@ private data class InputRowImagePreviewData( val title: TextReference, val subtitle: TextReference, val caption: TextReference, - val iconState: TokenIconState, + val iconState: CurrencyIconState, val showDivider: Boolean, val actionIconRes: Int?, val showNetworkIcon: Boolean = false, @@ -160,7 +160,7 @@ private class InputRowImagePreviewDataProvider : title = TextReference.Str("title"), subtitle = TextReference.Str("subtitle"), caption = TextReference.Str("caption"), - iconState = TokenIconState.Locked, + iconState = CurrencyIconState.Locked, actionIconRes = null, showDivider = false, showNetworkIcon = false, @@ -169,7 +169,7 @@ private class InputRowImagePreviewDataProvider : title = TextReference.Str("title"), subtitle = TextReference.Str("subtitle"), caption = TextReference.Str("caption"), - iconState = TokenIconState.Locked, + iconState = CurrencyIconState.Locked, actionIconRes = R.drawable.ic_chevron_right_24, showDivider = true, showNetworkIcon = true, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt new file mode 100644 index 0000000000..871dccfd94 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt @@ -0,0 +1,51 @@ +package com.tangem.core.ui.components.inputrow + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun InputRowImageBase( + subtitle: TextReference, + caption: TextReference, + imageUrl: String, + modifier: Modifier = Modifier, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, + extraContent: @Composable RowScope.() -> Unit = {}, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = modifier, + ) { + InputRowAsyncImage( + imageUrl = imageUrl, + modifier = Modifier + .size(TangemTheme.dimens.spacing36) + .padding(vertical = TangemTheme.dimens.size1), + ) + Column { + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = subtitleColor, + ) + Text( + text = caption.resolveAnnotatedReference(), + style = TangemTheme.typography.caption2, + color = captionColor, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing2), + ) + } + extraContent() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt new file mode 100644 index 0000000000..83fd0ccbf0 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt @@ -0,0 +1,79 @@ +package com.tangem.core.ui.components.inputrow + +import android.content.res.Configuration +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Input row component with selector + * [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2100-842&t=hoBXmDX8NeLrp4p6-4) + * + * @param subtitle subtitle text + * @param caption caption text + * @param imageUrl icon to load + * @param modifier modifier + * @param subtitleColor subtitle text color + * @param captionColor caption text color + */ +@Composable +fun InputRowImageChevron( + subtitle: TextReference, + caption: TextReference, + imageUrl: String, + modifier: Modifier = Modifier, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, +) { + InputRowImageBase( + subtitle = subtitle, + caption = caption, + imageUrl = imageUrl, + modifier = modifier, + subtitleColor = subtitleColor, + captionColor = captionColor, + ) { + SpacerWMax() + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun InputRowImageChevron_Preview() { + TangemThemePreview { + InputRowImageChevron( + subtitle = stringReference("Binance"), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(SpanStyle(TangemTheme.colors.text.accent)) { + stringReference("3,54%") + } + }, + ), + ), + imageUrl = "", + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt new file mode 100644 index 0000000000..c4fb398c6e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt @@ -0,0 +1,114 @@ +package com.tangem.core.ui.components.inputrow + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.atoms.text.EllipsisText +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * Input row component with selector + * [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2841-1589&t=u6pOF6lsdpvWLELb-4) + * + * @param title title text + * @param subtitle subtitle text + * @param caption caption text + * @param infoTitle info title text + * @param infoSubtitle info subtitle text + * @param modifier modifier + * @param imageUrl icon to load + * @param subtitleColor subtitle text color + * @param captionColor caption text color + */ +@Suppress("LongParameterList") +@Composable +fun InputRowImageInfo( + subtitle: TextReference, + caption: TextReference, + infoTitle: TextReference, + infoSubtitle: TextReference, + imageUrl: String, + modifier: Modifier = Modifier, + title: TextReference? = null, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, +) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), + modifier = modifier + .padding(TangemTheme.dimens.spacing12), + ) { + if (title != null) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + } + InputRowImageBase( + subtitle = subtitle, + caption = caption, + imageUrl = imageUrl, + subtitleColor = subtitleColor, + captionColor = captionColor, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + horizontalAlignment = Alignment.End, + modifier = Modifier.weight(1f), + ) { + EllipsisText( + text = infoTitle.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + EllipsisText( + text = infoSubtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun InputRowImageInfo_Preview() { + TangemThemePreview { + InputRowImageInfo( + title = stringReference("Active"), + subtitle = stringReference("Binance"), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(SpanStyle(TangemTheme.colors.text.accent)) { + stringReference("3,54%") + } + }, + ), + ), + infoTitle = stringReference("5431231231231231231231232 USD"), + infoSubtitle = stringReference("5 SOL"), + imageUrl = "", + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt new file mode 100644 index 0000000000..9e672eb56b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageSelector.kt @@ -0,0 +1,131 @@ +package com.tangem.core.ui.components.inputrow + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.atoms.radiobutton.TangemRadioButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.annotatedReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.BigDecimalFormatter +import java.math.BigDecimal + +/** + * Input row component with selector + * [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2772-905&t=FH84ljLBk1vmGAei-4) + * + * @param subtitle subtitle text + * @param caption caption text + * @param imageUrl icon to load + * @param onSelect callback when selected + * @param modifier modifier + * @param subtitleColor subtitle text color + * @param captionColor caption text color + * @param isSelected true if selected + */ +@Composable +fun InputRowImageSelector( + subtitle: TextReference, + caption: TextReference, + imageUrl: String, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + subtitleColor: Color = TangemTheme.colors.text.primary1, + captionColor: Color = TangemTheme.colors.text.tertiary, + isSelected: Boolean = false, +) { + InputRowImageBase( + subtitle = subtitle, + caption = caption, + imageUrl = imageUrl, + subtitleColor = subtitleColor, + captionColor = captionColor, + modifier = modifier + .clickable( + onClick = onSelect, + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + ) + .padding(TangemTheme.dimens.spacing12), + ) { + TangemRadioButton(isSelected = isSelected, isEnabled = false, onClick = onSelect) + } +} + +//region preview +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun InputRowImageSelectorPreview( + @PreviewParameter(InputRowImageSelectorPreviewDataProvider::class) data: InputRowImageSelectorPreviewData, +) { + TangemThemePreview { + InputRowImageSelector( + modifier = Modifier.background(TangemTheme.colors.background.action), + subtitle = data.subtitle, + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) { + append( + BigDecimalFormatter.formatPercent(BigDecimal.ZERO, true), + ) + } + }, + ), + ), + imageUrl = "", + isSelected = false, + onSelect = {}, + ) + } +} + +private data class InputRowImageSelectorPreviewData( + val subtitle: TextReference, + val caption: TextReference, + val showDivider: Boolean, + val actionIconRes: Int?, + val isSelected: Boolean, +) + +private class InputRowImageSelectorPreviewDataProvider : + PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InputRowImageSelectorPreviewData( + subtitle = TextReference.Str("subtitle"), + caption = TextReference.Str("caption"), + actionIconRes = null, + showDivider = false, + isSelected = false, + ), + InputRowImageSelectorPreviewData( + subtitle = TextReference.Str("subtitle"), + caption = TextReference.Str("caption"), + actionIconRes = R.drawable.ic_chevron_right_24, + showDivider = true, + isSelected = true, + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt new file mode 100644 index 0000000000..3fd1ae7722 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/InputRowAsyncImage.kt @@ -0,0 +1,41 @@ +package com.tangem.core.ui.components.inputrow.inner + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.currency.icon.LoadingIcon +import com.tangem.core.ui.res.TangemTheme + +/** + * Loads image by url for icon in the input row + * + * @param imageUrl url of the image + * @param modifier modifier + */ +@Composable +internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier) { + SubcomposeAsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(imageUrl) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { LoadingIcon() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors.background.tertiary, + shape = CircleShape, + ), + ) + }, + contentDescription = null, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 606416093a..8ef37d1f7c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -4,14 +4,12 @@ import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -20,8 +18,8 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter /** @@ -116,7 +114,10 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { ) { Price(price = marketPriceBlockState.price, modifier = priceModifier) - PriceChangeInPercent(marketPriceBlockState.priceChangeConfig) + PriceChangeInPercent( + valueInPercent = marketPriceBlockState.priceChangeConfig.valueInPercent, + type = marketPriceBlockState.priceChangeConfig.type, + ) } } else { Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier) @@ -136,47 +137,6 @@ private fun Price(price: String, modifier: Modifier = Modifier) { ) } -@Composable -private fun PriceChangeInPercent(config: PriceChangeState.Content) { - AnimatedContent( - targetState = config.type, - contentAlignment = Alignment.CenterStart, - label = "Update price change", - ) { type -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2), - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size8), - painter = painterResource( - id = when (type) { - PriceChangeType.UP -> R.drawable.ic_arrow_up_8 - PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 - PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 - }, - ), - tint = when (type) { - PriceChangeType.UP -> TangemTheme.colors.icon.accent - PriceChangeType.DOWN -> TangemTheme.colors.icon.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive - }, - contentDescription = null, - ) - - Text( - text = config.valueInPercent, - color = when (type) { - PriceChangeType.UP -> TangemTheme.colors.text.accent - PriceChangeType.DOWN -> TangemTheme.colors.text.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled - }, - style = TangemTheme.typography.body2, - ) - } - } -} - @Composable private fun LoadingContent() { Row( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt new file mode 100644 index 0000000000..d2c55ed576 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt @@ -0,0 +1,96 @@ +package com.tangem.core.ui.components.marketprice + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun PriceChangeInPercent( + valueInPercent: String, + type: PriceChangeType, + modifier: Modifier = Modifier, + textStyle: TextStyle = TangemTheme.typography.body2, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2), + ) { + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size8) + .align(Alignment.CenterVertically), + imageVector = ImageVector.vectorResource( + id = when (type) { + PriceChangeType.UP -> R.drawable.ic_arrow_up_8 + PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 + PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 + }, + ), + tint = when (type) { + PriceChangeType.UP -> TangemTheme.colors.icon.accent + PriceChangeType.DOWN -> TangemTheme.colors.icon.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive + }, + contentDescription = null, + ) + + Text( + text = valueInPercent, + color = when (type) { + PriceChangeType.UP -> TangemTheme.colors.text.accent + PriceChangeType.DOWN -> TangemTheme.colors.text.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled + }, + style = textStyle, + overflow = TextOverflow.Visible, + maxLines = 1, + ) + } +} + +//region Preview + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + Column { + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.NEUTRAL, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.UP, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.DOWN, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.DOWN, + textStyle = TangemTheme.typography.caption2, + ) + } + } +} + +//endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt index 9e6ba3db1d..6fc2f27ebd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/TravalaNotificationWithBackground.kt @@ -36,6 +36,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemColorPalette.White import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Travala notification with image background @@ -192,7 +193,7 @@ private fun formatSubtitle(subtitle: String): AnnotatedString { @Preview @Composable private fun TravalaNotificationWithBackgroundPreview() { - TangemTheme { + TangemThemePreview { TravalaNotificationWithBackground( config = NotificationConfig( title = resourceReference( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt new file mode 100644 index 0000000000..0e594fbd91 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ArrowRow.kt @@ -0,0 +1,168 @@ +package com.tangem.core.ui.components.rows + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.* + +@Composable +inline fun ArrowRow( + isLastItem: Boolean, + content: @Composable() (BoxScope.() -> Unit), + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(all = TangemTheme.dimens.spacing0), +) { + val density = LocalDensity.current.density + val defaultRowHeight = TangemTheme.dimens.size0 + var itemHeight by remember { mutableStateOf(defaultRowHeight) } + + Row( + modifier = modifier.onSizeChanged { size -> + val height = size.height.toFloat() + if (height != itemHeight.toPx(density)) { + itemHeight = convertPxToDp(px = height, density = density) + } + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + ) { + ChildArrow( + childHeight = itemHeight, + isLastChild = isLastItem, + ) + + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart, + ) { + content() + } + } +} + +@Suppress("LongParameterList") +@Immutable +private class ChildArrowScope( + val figureRect: Rect, + val arrowHeadRect: Rect, + val curvedArrowRect: Rect, + val arrowStrokeWidth: Float, + val arrowHeadRadius: Float, + val strokeColor: Color, + drawScope: DrawScope, +) : DrawScope by drawScope + +@Composable +fun ChildArrow(childHeight: Dp, isLastChild: Boolean) { + val figureWidth = TangemTheme.dimens.size40 + + val strokeColor = TangemTheme.colors.stroke.secondary + val arrowStrokeWidthDp = TangemTheme.dimens.size1 + + val arrowHeadRadiusDp = TangemTheme.dimens.size1 + + val figureRectDp = DpRect( + origin = DpOffset.Zero, + size = DpSize(width = TangemTheme.dimens.size40, height = childHeight), + ) + + val arrowHeadSize = DpSize( + width = TangemTheme.dimens.size6, + height = TangemTheme.dimens.size6, + ) + val arrowHeadRectDp = DpRect( + origin = DpOffset( + x = figureWidth - arrowHeadSize.width, + y = figureRectDp.size.center.y - arrowHeadSize.center.y, + ), + size = arrowHeadSize, + ) + + val curvedArrowRectDp = DpRect( + top = figureRectDp.top, + left = TangemTheme.dimens.size18, + right = figureRectDp.right - arrowHeadRectDp.width, + bottom = figureRectDp.size.center.y, + ) + + Canvas( + modifier = Modifier + .width(figureWidth) + .height(childHeight), + ) { + val scope = ChildArrowScope( + figureRect = figureRectDp.toRect(), + arrowHeadRect = arrowHeadRectDp.toRect(), + curvedArrowRect = curvedArrowRectDp.toRect(), + arrowStrokeWidth = arrowStrokeWidthDp.toPx(), + arrowHeadRadius = arrowHeadRadiusDp.toPx(), + strokeColor = strokeColor, + drawScope = this, + ) + + scope.drawCurveArrow() + scope.drawArrowHead() + + if (!isLastChild) { + scope.drawArrowLine() + } + } +} + +private fun ChildArrowScope.drawArrowHead() { + val arrowHeadPath = Path().apply { + moveTo(arrowHeadRect.centerRight) + lineTo(arrowHeadRect.topLeft) + lineTo(arrowHeadRect.bottomLeft) + close() + } + val paint = Paint().apply { + color = strokeColor + style = PaintingStyle.Fill + pathEffect = PathEffect.cornerPathEffect(arrowHeadRadius) + } + drawIntoCanvas { canvas -> + canvas.drawOutline( + outline = Outline.Generic(arrowHeadPath), + paint = paint, + ) + } +} + +private fun ChildArrowScope.drawCurveArrow() { + val curveArrowPath = Path().apply { + moveTo(curvedArrowRect.topLeft) + quadraticBezierTo( + control = curvedArrowRect.bottomLeft, + end = curvedArrowRect.bottomRight, + ) + } + drawPath( + path = curveArrowPath, + color = strokeColor, + style = Stroke(width = arrowStrokeWidth), + ) +} + +private fun ChildArrowScope.drawArrowLine() { + drawLine( + color = strokeColor, + start = curvedArrowRect.topLeft, + end = Offset(curvedArrowRect.left, figureRect.bottom), + strokeWidth = arrowStrokeWidth, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt new file mode 100644 index 0000000000..d0341755a7 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/BlockchainRow.kt @@ -0,0 +1,161 @@ +package com.tangem.core.ui.components.rows + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +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.Icon +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.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.rows.model.BlockchainRowUM +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2737-2800&t=ewlXfWwbDnRhjw4B-4) + * */ +@Composable +fun BlockchainRow(model: BlockchainRowUM, action: @Composable BoxScope.() -> Unit, modifier: Modifier = Modifier) { + RowContentContainer( + modifier = modifier + .heightIn(min = TangemTheme.dimens.size52) + .padding( + vertical = TangemTheme.dimens.spacing16, + horizontal = TangemTheme.dimens.spacing8, + ), + icon = { + RowIcon( + resId = model.iconResId, + isColored = model.isSelected, + showAccentBadge = model.isMainNetwork, + ) + }, + text = { + RowText( + mainText = model.name, + secondText = model.type, + accentMainText = model.isSelected, + accentSecondText = model.isMainNetwork, + ) + }, + action = action, + ) +} + +@Composable +private fun RowIcon( + @DrawableRes resId: Int, + isColored: Boolean, + showAccentBadge: Boolean, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier.size(TangemTheme.dimens.size24), + ) { + if (isColored) { + Image( + modifier = Modifier + .align(Alignment.Center) + .size(TangemTheme.dimens.size22), + painter = painterResource(id = resId), + contentDescription = null, + ) + } else { + Icon( + modifier = Modifier + .align(Alignment.Center) + .background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ) + .size(TangemTheme.dimens.size22), + painter = painterResource(id = resId), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + + if (showAccentBadge) { + Badge(modifier = Modifier.align(Alignment.TopEnd)) + } + } +} + +@Composable +private fun Badge(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size8) + .background( + color = TangemTheme.colors.background.primary, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size5) + .background( + color = TangemTheme.colors.icon.accent, + shape = CircleShape, + ), + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_BlockchainRow(@PreviewParameter(BlockchainRowParameterProvider::class) state: BlockchainRowUM) { + TangemThemePreview { + ArrowRow( + modifier = Modifier.background(TangemTheme.colors.background.primary), + isLastItem = false, + content = { + BlockchainRow( + model = state, + action = { + TangemSwitch(onCheckedChange = { /* [REDACTED_TODO_COMMENT]*/ }, checked = true) + }, + ) + }, + ) + } +} + +private class BlockchainRowParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + BlockchainRowUM( + name = "BNB BEACON CHAIN", + type = "BEP20", + iconResId = R.drawable.img_bsc_22, + isMainNetwork = true, + isSelected = true, + ), + BlockchainRowUM( + name = "1234567890111213141516171819", + type = "BEP20", + iconResId = R.drawable.ic_bsc_16, + isMainNetwork = true, + isSelected = false, + ), + BlockchainRowUM( + name = "BNB BEACON CHAIN", + type = "1234567890111213141516171819", + iconResId = R.drawable.ic_bsc_16, + isMainNetwork = false, + isSelected = false, + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt new file mode 100644 index 0000000000..175ace75f5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt @@ -0,0 +1,112 @@ +package com.tangem.core.ui.components.rows + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +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.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.rows.model.ChainRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +/** + * [Figma Component](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=1608-1147&t=ewlXfWwbDnRhjw4B-4) + * */ +@Composable +fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composable BoxScope.() -> Unit = {}) { + RowContentContainer( + modifier = modifier + .heightIn(min = TangemTheme.dimens.size68) + .padding( + vertical = TangemTheme.dimens.spacing12, + horizontal = TangemTheme.dimens.spacing14, + ), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + icon = { + CurrencyIcon( + state = model.icon, + shouldDisplayNetwork = true, + ) + }, + text = { + RowText( + mainText = model.name, + secondText = model.type, + subtitle = if (model.showCustom) { + resourceReference(R.string.common_custom) + } else { + null + }, + accentMainText = true, + accentSecondText = false, + ) + }, + action = action, + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_ChainRow(@PreviewParameter(ChainRowParameterProvider::class) state: ChainRowUM) { + TangemThemePreview { + ChainRow( + modifier = Modifier.background(TangemTheme.colors.background.primary), + model = state, + action = { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = { /* [REDACTED_TODO_COMMENT]*/ }, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + tint = TangemTheme.colors.icon.informative, + painter = painterResource(id = R.drawable.ic_chevron_24), + contentDescription = null, + ) + } + }, + ) + } +} + +private class ChainRowParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + ChainRowUM( + name = "Cardano", + type = "ADA", + icon = CurrencyIconState.Locked, + showCustom = true, + ), + ChainRowUM( + name = "Binance", + type = "BNB", + icon = CurrencyIconState.Locked, + showCustom = false, + ), + ChainRowUM( + name = "123456789010111213141516", + type = "BNB", + icon = CurrencyIconState.Locked, + showCustom = true, + ), + ChainRowUM( + name = "123456789010111213141516", + type = "123456789010111213141516", + icon = CurrencyIconState.Locked, + showCustom = false, + ), + ), +) +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt new file mode 100644 index 0000000000..c2b1e97286 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RoundableCornersRow.kt @@ -0,0 +1,157 @@ +package com.tangem.core.ui.components.rows + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +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.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Suppress("LongParameterList") +@Composable +fun RoundableCornersRow( + startText: String, + startTextColor: Color, + startTextStyle: TextStyle, + endText: String, + endTextColor: Color, + endTextStyle: TextStyle, + cornersToRound: CornersToRound, + iconResId: Int? = null, + iconClick: (() -> Unit)? = null, +) { + Surface( + shape = cornersToRound.getShape(), + color = TangemTheme.colors.background.primary, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = startText, + color = startTextColor, + maxLines = 1, + style = startTextStyle, + ) + if (iconResId != null && iconClick != null) { + Icon( + modifier = Modifier + .padding(TangemTheme.dimens.spacing4) + .size(TangemTheme.dimens.size16) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.radius10), + onClick = iconClick, + ), + painter = painterResource(id = R.drawable.ic_alert_24), + contentDescription = null, + tint = TangemTheme.colors.text.tertiary, + ) + } + Spacer(modifier = Modifier.weight(1f)) + Text( + text = endText, + color = endTextColor, + maxLines = 1, + style = endTextStyle, + ) + } + } +} + +enum class CornersToRound { + + ALL_4, + TOP_2, + BOTTOM_2, + ZERO, + ; + + @Suppress("TopLevelComposableFunctions") + @Composable + fun getShape(): RoundedCornerShape { + val radius = TangemTheme.dimens.radius12 + return when (this) { + ALL_4 -> RoundedCornerShape(radius) + TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) + BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + ZERO -> RoundedCornerShape(0.dp) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_RoundableCornersRow( + @PreviewParameter(RoundableCornersRowDataProvider::class) previewData: RoundableCornersRowPreviewData, +) { + TangemThemePreview { + Box(modifier = Modifier.background(color = TangemTheme.colors.icon.attention)) { + RoundableCornersRow( + startText = previewData.startText, + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = previewData.endText, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = previewData.cornersToRound, + iconResId = previewData.iconResId, + ) + } + } +} + +private data class RoundableCornersRowPreviewData( + val startText: String, + val endText: String, + val cornersToRound: CornersToRound, + val iconResId: Int? = null, +) + +private class RoundableCornersRowDataProvider : + PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + getPreviewData(cornersToRound = CornersToRound.ZERO), + getPreviewData(cornersToRound = CornersToRound.TOP_2), + getPreviewData(cornersToRound = CornersToRound.BOTTOM_2), + getPreviewData(cornersToRound = CornersToRound.ZERO, iconResId = R.drawable.ic_alert_24), + getPreviewData(cornersToRound = CornersToRound.TOP_2, iconResId = R.drawable.ic_alert_24), + getPreviewData(cornersToRound = CornersToRound.BOTTOM_2, iconResId = R.drawable.ic_alert_24), + ) + + private fun getPreviewData(cornersToRound: CornersToRound, iconResId: Int? = null) = RoundableCornersRowPreviewData( + startText = "startText", + endText = "endText", + cornersToRound = cornersToRound, + iconResId = iconResId, + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..f93f42b41d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt @@ -0,0 +1,95 @@ +package com.tangem.core.ui.components.rows + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.isNullOrEmpty +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal inline fun RowContentContainer( + icon: @Composable BoxScope.() -> Unit, + text: @Composable BoxScope.() -> Unit, + action: @Composable BoxScope.() -> Unit, + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(TangemTheme.dimens.spacing8), +) { + Row( + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = horizontalArrangement, + ) { + Box( + contentAlignment = Alignment.Center, + content = icon, + ) + Box( + modifier = Modifier + .weight(1f) + .heightIn(min = TangemTheme.dimens.size22), + contentAlignment = Alignment.CenterStart, + content = text, + ) + Box( + modifier = Modifier + .requiredWidthIn(max = TangemTheme.dimens.size80) + .heightIn(min = TangemTheme.dimens.size24), + contentAlignment = Alignment.CenterEnd, + content = action, + ) + } +} + +@Composable +internal fun RowText( + mainText: String, + secondText: String, + accentMainText: Boolean, + accentSecondText: Boolean, + modifier: Modifier = Modifier, + subtitle: TextReference? = null, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Text( + modifier = Modifier.weight(weight = 10f, fill = false), + text = mainText, + style = TangemTheme.typography.subtitle2, + color = if (accentMainText) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + modifier = Modifier.weight(weight = 4f, fill = false), + text = secondText, + style = TangemTheme.typography.body2, + color = if (accentSecondText) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + if (!subtitle.isNullOrEmpty()) { + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt index c1e06e7270..5eb6b5f287 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/SelectorRowItem.kt @@ -25,6 +25,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.utils.Strings @Composable fun SelectorRowItem( @@ -127,7 +128,7 @@ private fun RowScope.SelectorValueContent( ) if (postDot != null) { Text( - text = "•", + text = Strings.DOT, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt new file mode 100644 index 0000000000..f8e756a9ae --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/BlockchainRowUM.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.components.rows.model + +import androidx.compose.runtime.Immutable + +@Immutable +data class BlockchainRowUM( + val name: String, + val type: String, + val iconResId: Int, + val isMainNetwork: Boolean, + val isSelected: Boolean, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt new file mode 100644 index 0000000000..c0c3764c5e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.components.rows.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.icon.CurrencyIconState + +@Immutable +data class ChainRowUM( + val name: String, + val type: String, + val icon: CurrencyIconState, + val showCustom: Boolean, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt new file mode 100644 index 0000000000..8112024b54 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/Showcase.kt @@ -0,0 +1,143 @@ +package com.tangem.core.ui.components.showcase + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.R +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel +import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * @param headerIconRes big header icon + * @param headerText header text + * @param showcaseItems list of bullet points + * @param primaryButton primary button + * @param secondaryButton secondary button + * @param modifier compose modifier + * + * @see Figma component + */ +@Composable +fun Showcase( + @DrawableRes headerIconRes: Int, + headerText: TextReference, + showcaseItems: ImmutableList, + primaryButton: ShowcaseButtonModel, + secondaryButton: ShowcaseButtonModel, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxSize(), + ) { + ShowcaseContent( + headerIconRes = headerIconRes, + headerText = headerText, + showcaseItems = showcaseItems, + modifier = Modifier + .weight(1f) + .align(Alignment.CenterHorizontally), + ) + ShowcaseButtons( + primaryButtonText = primaryButton.buttonText, + onPrimaryClick = primaryButton.onClick, + secondaryButtonText = secondaryButton.buttonText, + onSecondaryClick = secondaryButton.onClick, + ) + } +} + +@Composable +private fun ShowcaseButtons( + primaryButtonText: TextReference, + secondaryButtonText: TextReference, + onPrimaryClick: () -> Unit, + onSecondaryClick: () -> Unit, + hint: TextReference? = null, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + PrimaryButton( + text = primaryButtonText.resolveReference(), + onClick = onPrimaryClick, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), + ) + SecondaryButton( + text = secondaryButtonText.resolveReference(), + onClick = onSecondaryClick, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing16, + ), + ) + hint?.let { + Text( + text = it.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing12, + ), + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 720) +@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Showcase_Preview() { + TangemThemePreview { + Showcase( + headerIconRes = R.drawable.ic_notifications_unread_24, + headerText = resourceReference(R.string.user_push_notification_agreement_header), + showcaseItems = persistentListOf( + ShowcaseItemModel( + R.drawable.ic_rocket_launch_24, + resourceReference(R.string.user_push_notification_agreement_argument_one), + ), + ShowcaseItemModel( + R.drawable.ic_storefront_24, + resourceReference(R.string.user_push_notification_agreement_argument_two), + ), + ), + primaryButton = ShowcaseButtonModel(resourceReference(R.string.common_allow), {}), + secondaryButton = ShowcaseButtonModel(resourceReference(R.string.common_later), {}), + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt new file mode 100644 index 0000000000..336172a208 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseContent.kt @@ -0,0 +1,71 @@ +package com.tangem.core.ui.components.showcase + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun ShowcaseContent( + @DrawableRes headerIconRes: Int, + headerText: TextReference, + showcaseItems: ImmutableList, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.Center, + modifier = modifier, + ) { + Icon( + painter = painterResource(id = headerIconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(TangemTheme.dimens.size56), + ) + Text( + text = headerText.resolveReference(), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing34, + top = TangemTheme.dimens.spacing28, + ), + ) + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing34, + top = TangemTheme.dimens.spacing28, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + repeat(showcaseItems.size) { index -> + ShowcaseItem( + iconRes = showcaseItems[index].iconRes, + text = showcaseItems[index].text, + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt new file mode 100644 index 0000000000..824144dbf5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/ShowcaseItem.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.components.showcase + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun ShowcaseItem(@DrawableRes iconRes: Int, text: TextReference) { + Row { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + Text( + text = text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing20), + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt new file mode 100644 index 0000000000..602465c864 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseButtonModel.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.components.showcase.model + +import com.tangem.core.ui.extensions.TextReference + +data class ShowcaseButtonModel( + val buttonText: TextReference, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt new file mode 100644 index 0000000000..f9865aab9e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/showcase/model/ShowcaseItemModel.kt @@ -0,0 +1,9 @@ +package com.tangem.core.ui.components.showcase.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference + +data class ShowcaseItemModel( + @DrawableRes val iconRes: Int, + val text: TextReference, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt index 35cc8f6903..eea8a09bae 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/snackbar/CopiedTextSnackbar.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview /** * Snackbar to inform the user about copying text to the clipboard @@ -100,7 +101,7 @@ private fun MessageText(text: TextReference, modifier: Modifier = Modifier) { private fun Preview_CopiedTextSnackbar( @PreviewParameter(CopiedTextSnackbarDataProvider::class) message: TextReference, ) { - TangemTheme(isDark = false) { + TangemThemePreview { CopiedTextSnackbar(message = message) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt new file mode 100644 index 0000000000..110080210d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/TooltipText.kt @@ -0,0 +1,91 @@ +package com.tangem.core.ui.components.text + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +fun TooltipText( + text: TextReference, + onInfoClick: () -> Unit, + modifier: Modifier = Modifier, + useSmallerText: Boolean = false, +) { + val interactionSource = remember { MutableInteractionSource() } + + Row( + modifier = modifier + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = onInfoClick, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceAround, + ) { + Text( + modifier = Modifier.weight(1f, fill = false), + text = text.resolveReference(), + style = if (useSmallerText) { + TangemTheme.typography.caption2 + } else { + TangemTheme.typography.subtitle2 + }, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + IconButton( + modifier = Modifier.requiredSize(TangemTheme.dimens.size24), + interactionSource = interactionSource, + onClick = onInfoClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_information_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_TooltipText() { + TangemThemePreview { + Box( + modifier = Modifier + .width(width = 90.dp) + .background(color = TangemTheme.colors.background.primary), + ) { + TooltipText( + text = stringReference("Text"), + onInfoClick = { /* [REDACTED_TODO_COMMENT]*/ }, + ) + } + } +} +// endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt index e43769432c..3b99a455ce 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/Transaction.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension -import com.tangem.common.Strings import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer @@ -35,6 +34,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.utils.Strings import java.util.UUID /** diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt new file mode 100644 index 0000000000..4f8144398d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableContentComponent.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui.decompose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier + +@Stable +fun interface ComposableContentComponent { + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + fun Content(modifier: Modifier) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt new file mode 100644 index 0000000000..a827872ee3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableDialogComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.core.ui.decompose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable + +@Stable +interface ComposableDialogComponent { + + val doOnDismiss: () -> Unit + + @Composable + @Suppress("TopLevelComposableFunctions") // TODO: Remove this check + fun Dialog() +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt index 408e9130ee..b8b11c058f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt @@ -53,4 +53,6 @@ fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode): } } return this -} \ No newline at end of file +} + +fun AnnotatedString.Builder.appendSpace() = append(" ") \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index 2f22994f64..386f69b66b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import org.intellij.markdown.MarkdownElementTypes +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract /** * Utility class for creating text as [String] or [StringRes]. @@ -46,6 +48,13 @@ sealed interface TextReference { */ data class Str(val value: String) : TextReference + /** + * Annotated text + * + * @property value annotated string + */ + data class Annotated(val value: AnnotatedString) : TextReference + /** * Combined reference. It concatenates all [refs]. * @@ -81,6 +90,16 @@ fun stringReference(value: String): TextReference { return TextReference.Str(value) } +/** + * Creates a [TextReference] using an annotated string value. + * + * @param value The annotated string value. + * @return A [TextReference] representing the provided annotated string value. + */ +fun annotatedReference(value: AnnotatedString): TextReference { + return TextReference.Annotated(value) +} + /** * Creates a [TextReference] using a plural string resource ID with count and optional format arguments. * @@ -131,6 +150,7 @@ fun TextReference.resolveReference(): String { } is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray()) is TextReference.Str -> value + is TextReference.Annotated -> value.text is TextReference.Combined -> { buildString { refs.forEach { @@ -153,6 +173,7 @@ fun TextReference.resolveReference(resources: Resources): String { } is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray()) is TextReference.Str -> value + is TextReference.Annotated -> value.text is TextReference.Combined -> { buildString { refs.forEach { @@ -178,9 +199,10 @@ fun TextReference.resolveAnnotatedReference(): AnnotatedString { pluralStringResource(id, count, *formatArgs.toTypedArray()), ) is TextReference.Str -> formatAnnotated(value) + is TextReference.Annotated -> value is TextReference.Combined -> buildAnnotatedString { refs.forEach { - append(formatAnnotated(it.resolveReference())) + append(it.resolveAnnotatedReference()) } } } @@ -193,10 +215,21 @@ operator fun TextReference.plus(ref: TextReference): TextReference { is TextReference.PluralRes, is TextReference.Res, is TextReference.Str, + is TextReference.Annotated, -> TextReference.Combined(refs = wrappedList(this, ref)) } } +@Suppress("NOTHING_TO_INLINE") +@OptIn(ExperimentalContracts::class) +inline fun TextReference?.isNullOrEmpty(): Boolean { + contract { + returns(false) implies (this@isNullOrEmpty != null) + } + + return this == null || this == TextReference.EMPTY +} + @Composable private fun formatAnnotated(rawString: String): AnnotatedString { val markdownDescriptor = rememberMarkdownParser() diff --git a/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt index 876ee7a05d..ad74ee1bd1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/haptic/MockHapticManager.kt @@ -1,16 +1,30 @@ package com.tangem.core.ui.haptic -object MockHapticManager : HapticManager { +import androidx.compose.ui.hapticfeedback.HapticFeedback +import androidx.compose.ui.hapticfeedback.HapticFeedbackType + +@Suppress("FunctionName") +fun MockHapticManager(mockHapticFeedback: HapticFeedback? = null): HapticManager = + if (mockHapticFeedback == null) MockHapticManager else MockHapticManagerImpl(mockHapticFeedback) + +val MockHapticManager: HapticManager = MockHapticManagerImpl() + +private class MockHapticManagerImpl( + private val mockHapticFeedback: HapticFeedback? = null, +) : HapticManager { override fun vibrateShort() { - /** Intentionnaly do nothing */ + mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove) + /** Intentionally do nothing */ } override fun vibrateMeduim() { - /** Intentionnaly do nothing */ + mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.TextHandleMove) + /** Intentionally do nothing */ } override fun vibrateLong() { - /** Intentionnaly do nothing */ + mockHapticFeedback?.performHapticFeedback(HapticFeedbackType.LongPress) + /** Intentionally do nothing */ } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt index 0f4d117dab..782cd2a84f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/EventMessageHandler.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.message +import androidx.compose.runtime.Stable import com.tangem.core.decompose.ui.UiMessage import com.tangem.core.decompose.ui.UiMessageHandler import com.tangem.core.ui.event.StateEvent @@ -11,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow /** * Message handler that is used to show or remove an [EventMessage] in the UI. */ +@Stable class EventMessageHandler( private val events: MutableStateFlow> = MutableStateFlow(consumedEvent()), ) : UiMessageHandler, StateFlow> by events { diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 55321fa0b2..4d47c4bf0f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -45,8 +45,10 @@ data class TangemDimens internal constructor( val size2: Dp = 2.dp, val size4: Dp = 4.dp, val size5: Dp = 5.dp, + val size6: Dp = 6.dp, val size7: Dp = 7.dp, val size8: Dp = 8.dp, + val size9: Dp = 9.dp, val size10: Dp = 10.dp, val size11: Dp = 11.dp, val size12: Dp = 12.dp, @@ -55,6 +57,7 @@ data class TangemDimens internal constructor( val size16: Dp = 16.dp, val size18: Dp = 18.dp, val size20: Dp = 20.dp, + val size22: Dp = 22.dp, val size24: Dp = 24.dp, val size28: Dp = 28.dp, val size30: Dp = 30.dp, @@ -81,8 +84,10 @@ data class TangemDimens internal constructor( val size90: Dp = 90.dp, val size93: Dp = 93.dp, val size96: Dp = 96.dp, + val size100: Dp = 100.dp, val size102: Dp = 102.dp, val size108: Dp = 108.dp, + val size110: Dp = 110.dp, val size116: Dp = 116.dp, val size120: Dp = 120.dp, val size142: Dp = 142.dp, @@ -99,9 +104,11 @@ data class TangemDimens internal constructor( val spacing2: Dp = 2.dp, val spacing3: Dp = 3.dp, val spacing4: Dp = 4.dp, + val spacing5: Dp = 5.dp, val spacing6: Dp = 6.dp, val spacing8: Dp = 8.dp, val spacing10: Dp = 10.dp, + val spacing11: Dp = 11.dp, val spacing12: Dp = 12.dp, val spacing14: Dp = 14.dp, val spacing15: Dp = 15.dp, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt index 8bf99452ed..5da8defa94 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemShapes.kt @@ -12,6 +12,7 @@ data class TangemShapes internal constructor( val roundedCornersMedium: Shape, val roundedCornersXMedium: Shape, val roundedCornersLarge: Shape, + val roundedCornersXLarge: Shape, val bottomSheet: Shape, val bottomSheetLarge: Shape, ) { @@ -22,6 +23,7 @@ data class TangemShapes internal constructor( roundedCornersMedium = RoundedCornerShape(size = dimens.radius12), roundedCornersXMedium = RoundedCornerShape(size = dimens.radius16), roundedCornersLarge = RoundedCornerShape(size = dimens.radius28), + roundedCornersXLarge = RoundedCornerShape(size = dimens.radius36), bottomSheet = RoundedCornerShape( topStart = dimens.radius16, topEnd = dimens.radius16, diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index 4f3287a86d..ea874c3bfb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -3,19 +3,22 @@ package com.tangem.core.ui.res import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.tangem.core.ui.haptic.HapticManager import com.tangem.core.ui.haptic.MockHapticManager - -// TODO: use isSystemInDarkTheme() for automatic color detection -internal const val IS_SYSTEM_IN_DARK_THEME: Boolean = false +import com.tangem.core.ui.windowsize.WindowSize @Composable fun TangemTheme( isDark: Boolean = false, + windowSize: WindowSize, typography: TangemTypography = TangemTheme.typography, dimens: TangemDimens = TangemTheme.dimens, hapticManager: HapticManager = MockHapticManager, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, content: @Composable () -> Unit, ) { val themeColors = if (isDark) darkThemeColors() else lightThemeColors() @@ -23,6 +26,16 @@ fun TangemTheme( .also { it.update(themeColors) } val shapes = remember { TangemShapes(dimens) } + val systemUiController = rememberSystemUiController() + + SideEffect { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = !isDark, + isNavigationBarContrastEnforced = false, + ) + } + MaterialTheme( colors = materialThemeColors(colors = themeColors, isDark = isDark), ) { @@ -33,6 +46,8 @@ fun TangemTheme( LocalTangemShapes provides shapes, LocalIsInDarkTheme provides isDark, LocalHapticManager provides hapticManager, + LocalSnackbarHostState provides snackbarHostState, + LocalWindowSize provides windowSize, ) { ProvideTextStyle( value = TangemTheme.typography.body1, @@ -124,7 +139,7 @@ private fun lightThemeColors(): TangemColors { ), stroke = TangemColors.Stroke( primary = TangemColorPalette.Light2, - secondary = TangemColorPalette.Dark5, + secondary = TangemColorPalette.Light5, transparency = TangemColorPalette.White, ), field = TangemColors.Field( @@ -204,4 +219,12 @@ val LocalIsInDarkTheme = staticCompositionLocalOf { false } val LocalHapticManager = staticCompositionLocalOf { error("No HapticManager provided") +} + +val LocalSnackbarHostState = staticCompositionLocalOf { + error("No SnackbarHostState provided") +} + +val LocalWindowSize = staticCompositionLocalOf { + error("No WindowSize provided") } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt index 2a9e0caf65..2135bbdff8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemePreview.kt @@ -1,7 +1,11 @@ package com.tangem.core.ui.res import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalHapticFeedback +import com.tangem.core.ui.haptic.MockHapticManager +import com.tangem.core.ui.windowsize.rememberWindowSizePreview @Composable fun TangemThemePreview( @@ -12,10 +16,14 @@ fun TangemThemePreview( ) { val isDarkTheme = isDark ?: isSystemInDarkTheme() - TangemTheme( - isDark = isDarkTheme, - typography = typography, - dimens = dimens, - content = content, - ) + BoxWithConstraints { + TangemTheme( + isDark = isDarkTheme, + typography = typography, + dimens = dimens, + windowSize = rememberWindowSizePreview(maxWidth, maxHeight), + hapticManager = MockHapticManager(LocalHapticFeedback.current), + content = content, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt index fe4f115dcd..9d4acd5fc0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeActivity.kt @@ -12,6 +12,6 @@ abstract class ComposeActivity : ComponentActivity(), ComposeScreen { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(createComposeView(context = this)) + setContentView(createComposeView(context = this, activity = this)) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt index b437a39cd5..75dc4e8a98 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeBottomSheetFragment.kt @@ -52,7 +52,7 @@ abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), Compose override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return createComposeView(inflater.context) + return createComposeView(inflater.context, requireActivity()) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt index c60bf1ba7b..48565c82d7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeFragment.kt @@ -18,7 +18,7 @@ abstract class ComposeFragment : Fragment(), ComposeScreen { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions() - return createComposeView(inflater.context).also { + return createComposeView(inflater.context, requireActivity()).also { it.isTransitionGroup = isTransitionsInflated } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index 4c2faae280..d6b1869ec5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.screen +import android.app.Activity import android.content.Context import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize @@ -10,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.windowsize.rememberWindowSize import com.tangem.domain.apptheme.model.AppThemeMode /** @@ -50,14 +52,17 @@ internal interface ComposeScreen { * @param context The context. * @return A [ComposeView] instance with the defined screen content. */ -internal fun ComposeScreen.createComposeView(context: Context): ComposeView { +internal fun ComposeScreen.createComposeView(context: Context, activity: Activity): ComposeView { return ComposeView(context).apply { setContent { val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode + val windowSize = rememberWindowSize(activity = activity) TangemTheme( isDark = shouldUseDarkTheme(appThemeMode), + windowSize = windowSize, hapticManager = uiDependencies.hapticManager, + snackbarHostState = uiDependencies.globalSnackbarHostState, ) { ScreenContent(modifier = screenModifier) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt new file mode 100644 index 0000000000..105d2416d3 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DensityUtils.kt @@ -0,0 +1,18 @@ +package com.tangem.core.ui.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +@Stable +@Composable +fun Dp.toPx(): Float = toPx(density = LocalDensity.current.density) + +@Stable +@Composable +fun convertPxToDp(px: Float): Dp = convertPxToDp(px, density = LocalDensity.current.density) + +fun Dp.toPx(density: Float): Float = this.value * density + +fun convertPxToDp(px: Float, density: Float): Dp = Dp(value = px / density) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/PathUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/PathUtils.kt new file mode 100644 index 0000000000..8ab1fe470b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/PathUtils.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.utils + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Path + +fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y) + +fun Path.moveTo(offset: Offset) = moveTo(offset.x, offset.y) + +fun Path.quadraticBezierTo(control: Offset, end: Offset) = quadraticBezierTo(control.x, control.y, end.x, end.y) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt new file mode 100644 index 0000000000..575485aae9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/RequestPushPermission.kt @@ -0,0 +1,52 @@ +package com.tangem.core.ui.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale + +/** + * Returns push permission requester. + * Handles granting permission from app settings. + */ +@Suppress("LongParameterList") +@OptIn(ExperimentalPermissionsApi::class) +@Composable +fun requestPushPermission( + isFirstTimeAsking: Boolean, + pushPermission: String?, + isClicked: MutableState, + onAllow: () -> Unit, + onDeny: () -> Unit, + onOpenSettings: () -> Unit, +): () -> Unit { + val permissionState = pushPermission?.let { permission -> + val tempPermissionState = rememberPermissionState(permission = permission) + rememberPermissionState(permission = permission) { + when { + it -> onAllow() + !tempPermissionState.status.shouldShowRationale && !isFirstTimeAsking -> onOpenSettings() + else -> onDeny() + } + } + } + + // Check if user granted permission and close bottom sheet + LaunchedEffect(key1 = permissionState?.status, isClicked) { + if (!isClicked.value) return@LaunchedEffect + if (permissionState?.status?.isGranted == true) { + onAllow() + } else { + onDeny() + } + } + + return if (permissionState == null) { + onOpenSettings + } else { + permissionState::launchPermissionRequest + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/WindowInsetsZero.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/WindowInsetsZero.kt new file mode 100644 index 0000000000..a17ac5f1ef --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/WindowInsetsZero.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.utils + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection + +object WindowInsetsZero : WindowInsets { + override fun getBottom(density: Density): Int = 0 + override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0 + override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0 + override fun getTop(density: Density): Int = 0 +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt b/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt new file mode 100644 index 0000000000..ed47c74673 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/windowsize/WindowSize.kt @@ -0,0 +1,99 @@ +package com.tangem.core.ui.windowsize + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.toComposeRect +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.window.layout.WindowMetricsCalculator + +/** + * The preferred way to determine the window size is because it provides the actual size of the window as opposed to + * [LocalConfiguration.current.screenHeightDp] + * + * @property width actual window width + * @property height actual window height + * @property widthSizeType window size type based on width + * @property heightSizeType window size type based on height + * @property smallestSize smallest size of width and height + */ +data class WindowSize( + val width: Dp, + val height: Dp, +) { + val widthSizeType: WindowSizeType = getWidthWindowSizeType(width) + val heightSizeType: WindowSizeType = getHeightWindowSizeType(height) + val smallestSize: Dp = minOf(width, height) + + fun widthAtLeast(type: WindowSizeType): Boolean = this.widthSizeType.ordinal >= type.ordinal + + fun widthAtLeast(value: Dp): Boolean = this.width >= value + + fun heightAtLeast(type: WindowSizeType): Boolean = this.heightSizeType.ordinal >= type.ordinal + + fun heightAtLeast(value: Dp): Boolean = this.height >= value + + fun widthAtMost(type: WindowSizeType): Boolean = this.widthSizeType.ordinal <= type.ordinal + + fun widthAtMost(value: Dp): Boolean = this.width <= value + + fun heightAtMost(type: WindowSizeType): Boolean = this.heightSizeType.ordinal <= type.ordinal + + fun heightAtMost(value: Dp): Boolean = this.height <= value + + private fun getWidthWindowSizeType(windowDp: Dp): WindowSizeType = when { + windowDp <= 320.dp -> WindowSizeType.ExtraSmall + windowDp <= 360.dp -> WindowSizeType.Small + windowDp <= 540.dp -> WindowSizeType.Normal + windowDp <= 700.dp -> WindowSizeType.Large + else -> WindowSizeType.ExtraLarge + } + + private fun getHeightWindowSizeType(heightDp: Dp): WindowSizeType = when { + heightDp <= 480.dp -> WindowSizeType.ExtraSmall + heightDp <= 640.dp -> WindowSizeType.Small + heightDp <= 860.dp -> WindowSizeType.Normal + heightDp <= 1100.dp -> WindowSizeType.Large + else -> WindowSizeType.ExtraLarge + } +} + +enum class WindowSizeType { + ExtraSmall, Small, Normal, Large, ExtraLarge +} + +@Composable +fun rememberWindowSize(activity: Activity): WindowSize { + val windowBoundsSize = rememberWindowBoundsSize(activity) + val windowDpSize = with(LocalDensity.current) { + windowBoundsSize.toDpSize() + } + + return WindowSize( + width = windowDpSize.width, + height = windowDpSize.height, + ) +} + +@Composable +internal fun rememberWindowSizePreview(width: Dp, height: Dp): WindowSize { + return remember(width, height) { + WindowSize( + width = width, + height = height, + ) + } +} + +@Composable +private fun rememberWindowBoundsSize(activity: Activity): Size { + val configuration = LocalConfiguration.current + val windowMetrics = remember(configuration) { + WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity) + } + return windowMetrics.bounds.toComposeRect().size +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_friends.xml b/core/ui/src/main/res/drawable/ic_add_friends_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_add_friends.xml rename to core/ui/src/main/res/drawable/ic_add_friends_24.xml diff --git a/core/ui/src/main/res/drawable/ic_card_foget_24.xml b/core/ui/src/main/res/drawable/ic_card_foget_24.xml new file mode 100644 index 0000000000..77d2117ba5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_card_foget_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_card_settings.xml b/core/ui/src/main/res/drawable/ic_card_settings_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_card_settings.xml rename to core/ui/src/main/res/drawable/ic_card_settings_24.xml diff --git a/core/ui/src/main/res/drawable/ic_check_circle_24.xml b/core/ui/src/main/res/drawable/ic_check_circle_24.xml new file mode 100644 index 0000000000..21b193accf --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_check_circle_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_infinity_24.xml b/core/ui/src/main/res/drawable/ic_infinity_24.xml new file mode 100644 index 0000000000..cf0d1b2c54 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_infinity_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml b/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml new file mode 100644 index 0000000000..c50e451c69 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_notifications_unread_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_plus_mini_28.xml b/core/ui/src/main/res/drawable/ic_plus_mini_28.xml new file mode 100644 index 0000000000..e6e483128c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_plus_mini_28.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml b/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml new file mode 100644 index 0000000000..a7b572f5f0 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_rocket_launch_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_storefront_24.xml b/core/ui/src/main/res/drawable/ic_storefront_24.xml new file mode 100644 index 0000000000..7f17e6c496 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_storefront_24.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/core/utils/src/main/java/com/tangem/utils/Strings.kt b/core/utils/src/main/java/com/tangem/utils/Strings.kt new file mode 100644 index 0000000000..b40470e852 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/Strings.kt @@ -0,0 +1,8 @@ +package com.tangem.utils + +object Strings { + + const val STARS = "\u2217\u2217\u2217" + const val DOT = "•" + const val PLUS = "+" +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt b/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt new file mode 100644 index 0000000000..d09f3f6a6d --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt @@ -0,0 +1,10 @@ +package com.tangem.utils.extensions + +import java.math.BigDecimal + +/** + * Converts `BigDecimal?` to `BigDecimal` + * + * If `BigDecimal?` is `null`, returns `BigDecimal.ZERO` + */ +fun BigDecimal?.orZero(): BigDecimal = this ?: BigDecimal.ZERO \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt new file mode 100644 index 0000000000..9c271d0920 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/transformer/Transformer.kt @@ -0,0 +1,8 @@ +package com.tangem.utils.transformer + +/** + * Transforms state to updated state. + */ +interface Transformer { + fun transform(prevState: S): S +} \ No newline at end of file diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts index 4847e15ddd..e936310c64 100644 --- a/data/feedback/build.gradle.kts +++ b/data/feedback/build.gradle.kts @@ -37,7 +37,9 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.legacy) - implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + + implementation(projects.libs.blockchainSdk) } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 4bf2f7eb2a..b5313ac608 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -9,12 +9,14 @@ import com.tangem.data.feedback.converters.CardInfoConverter import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMap -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import timber.log.Timber @@ -25,43 +27,47 @@ import java.io.StringWriter /** * Implementation of [FeedbackRepository] * - * @property appPreferencesStore application preferences store - * @property userWalletsStore user wallets store - * @property walletManagersStore wallet managers store - * @property context context for getting app version + * @property appPreferencesStore application preferences store + * @property userWalletsListManager user wallets list manager + * @property walletManagersStore wallet managers store + * @property context context for getting app version + * @property dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class DefaultFeedbackRepository( private val appPreferencesStore: AppPreferencesStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListManager: UserWalletsListManager, private val walletManagersStore: WalletManagersStore, private val context: Context, + private val dispatchers: CoroutineDispatcherProvider, ) : FeedbackRepository { private val blockchainsErrors = MutableStateFlow>(emptyMap()) - override suspend fun getUserWalletsInfo(): UserWalletsInfo { + override suspend fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse) + + override suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo { return UserWalletsInfo( - selectedUserWalletId = getSelectedUserWallet().walletId.stringValue, - totalUserWallets = userWalletsStore.getAllSyncOrNull()?.size ?: error("No user wallets found"), + selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated", + totalUserWallets = userWalletsListManager.walletsCount, ) } - override suspend fun getCardInfo(): CardInfo { - return CardInfoConverter.convert(value = getSelectedUserWallet()) - } - - override suspend fun getBlockchainInfoList(): List { + override suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List { return walletManagersStore - .getAllSync(userWalletId = getSelectedUserWallet().walletId) + .getAllSync(userWalletId = userWalletId) .map(BlockchainInfoConverter::convert) } - override suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? { + override suspend fun getBlockchainInfo( + userWalletId: UserWalletId, + blockchainId: String, + derivationPath: String?, + ): BlockchainInfo? { return walletManagersStore .getSyncOrNull( - userWalletId = getSelectedUserWallet().walletId, + userWalletId = userWalletId, blockchain = Blockchain.fromId(blockchainId), derivationPath = derivationPath, ) @@ -77,18 +83,18 @@ internal class DefaultFeedbackRepository( } override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) { + val userWallet = userWalletsListManager.selectedUserWalletSync ?: error("UserWallet is not selected") + blockchainsErrors.update { it.toMutableMap().apply { - put(getSelectedUserWallet().walletId, error) + put(userWallet.walletId, error) } } } - override suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? { - return blockchainsErrors.value[getSelectedUserWallet().walletId].also { - if (it == null) { - Timber.e("Blockchain error info is null for ${getSelectedUserWallet().walletId}") - } + override suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? { + return blockchainsErrors.value[userWalletId].also { + if (it == null) Timber.e("Blockchain error info is null for $userWalletId") } } @@ -99,7 +105,7 @@ internal class DefaultFeedbackRepository( } override suspend fun createLogFile(logs: String): File? { - return try { + return runCatching(dispatchers.io) { val file = File(context.filesDir, LOGS_FILE) file.delete() file.createNewFile() @@ -113,8 +119,8 @@ internal class DefaultFeedbackRepository( fileWriter.close() file - } catch (ex: Exception) { - Timber.e(ex, "Logs file isn't created") + }.getOrElse { + Timber.e(it, "Logs file isn't created") null } } @@ -130,11 +136,6 @@ internal class DefaultFeedbackRepository( ) } - private fun getSelectedUserWallet(): UserWallet { - return userWalletsStore.selectedUserWalletOrNull - ?: error("UserWallet is not selected") - } - private companion object { const val LOGS_FILE = "logs.txt" } diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt index 7d16454467..c4aafd2bf2 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/CardInfoConverter.kt @@ -2,28 +2,36 @@ package com.tangem.data.feedback.converters import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.feedback.models.CardInfo -import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.converter.Converter /** - * Converter from [UserWallet] to [CardInfo] + * Converter from [ScanResponse] to [CardInfo] * [REDACTED_AUTHOR] */ -internal object CardInfoConverter : Converter { +internal object CardInfoConverter : Converter { - override fun convert(value: UserWallet): CardInfo { - return with(value.scanResponse) { + override fun convert(value: ScanResponse): CardInfo { + return with(value) { CardInfo( + userWalletId = createUserWalletId(scanResponse = value), cardId = card.cardId, firmwareVersion = card.firmwareVersion.stringValue, cardBlockchain = walletData?.blockchain, signedHashesList = card.wallets.map { CardInfo.SignedHashes(curve = it.curve.curve, total = it.totalSignedHashes?.toString()) }, - isImported = value.isImported, - isStart2Coin = value.scanResponse.card.isStart2Coin, + isImported = value.card.wallets.any(CardDTO.Wallet::isImported), + isStart2Coin = value.card.isStart2Coin, ) } } + + private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId? { + return UserWalletIdBuilder.scanResponse(scanResponse = scanResponse).build() + } } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt index 5a00938959..393a30cf25 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackRepositoryModule.kt @@ -3,9 +3,10 @@ package com.tangem.data.feedback.di import android.content.Context import com.tangem.data.feedback.DefaultFeedbackRepository import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -21,10 +22,17 @@ internal object FeedbackRepositoryModule { @Singleton fun provideFeedbackRepository( appPreferencesStore: AppPreferencesStore, - userWalletsStore: UserWalletsStore, + userWalletsListManager: UserWalletsListManager, walletManagersStore: WalletManagersStore, @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, ): FeedbackRepository { - return DefaultFeedbackRepository(appPreferencesStore, userWalletsStore, walletManagersStore, context) + return DefaultFeedbackRepository( + appPreferencesStore = appPreferencesStore, + userWalletsListManager = userWalletsListManager, + walletManagersStore = walletManagersStore, + context = context, + dispatchers = dispatchers, + ) } } \ No newline at end of file diff --git a/data/markets/.gitignore b/data/markets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/markets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts new file mode 100644 index 0000000000..8e9347ad54 --- /dev/null +++ b/data/markets/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.data.markets" +} + +dependencies { + implementation(projects.core.datasource) + implementation(projects.core.utils) + implementation(projects.core.pagination) + implementation(projects.domain.tokens.models) + implementation(projects.domain.markets) + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Others dependencies + implementation(deps.kotlin.coroutines) + implementation(deps.moshi) + implementation(deps.moshi.kotlin) + + implementation(projects.libs.blockchainSdk) + // endregion +} diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt new file mode 100644 index 0000000000..65ebc717bb --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -0,0 +1,121 @@ +package com.tangem.data.markets + +import com.tangem.data.markets.converters.* +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.markets.* +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.pagination.* +import com.tangem.pagination.fetcher.LimitOffsetBatchFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultMarketsTokenRepository( + private val marketsApi: TangemTechMarketsApi, + private val tangemTechApi: TangemTechApi, + private val dispatcherProvider: CoroutineDispatcherProvider, +) : MarketsTokenRepository { + + private val tokenListConverter = TokenMarketListConverter() + private val tokenListChartsConverter = TokenMarketChartsConverter(TokenListChartConverter()) + private val tokenQuotesConverter = TokenQuotesConverter() + + private val tokenMarketsFetcher + get() = LimitOffsetBatchFetcher>( + prefetchDistance = 50, + batchSize = 30, + fetch = { params -> + withContext(dispatcherProvider.io) { + val res = marketsApi.getCoinsList( + currency = params.request.fiatPriceCurrency, + interval = params.request.priceChangeInterval.toRequestParam(), + order = params.request.priceChangeInterval.toRequestParam(), + search = params.request.searchText, + generalCoins = params.request.showUnder100kMarketCapTokens.not(), + offset = params.offset, + limit = params.limit, + ).getOrThrow() + + val last = res.tokens.size < params.limit + + BatchFetchResult.Success( + data = tokenListConverter.convert(res), + last = last, + ) + } + }, + ) + + private val tokenMarketsUpdateFetcher + get() = BatchUpdateFetcher, TokenMarketUpdateRequest> { toUpdate, updateRequest -> + withContext(dispatcherProvider.io) { + val idsToUpdate = toUpdate.map { batch -> + batch.data.map { it.id } + }.flatten() + + val updatedBatches = when (updateRequest) { + is TokenMarketUpdateRequest.UpdateChart -> { + val res = marketsApi.getCoinsListCharts( + coinIds = idsToUpdate, + interval = updateRequest.interval.toRequestParam(), + currency = updateRequest.currency, + ).getOrThrow() + + toUpdate.map { batch -> + batch.copy( + data = batch.data.map { + it.copy( + tokenCharts = tokenListChartsConverter.convert( + chartsToCopy = it.tokenCharts, + tokenId = it.id, + interval = updateRequest.interval, + value = res, + ), + ) + }, + ) + } + } + is TokenMarketUpdateRequest.UpdateQuotes -> { + val quotesRes = tangemTechApi.getQuotes( + currencyId = updateRequest.currencyId, + coinIds = idsToUpdate.joinToString(separator = ","), + fields = quoteFields.joinToString(separator = ","), + ).getOrThrow() + + toUpdate.map { batch -> + batch.copy( + data = batch.data.map { + it.copy(tokenQuotes = tokenQuotesConverter.convert(it.id, quotesRes)) + }, + ) + } + } + } + + BatchUpdateResult.Success(updatedBatches) + } + } + + override suspend fun getTokenListFlow( + batchingContext: BatchingContext, + ): BatchFlow, TokenMarketUpdateRequest> { + return BatchListSource( + fetchDispatcher = dispatcherProvider.io, + context = batchingContext, + generateNewKey = { it.size }, + batchFetcher = tokenMarketsFetcher, + updateFetcher = tokenMarketsUpdateFetcher, + ).toBatchFlow() + } + + companion object { + private val quoteFields = listOf( + "price", + "priceChange24h", + "priceChange1w", + "priceChange30d", + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt new file mode 100644 index 0000000000..0b4cf678c6 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListChartConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.markets.models.response.TokenMarketChartResponse +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenChart + +class TokenListChartConverter { + + fun convert(interval: PriceChangeInterval, value: TokenMarketChartResponse): TokenChart { + return TokenChart( + interval = interval, + priceY = value.prices.values.toList(), + timeStamp = value.prices.keys.toList(), + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt new file mode 100644 index 0000000000..eee63ef4e9 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenListConfigConverters.kt @@ -0,0 +1,28 @@ +package com.tangem.data.markets.converters + +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenMarketListConfig + +fun TokenMarketListConfig.Interval.toRequestParam(): String = when (this) { + TokenMarketListConfig.Interval.H24 -> "24h" + TokenMarketListConfig.Interval.WEEK -> "1w" + TokenMarketListConfig.Interval.MONTH -> "30d" +} + +fun TokenMarketListConfig.Order.toRequestParam(): String = when (this) { + TokenMarketListConfig.Order.ByRating -> "rating" + TokenMarketListConfig.Order.Trending -> "trending" + TokenMarketListConfig.Order.Buyers -> "buyers" + TokenMarketListConfig.Order.TopGainers -> "gainers" + TokenMarketListConfig.Order.TopLosers -> "losers" +} + +fun PriceChangeInterval.toRequestParam(): String = when (this) { + PriceChangeInterval.H24 -> "24h" + PriceChangeInterval.WEEK -> "1w" + PriceChangeInterval.MONTH -> "1m" + PriceChangeInterval.MONTH3 -> "3m" + PriceChangeInterval.MONTH6 -> "6m" + PriceChangeInterval.YEAR -> "1y" + PriceChangeInterval.ALL_TIME -> "all" +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt new file mode 100644 index 0000000000..aade2d2073 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketChartsConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenMarket + +class TokenMarketChartsConverter( + private val tokenListChartConverter: TokenListChartConverter, +) { + + fun convert( + chartsToCopy: TokenMarket.Charts, + tokenId: String, + interval: PriceChangeInterval, + value: TokenMarketChartListResponse, + ): TokenMarket.Charts { + val prices = requireNotNull(value.tokens[tokenId]) { + "$tokenId is not found in the response. This shouldn't have happened." + } + return when (interval) { + PriceChangeInterval.H24 -> chartsToCopy.copy( + h24 = tokenListChartConverter.convert(interval, prices), + ) + PriceChangeInterval.WEEK -> chartsToCopy.copy( + week = tokenListChartConverter.convert(interval, prices), + ) + PriceChangeInterval.MONTH -> chartsToCopy.copy( + month = tokenListChartConverter.convert(interval, prices), + ) + else -> error("unsupported interval=$interval. This shouldn't have happened.") + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt new file mode 100644 index 0000000000..ea8587e123 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenQuotes +import com.tangem.utils.converter.Converter + +class TokenMarketListConverter : Converter> { + + override fun convert(value: TokenMarketListResponse): List { + return value.tokens.map { token -> + TokenMarket( + id = token.id, + name = token.name, + symbol = token.symbol, + marketRating = token.marketRating, + marketCap = token.marketCap, + imageHost = value.imageHost, + tokenQuotes = TokenQuotes( + currentPrice = token.currentPrice, + priceChanges = mapOf( + PriceChangeInterval.H24 to token.priceChangePercentage.h24, + PriceChangeInterval.WEEK to token.priceChangePercentage.week1, + PriceChangeInterval.MONTH to token.priceChangePercentage.day30, + ), + ), + tokenCharts = TokenMarket.Charts(null, null, null), + ) + } + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt new file mode 100644 index 0000000000..a2b61460d0 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenQuotesConverter.kt @@ -0,0 +1,30 @@ +package com.tangem.data.markets.converters + +import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.domain.markets.TokenQuotes + +class TokenQuotesConverter { + + fun convert(tokenId: String, value: QuotesResponse): TokenQuotes { + val quote = requireNotNull(value.quotes[tokenId]) { + "$tokenId is not found in the response. This shouldn't have happened." + } + return TokenQuotes( + currentPrice = requireNotNull(quote.price) { + "Price is not found in the QuotesResponse. This shouldn't have happened." + }, + priceChanges = mapOf( + PriceChangeInterval.H24 to requireNotNull(quote.priceChange1w) { + "priceChange1w is not found in the QuotesResponse. This shouldn't have happened." + }, + PriceChangeInterval.WEEK to requireNotNull(quote.priceChange1w) { + "priceChange1w is not found in the QuotesResponse. This shouldn't have happened." + }, + PriceChangeInterval.MONTH to requireNotNull(quote.priceChange30d) { + "priceChange30d is not found in the QuotesResponse. This shouldn't have happened." + }, + ), + ) + } +} \ No newline at end of file diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt new file mode 100644 index 0000000000..be8a925029 --- /dev/null +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -0,0 +1,31 @@ +package com.tangem.data.markets.di + +import com.tangem.data.markets.DefaultMarketsTokenRepository +import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.domain.markets.repositories.MarketsTokenRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object MarketsDataModule { + + @Provides + @Singleton + fun provideMarketsRepository( + marketsApi: TangemTechMarketsApi, + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + ): MarketsTokenRepository { + return DefaultMarketsTokenRepository( + marketsApi = marketsApi, + tangemTechApi = tangemTechApi, + dispatcherProvider = dispatchers, + ) + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt new file mode 100644 index 0000000000..b73d5d069f --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultPermissionRepository.kt @@ -0,0 +1,69 @@ +package com.tangem.data.settings + +import android.os.SystemClock +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.PreferencesKeys.getIsFirstTimeAskingPermission +import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionDaysCount +import com.tangem.datasource.local.preferences.PreferencesKeys.getPermissionLaunchCount +import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowInitialPermissionScreen +import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowPermission +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.settings.repositories.PermissionRepository + +internal class DefaultPermissionRepository( + private val appPreferencesStore: AppPreferencesStore, +) : PermissionRepository { + + override suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean { + val key = getShouldShowInitialPermissionScreen(permission) + val initialPermissionScreen = appPreferencesStore.getSyncOrDefault(key = key, default = true) + if (initialPermissionScreen) appPreferencesStore.store(key = key, value = false) + return initialPermissionScreen + } + + override suspend fun isFirstTimeAskingPermission(permission: String): Boolean = + appPreferencesStore.getSyncOrDefault( + key = getIsFirstTimeAskingPermission(permission), + default = true, + ) + + override suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) { + appPreferencesStore.store( + key = getIsFirstTimeAskingPermission(permission), + value = value, + ) + } + + override suspend fun shouldAskPermission(permission: String): Boolean { + val shouldAskPermission = appPreferencesStore.getSyncOrDefault(getShouldShowPermission(permission), true) + val delayedLaunches = appPreferencesStore.getSyncOrDefault(getPermissionLaunchCount(permission), 0) + val delayedDays = appPreferencesStore.getSyncOrDefault(getPermissionDaysCount(permission), 0) + val currentLaunchCounter = appPreferencesStore.getSyncOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) + + val nowMillis = SystemClock.elapsedRealtime() + val isDaysDelayed = delayedDays < nowMillis + val isLaunchesDelayed = delayedLaunches < currentLaunchCounter + return shouldAskPermission && isDaysDelayed && isLaunchesDelayed + } + + override suspend fun neverAskPermission(permission: String) { + appPreferencesStore.store(key = getShouldShowPermission(permission), value = false) + } + + override suspend fun delayPermissionAsking(permission: String) { + appPreferencesStore.editData { + val appLaunchCounter = it.getOrDefault(PreferencesKeys.APP_LAUNCH_COUNT_KEY, 0) + val nowMillis = SystemClock.elapsedRealtime() + + it[getPermissionLaunchCount(permission)] = appLaunchCounter + DELAY_LAUNCH_COUNT + it[getPermissionDaysCount(permission)] = nowMillis + DELAY_DAYS_COUNT + } + } + + private companion object { + const val DELAY_LAUNCH_COUNT = 5 + const val DELAY_DAYS_COUNT = 3L * 24 * 3600 * 1000 // 3 days in millis + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index 4f6bf8e17b..da47f1f604 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -1,12 +1,14 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository -import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.data.settings.DefaultPromoSettingsRepository +import com.tangem.data.settings.DefaultPermissionRepository +import com.tangem.data.settings.DefaultSettingsRepository import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository -import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.repositories.PromoSettingsRepository +import com.tangem.domain.settings.repositories.PermissionRepository +import com.tangem.domain.settings.repositories.SettingsRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -34,4 +36,10 @@ internal object SettingsDataModule { fun providePromoSettingsSettingsRepository(appPreferencesStore: AppPreferencesStore): PromoSettingsRepository { return DefaultPromoSettingsRepository(appPreferencesStore = appPreferencesStore) } + + @Provides + @Singleton + fun providePushPermissionRepository(appPreferencesStore: AppPreferencesStore): PermissionRepository { + return DefaultPermissionRepository(appPreferencesStore = appPreferencesStore) + } } \ No newline at end of file diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 83a8cccc87..009048e07e 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -12,11 +12,20 @@ android { dependencies { + /** Core modules */ implementation(projects.core.datasource) implementation(projects.core.utils) - implementation(projects.domain.staking) - implementation(projects.features.staking.api) + /** Common modules */ + implementation(projects.data.common) + + /** Domain modules */ + implementation(projects.domain.tokens.models) + implementation(projects.domain.staking) + implementation(projects.domain.wallets.models) + + /** Feature Api modules */ + implementation(projects.features.staking.api) // region DI implementation(deps.hilt.android) @@ -29,6 +38,7 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) + implementation(projects.libs.blockchainSdk) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index a60a8c5141..edb32a111b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -1,29 +1,118 @@ package com.tangem.data.staking +import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.staking.converters.* +import com.tangem.data.staking.converters.action.ActionStatusConverter +import com.tangem.data.staking.converters.action.EnterActionResponseConverter +import com.tangem.data.staking.converters.action.StakingActionTypeConverter +import com.tangem.data.staking.converters.transaction.GasEstimateConverter +import com.tangem.data.staking.converters.transaction.StakingTransactionConverter +import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter +import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.datasource.api.stakekit.models.request.Address +import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody +import com.tangem.datasource.api.stakekit.models.request.EnterActionRequestBody +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.token.StakingBalanceStore +import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.toFormattedString +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.math.BigDecimal +@Suppress("LargeClass") internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, - private val stakingFeatureToggles: StakingFeatureToggles, + private val stakingYieldsStore: StakingYieldsStore, + private val stakingBalanceStore: StakingBalanceStore, + private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, + private val stakingFeatureToggle: StakingFeatureToggles, ) : StakingRepository { - override fun getStakingAvailability(blockchainId: String): StakingAvailability { - if (!stakingFeatureToggles.isStakingEnabled) { - return StakingAvailability.Unavailable - } + private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() + private val networkTypeConverter = StakingNetworkTypeConverter() + private val transactionStatusConverter = StakingTransactionStatusConverter() + private val transactionTypeConverter = StakingTransactionTypeConverter() + private val actionStatusConverter = ActionStatusConverter() + private val stakingActionTypeConverter = StakingActionTypeConverter() + private val tokenConverter = TokenConverter( + stakingNetworkTypeConverter = stakingNetworkTypeConverter, + ) + private val yieldConverter = YieldConverter( + tokenConverter = tokenConverter, + ) + private val gasEstimateConverter = GasEstimateConverter( + tokenConverter = tokenConverter, + ) + private val transactionConverter = StakingTransactionConverter( + networkTypeConverter = networkTypeConverter, + transactionStatusConverter = transactionStatusConverter, + transactionTypeConverter = transactionTypeConverter, + gasEstimateConverter = gasEstimateConverter, + ) + private val enterActionResponseConverter = EnterActionResponseConverter( + actionStatusConverter = actionStatusConverter, + stakingActionTypeConverter = stakingActionTypeConverter, + transactionConverter = transactionConverter, + ) - return integrationIdMap[Blockchain.fromId(blockchainId)]?.let { - StakingAvailability.Available(it) - } ?: StakingAvailability.Unavailable + private val yieldBalanceConverter = YieldBalanceConverter() + + private val yieldBalanceListConverter = YieldBalanceListConverter() + + private val isYieldBalanceFetching = MutableStateFlow( + value = emptyMap(), + ) + + override fun isStakingSupported(currencyId: String): Boolean { + return integrationIdMap.containsKey(currencyId) + } + + override suspend fun fetchEnabledYields(refresh: Boolean) { + withContext(dispatchers.io) { + cacheRegistry.invokeOnExpire( + key = YIELDS_STORE_KEY, + skipCache = refresh, + block = { + val stakingTokensWithYields = stakeKitApi.getMultipleYields().getOrThrow() + stakingYieldsStore.store(stakingTokensWithYields.data) + }, + ) + } + } + + override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield { + return withContext(dispatchers.io) { + val yields = getEnabledYields() ?: error("No yields found") + val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: error("Staking custom tokens is not available") + + val prefetchedYield = findPrefetchedYield( + yields = yields, + currencyId = rawCurrencyId, + symbol = symbol, + ) + + prefetchedYield ?: error("Staking is unavailable") + } } override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo { @@ -38,31 +127,303 @@ internal class DefaultStakingRepository( } } - companion object { - private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" - private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" - private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" - private const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking" - private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" - private const val TRON_INTEGRATION_ID = "tron-trx-native-staking" - private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - private const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking" - private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" - private const val NEAR_INTEGRATION_ID = "near-near-native-staking" - private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + override suspend fun getStakingAvailabilityForActions( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): StakingAvailability { + val rawCurrencyId = cryptoCurrencyId.rawCurrencyId ?: return StakingAvailability.Unavailable - private val integrationIdMap = mapOf( - Blockchain.Solana to SOLANA_INTEGRATION_ID, - Blockchain.Cosmos to COSMOS_INTEGRATION_ID, - Blockchain.Polkadot to POLKADOT_INTEGRATION_ID, - Blockchain.Polygon to ETHEREUM_INTEGRATION_ID, - Blockchain.Avalanche to AVALANCHE_INTEGRATION_ID, - Blockchain.Tron to TRON_INTEGRATION_ID, - Blockchain.Cronos to CRONOS_INTEGRATION_ID, - Blockchain.Binance to BINANCE_INTEGRATION_ID, - Blockchain.Kava to KAVA_INTEGRATION_ID, - Blockchain.Near to NEAR_INTEGRATION_ID, - Blockchain.Tezos to TEZOS_INTEGRATION_ID, + return withContext(dispatchers.io) { + val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable + + val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol) + val isSupported = isStakingSupported(rawCurrencyId) + + when { + prefetchedYield != null && isSupported -> { + StakingAvailability.Available(prefetchedYield.id) + } + prefetchedYield == null && isSupported -> { + StakingAvailability.TemporaryDisabled + } + else -> StakingAvailability.Unavailable + } + } + } + + override suspend fun createEnterAction( + integrationId: String, + amount: BigDecimal, + address: String, + validatorAddress: String, + token: Token, + ): EnterAction { + return withContext(dispatchers.io) { + val body = EnterActionRequestBody( + integrationId = integrationId, + addresses = Address(address), + args = EnterActionRequestBody.EnterActionRequestBodyArgs( + amount = amount.toFormattedString(token.decimals), + inputToken = tokenConverter.convertBack(token), + validatorAddress = validatorAddress, + ), + ) + val response = stakeKitApi.createEnterAction(body) + + enterActionResponseConverter.convert(response.getOrThrow()) + } + } + + override suspend fun constructTransaction(transactionId: String): StakingTransaction { + return withContext(dispatchers.io) { + val transactionResponse = stakeKitApi.constructTransaction( + transactionId = transactionId, + body = ConstructTransactionRequestBody(), + ) + + transactionConverter.convert(transactionResponse.getOrThrow()) + } + } + + override suspend fun fetchSingleYieldBalance( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + refresh: Boolean, + ) = withContext(dispatchers.io) { + if (!stakingFeatureToggle.isStakingEnabled) return@withContext + + val cryptoCurrency = address.cryptoCurrency + val rawCurrencyId = + cryptoCurrency.id.rawCurrencyId ?: error("Staking custom tokens is not available") + + val integrationId = integrationIdMap[rawCurrencyId] ?: return@withContext + + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val requestBody = getBalanceRequestData(address.address, integrationId) + val result = stakeKitApi.getSingleYieldBalance( + integrationId = requestBody.integrationId, + body = requestBody, + ).getOrThrow() + + stakingBalanceStore.store( + requestBody.integrationId, + YieldBalanceWrapperDTO( + balances = result, + integrationId = requestBody.integrationId, + ), + ) + }, + ) + } + + override fun getSingleYieldBalanceFlow( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + ): Flow = channelFlow { + if (!stakingFeatureToggle.isStakingEnabled) { + send(YieldBalance.Empty) + } else { + launch(dispatchers.io) { + val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId] + ?: error("Could not get integrationId") + stakingBalanceStore.get(integrationId) + .collectLatest { + send( + yieldBalanceConverter.convert( + YieldBalanceConverter.Data( + balance = it, + integrationId = integrationId, + ), + ), + ) + } + } + + withContext(dispatchers.io) { + fetchSingleYieldBalance( + userWalletId, + address, + ) + } + } + }.cancellable() + + override suspend fun getSingleYieldBalanceSync( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + ): YieldBalance = withContext(dispatchers.io) { + if (!stakingFeatureToggle.isStakingEnabled) { + YieldBalance.Empty + } else { + fetchSingleYieldBalance(userWalletId, address) + + val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId] + ?: error("Could not get integrationId") + val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error + yieldBalanceConverter.convert( + YieldBalanceConverter.Data( + balance = result, + integrationId = integrationId, + ), + ) + } + } + + override suspend fun fetchMultiYieldBalance( + userWalletId: UserWalletId, + addresses: List, + refresh: Boolean, + ) = withContext(dispatchers.io) { + if (!stakingFeatureToggle.isStakingEnabled) return@withContext + try { + isYieldBalanceFetching.update { + it + (userWalletId to true) + } + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val result = stakeKitApi.getMultipleYieldBalances( + addresses + .mapNotNull { networkAddress -> + val cryptoCurrency = networkAddress.cryptoCurrency + val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: error("Currency raw id is null") + val integrationId = integrationIdMap[rawCurrencyId] + + if (integrationId != null) { + networkAddress.address to integrationId + } else { + null + } + } + .distinct() + .map { getBalanceRequestData(it.first, it.second) }, + ).getOrThrow() + + stakingBalanceStore.store(result) + }, + ) + } finally { + isYieldBalanceFetching.update { + it - userWalletId + } + } + } + + override fun getMultiYieldBalanceFlow( + userWalletId: UserWalletId, + addresses: List, + ): Flow = channelFlow { + if (!stakingFeatureToggle.isStakingEnabled) { + send(YieldBalanceList.Empty) + } else { + launch(dispatchers.io) { + stakingBalanceStore.get() + .collectLatest { send(yieldBalanceListConverter.convert(it)) } + } + + withContext(dispatchers.io) { + fetchMultiYieldBalance( + userWalletId, + addresses, + ) + } + } + }.cancellable() + + override fun getMultiYieldBalanceLce( + userWalletId: UserWalletId, + addresses: List, + ): LceFlow = lceFlow { + if (!stakingFeatureToggle.isStakingEnabled) { + send(YieldBalanceList.Empty) + } else { + launch(dispatchers.io) { + combine( + stakingBalanceStore.get(), + isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } }, + ) { result, isFetching -> + val balances = yieldBalanceListConverter.convert(result) + send(balances, isStillLoading = isFetching) + }.collect() + } + withContext(dispatchers.io) { + catch( + block = { fetchMultiYieldBalance(userWalletId, addresses, refresh = false) }, + catch = { raise(it) }, + ) + } + } + } + + override suspend fun getMultiYieldBalanceSync( + userWalletId: UserWalletId, + addresses: List, + ): YieldBalanceList = withContext(dispatchers.io) { + if (!stakingFeatureToggle.isStakingEnabled) { + YieldBalanceList.Empty + } else { + fetchMultiYieldBalance(userWalletId, addresses) + val result = stakingBalanceStore.getSyncOrNull() ?: return@withContext YieldBalanceList.Error + yieldBalanceListConverter.convert(result) + } + } + + private fun findPrefetchedYield(yields: List, currencyId: String, symbol: String): Yield? { + return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol } + } + + private suspend fun getEnabledYields(): List? { + val yields = stakingYieldsStore.getSyncOrNull() ?: return null + return yields.map { yieldConverter.convert(it) } + } + + private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody { + return YieldBalanceRequestBody( + addresses = Address( + address = address, + additionalAddresses = null, // todo fill additional addresses metadata if needed + explorerUrl = "", // todo fill exporer url [REDACTED_JIRA] + ), + args = YieldBalanceRequestBody.YieldBalanceRequestArgs( + validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA] + ), + integrationId = integrationId, + ) + } + + private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}" + + private companion object { + const val YIELDS_STORE_KEY = "yields" + + const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" + const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" + const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" + const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking" + const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" + const val TRON_INTEGRATION_ID = "tron-trx-native-staking" + const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" + const val BINANCE_INTEGRATION_ID = "binance-bnb-native-staking" + const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" + const val NEAR_INTEGRATION_ID = "near-near-native-staking" + const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + + val integrationIdMap = mapOf( + Blockchain.Solana.toCoinId() to SOLANA_INTEGRATION_ID, + Blockchain.Cosmos.toCoinId() to COSMOS_INTEGRATION_ID, + Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID, + Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID, + Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID, + Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID, + Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID, + Blockchain.Binance.toCoinId() to BINANCE_INTEGRATION_ID, + Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID, + Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID, + Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID, ) } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt new file mode 100644 index 0000000000..a768417427 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt @@ -0,0 +1,153 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO +import com.tangem.domain.staking.model.NetworkType +import com.tangem.utils.converter.TwoWayConverter + +@Suppress("CyclomaticComplexMethod", "LongMethod") +class StakingNetworkTypeConverter : TwoWayConverter { + + override fun convert(value: NetworkTypeDTO): NetworkType { + return when (value) { + NetworkTypeDTO.AVALANCHE_C -> NetworkType.AVALANCHE_C + NetworkTypeDTO.AVALANCHE_ATOMIC -> NetworkType.AVALANCHE_ATOMIC + NetworkTypeDTO.AVALANCHE_P -> NetworkType.AVALANCHE_P + NetworkTypeDTO.ARBITRUM -> NetworkType.ARBITRUM + NetworkTypeDTO.BINANCE -> NetworkType.BINANCE + NetworkTypeDTO.CELO -> NetworkType.CELO + NetworkTypeDTO.ETHEREUM -> NetworkType.ETHEREUM + NetworkTypeDTO.ETHEREUM_GOERLI -> NetworkType.ETHEREUM_GOERLI + NetworkTypeDTO.ETHEREUM_HOLESKY -> NetworkType.ETHEREUM_HOLESKY + NetworkTypeDTO.FANTOM -> NetworkType.FANTOM + NetworkTypeDTO.HARMONY -> NetworkType.HARMONY + NetworkTypeDTO.OPTIMISM -> NetworkType.OPTIMISM + NetworkTypeDTO.POLYGON -> NetworkType.POLYGON + NetworkTypeDTO.GNOSIS -> NetworkType.GNOSIS + NetworkTypeDTO.MOONRIVER -> NetworkType.MOONRIVER + NetworkTypeDTO.OKC -> NetworkType.OKC + NetworkTypeDTO.ZKSYNC -> NetworkType.ZKSYNC + NetworkTypeDTO.VICTION -> NetworkType.VICTION + NetworkTypeDTO.AGORIC -> NetworkType.AGORIC + NetworkTypeDTO.AKASH -> NetworkType.AKASH + NetworkTypeDTO.AXELAR -> NetworkType.AXELAR + NetworkTypeDTO.BAND_PROTOCOL -> NetworkType.BAND_PROTOCOL + NetworkTypeDTO.BITSONG -> NetworkType.BITSONG + NetworkTypeDTO.CANTO -> NetworkType.CANTO + NetworkTypeDTO.CHIHUAHUA -> NetworkType.CHIHUAHUA + NetworkTypeDTO.COMDEX -> NetworkType.COMDEX + NetworkTypeDTO.COREUM -> NetworkType.COREUM + NetworkTypeDTO.COSMOS -> NetworkType.COSMOS + NetworkTypeDTO.CRESCENT -> NetworkType.CRESCENT + NetworkTypeDTO.CRONOS -> NetworkType.CRONOS + NetworkTypeDTO.CUDOS -> NetworkType.CUDOS + NetworkTypeDTO.DESMOS -> NetworkType.DESMOS + NetworkTypeDTO.DYDX -> NetworkType.DYDX + NetworkTypeDTO.EVMOS -> NetworkType.EVMOS + NetworkTypeDTO.FETCH_AI -> NetworkType.FETCH_AI + NetworkTypeDTO.GRAVITY_BRIDGE -> NetworkType.GRAVITY_BRIDGE + NetworkTypeDTO.INJECTIVE -> NetworkType.INJECTIVE + NetworkTypeDTO.IRISNET -> NetworkType.IRISNET + NetworkTypeDTO.JUNO -> NetworkType.JUNO + NetworkTypeDTO.KAVA -> NetworkType.KAVA + NetworkTypeDTO.KI_NETWORK -> NetworkType.KI_NETWORK + NetworkTypeDTO.MARS_PROTOCOL -> NetworkType.MARS_PROTOCOL + NetworkTypeDTO.NYM -> NetworkType.NYM + NetworkTypeDTO.OKEX_CHAIN -> NetworkType.OKEX_CHAIN + NetworkTypeDTO.ONOMY -> NetworkType.ONOMY + NetworkTypeDTO.OSMOSIS -> NetworkType.OSMOSIS + NetworkTypeDTO.PERSISTENCE -> NetworkType.PERSISTENCE + NetworkTypeDTO.QUICKSILVER -> NetworkType.QUICKSILVER + NetworkTypeDTO.REGEN -> NetworkType.REGEN + NetworkTypeDTO.SECRET -> NetworkType.SECRET + NetworkTypeDTO.SENTINEL -> NetworkType.SENTINEL + NetworkTypeDTO.SOMMELIER -> NetworkType.SOMMELIER + NetworkTypeDTO.STAFI -> NetworkType.STAFI + NetworkTypeDTO.STARGAZE -> NetworkType.STARGAZE + NetworkTypeDTO.STRIDE -> NetworkType.STRIDE + NetworkTypeDTO.TERITORI -> NetworkType.TERITORI + NetworkTypeDTO.TGRADE -> NetworkType.TGRADE + NetworkTypeDTO.UMEE -> NetworkType.UMEE + NetworkTypeDTO.POLKADOT -> NetworkType.POLKADOT + NetworkTypeDTO.KUSAMA -> NetworkType.KUSAMA + NetworkTypeDTO.WESTEND -> NetworkType.WESTEND + NetworkTypeDTO.BINANCEBEACON -> NetworkType.BINANCEBEACON + NetworkTypeDTO.NEAR -> NetworkType.NEAR + NetworkTypeDTO.SOLANA -> NetworkType.SOLANA + NetworkTypeDTO.TEZOS -> NetworkType.TEZOS + NetworkTypeDTO.TRON -> NetworkType.TRON + else -> NetworkType.UNKNOWN + } + } + + override fun convertBack(value: NetworkType): NetworkTypeDTO { + return when (value) { + NetworkType.AVALANCHE_C -> NetworkTypeDTO.AVALANCHE_C + NetworkType.AVALANCHE_ATOMIC -> NetworkTypeDTO.AVALANCHE_ATOMIC + NetworkType.AVALANCHE_P -> NetworkTypeDTO.AVALANCHE_P + NetworkType.ARBITRUM -> NetworkTypeDTO.ARBITRUM + NetworkType.BINANCE -> NetworkTypeDTO.BINANCE + NetworkType.CELO -> NetworkTypeDTO.CELO + NetworkType.ETHEREUM -> NetworkTypeDTO.ETHEREUM + NetworkType.ETHEREUM_GOERLI -> NetworkTypeDTO.ETHEREUM_GOERLI + NetworkType.ETHEREUM_HOLESKY -> NetworkTypeDTO.ETHEREUM_HOLESKY + NetworkType.FANTOM -> NetworkTypeDTO.FANTOM + NetworkType.HARMONY -> NetworkTypeDTO.HARMONY + NetworkType.OPTIMISM -> NetworkTypeDTO.OPTIMISM + NetworkType.POLYGON -> NetworkTypeDTO.POLYGON + NetworkType.GNOSIS -> NetworkTypeDTO.GNOSIS + NetworkType.MOONRIVER -> NetworkTypeDTO.MOONRIVER + NetworkType.OKC -> NetworkTypeDTO.OKC + NetworkType.ZKSYNC -> NetworkTypeDTO.ZKSYNC + NetworkType.VICTION -> NetworkTypeDTO.VICTION + NetworkType.AGORIC -> NetworkTypeDTO.AGORIC + NetworkType.AKASH -> NetworkTypeDTO.AKASH + NetworkType.AXELAR -> NetworkTypeDTO.AXELAR + NetworkType.BAND_PROTOCOL -> NetworkTypeDTO.BAND_PROTOCOL + NetworkType.BITSONG -> NetworkTypeDTO.BITSONG + NetworkType.CANTO -> NetworkTypeDTO.CANTO + NetworkType.CHIHUAHUA -> NetworkTypeDTO.CHIHUAHUA + NetworkType.COMDEX -> NetworkTypeDTO.COMDEX + NetworkType.COREUM -> NetworkTypeDTO.COREUM + NetworkType.COSMOS -> NetworkTypeDTO.COSMOS + NetworkType.CRESCENT -> NetworkTypeDTO.CRESCENT + NetworkType.CRONOS -> NetworkTypeDTO.CRONOS + NetworkType.CUDOS -> NetworkTypeDTO.CUDOS + NetworkType.DESMOS -> NetworkTypeDTO.DESMOS + NetworkType.DYDX -> NetworkTypeDTO.DYDX + NetworkType.EVMOS -> NetworkTypeDTO.EVMOS + NetworkType.FETCH_AI -> NetworkTypeDTO.FETCH_AI + NetworkType.GRAVITY_BRIDGE -> NetworkTypeDTO.GRAVITY_BRIDGE + NetworkType.INJECTIVE -> NetworkTypeDTO.INJECTIVE + NetworkType.IRISNET -> NetworkTypeDTO.IRISNET + NetworkType.JUNO -> NetworkTypeDTO.JUNO + NetworkType.KAVA -> NetworkTypeDTO.KAVA + NetworkType.KI_NETWORK -> NetworkTypeDTO.KI_NETWORK + NetworkType.MARS_PROTOCOL -> NetworkTypeDTO.MARS_PROTOCOL + NetworkType.NYM -> NetworkTypeDTO.NYM + NetworkType.OKEX_CHAIN -> NetworkTypeDTO.OKEX_CHAIN + NetworkType.ONOMY -> NetworkTypeDTO.ONOMY + NetworkType.OSMOSIS -> NetworkTypeDTO.OSMOSIS + NetworkType.PERSISTENCE -> NetworkTypeDTO.PERSISTENCE + NetworkType.QUICKSILVER -> NetworkTypeDTO.QUICKSILVER + NetworkType.REGEN -> NetworkTypeDTO.REGEN + NetworkType.SECRET -> NetworkTypeDTO.SECRET + NetworkType.SENTINEL -> NetworkTypeDTO.SENTINEL + NetworkType.SOMMELIER -> NetworkTypeDTO.SOMMELIER + NetworkType.STAFI -> NetworkTypeDTO.STAFI + NetworkType.STARGAZE -> NetworkTypeDTO.STARGAZE + NetworkType.STRIDE -> NetworkTypeDTO.STRIDE + NetworkType.TERITORI -> NetworkTypeDTO.TERITORI + NetworkType.TGRADE -> NetworkTypeDTO.TGRADE + NetworkType.UMEE -> NetworkTypeDTO.UMEE + NetworkType.POLKADOT -> NetworkTypeDTO.POLKADOT + NetworkType.KUSAMA -> NetworkTypeDTO.KUSAMA + NetworkType.WESTEND -> NetworkTypeDTO.WESTEND + NetworkType.BINANCEBEACON -> NetworkTypeDTO.BINANCEBEACON + NetworkType.NEAR -> NetworkTypeDTO.NEAR + NetworkType.SOLANA -> NetworkTypeDTO.SOLANA + NetworkType.TEZOS -> NetworkTypeDTO.TEZOS + NetworkType.TRON -> NetworkTypeDTO.TRON + else -> NetworkTypeDTO.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt new file mode 100644 index 0000000000..06cbb55869 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/StakingTokenConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.TokenWithYieldDTO +import com.tangem.domain.staking.model.StakingToken +import com.tangem.domain.staking.model.StakingTokenWithYield +import com.tangem.utils.converter.Converter + +class StakingTokenConverter : Converter { + + override fun convert(value: TokenWithYieldDTO): StakingTokenWithYield { + return StakingTokenWithYield( + token = StakingToken( + name = value.token.name, + symbol = value.token.symbol, + decimals = value.token.decimals, + contractAddress = value.token.address, + coinGeckoId = value.token.coinGeckoId, + ), + availableYieldIds = value.availableYieldIds, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt new file mode 100644 index 0000000000..2148e18511 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt @@ -0,0 +1,36 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO +import com.tangem.domain.staking.model.Token +import com.tangem.utils.converter.TwoWayConverter + +class TokenConverter( + private val stakingNetworkTypeConverter: StakingNetworkTypeConverter, +) : TwoWayConverter { + + override fun convert(value: TokenDTO): Token { + return Token( + name = value.name, + network = stakingNetworkTypeConverter.convert(value.network), + symbol = value.symbol, + decimals = value.decimals, + address = value.address, + coinGeckoId = value.coinGeckoId, + logoURI = value.logoURI, + isPoints = value.isPoints, + ) + } + + override fun convertBack(value: Token): TokenDTO { + return TokenDTO( + name = value.name, + network = stakingNetworkTypeConverter.convertBack(value.network), + symbol = value.symbol, + decimals = value.decimals, + address = value.address, + coinGeckoId = value.coinGeckoId, + logoURI = value.logoURI, + isPoints = value.isPoints, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt new file mode 100644 index 0000000000..d0abdee20e --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt @@ -0,0 +1,37 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO +import com.tangem.domain.staking.model.BalanceItem +import com.tangem.domain.staking.model.BalanceType +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.YieldBalanceItem +import com.tangem.utils.converter.Converter + +internal class YieldBalanceConverter : Converter { + + override fun convert(value: Data): YieldBalance { + return if (value.balance.isEmpty()) { + YieldBalance.Empty + } else { + YieldBalance.Data( + balance = YieldBalanceItem( + items = value.balance.map { item -> + BalanceItem( + type = BalanceType.valueOf(item.type.name), + amount = item.amount, + pricePerShare = item.pricePerShare, + rawCurrencyId = item.tokenDTO.coinGeckoId, + validatorAddress = item.validatorAddress, + ) + }, + integrationId = value.integrationId, + ), + ) + } + } + + data class Data( + val balance: List, + val integrationId: String?, + ) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt new file mode 100644 index 0000000000..883ba21acd --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.utils.converter.Converter + +internal class YieldBalanceListConverter : Converter, YieldBalanceList> { + + internal val converter by lazy(LazyThreadSafetyMode.NONE) { + YieldBalanceConverter() + } + + override fun convert(value: List): YieldBalanceList { + return if (value.isEmpty()) { + YieldBalanceList.Empty + } else { + YieldBalanceList.Data( + balances = value.map { + converter.convert( + YieldBalanceConverter.Data( + balance = it.balances, + integrationId = it.integrationId, + ), + ) + }, + ) + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt new file mode 100644 index 0000000000..09e1026127 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -0,0 +1,124 @@ +package com.tangem.data.staking.converters + +import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.domain.staking.model.* +import com.tangem.utils.converter.Converter + +class YieldConverter( + private val tokenConverter: TokenConverter, +) : Converter { + + override fun convert(value: YieldDTO): Yield { + return Yield( + id = value.id, + token = tokenConverter.convert(value.token), + tokens = value.tokens.map { tokenConverter.convert(it) }, + args = convertArgs(value.args), + status = convertStatus(value.status), + apy = value.apy, + rewardRate = value.rewardRate, + rewardType = convertRewardType(value.rewardType), + metadata = convertMetadata(value.metadata), + validators = value.validators + .filter { it.preferred } + .map { convertValidator(it) } + .sortedByDescending { it.apr }, + isAvailable = value.isAvailable, + ) + } + + private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args { + return Yield.Args( + enter = convertEnter(argsDTO.enter), + exit = argsDTO.exit?.let { convertEnter(it) }, + ) + } + + private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter { + return Yield.Args.Enter( + addresses = convertAddresses(enterDTO.addresses), + args = enterDTO.args.mapValues { convertAddressArgument(it.value) }, + ) + } + + private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses { + return Yield.Args.Enter.Addresses( + address = convertAddressArgument(addressesDTO.address), + additionalAddresses = addressesDTO.additionalAddresses?.mapValues { convertAddressArgument(it.value) }, + ) + } + + private fun convertAddressArgument(addressArgumentDTO: AddressArgumentDTO): AddressArgument { + return AddressArgument( + required = addressArgumentDTO.required, + network = addressArgumentDTO.network, + minimum = addressArgumentDTO.minimum, + maximum = addressArgumentDTO.maximum, + ) + } + + private fun convertStatus(statusDTO: YieldDTO.StatusDTO): Yield.Status { + return Yield.Status( + enter = statusDTO.enter, + exit = statusDTO.exit, + ) + } + + private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata { + return Yield.Metadata( + name = metadataDTO.name, + logoUri = metadataDTO.logoUri, + description = metadataDTO.description, + documentation = metadataDTO.documentation, + gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO), + token = tokenConverter.convert(metadataDTO.tokenDTO), + tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) }, + type = metadataDTO.type, + rewardSchedule = metadataDTO.rewardSchedule, + cooldownPeriod = convertPeriod(metadataDTO.cooldownPeriod), + warmupPeriod = convertPeriod(metadataDTO.warmupPeriod), + rewardClaiming = metadataDTO.rewardClaiming, + defaultValidator = metadataDTO.defaultValidator, + minimumStake = metadataDTO.minimumStake, + supportsMultipleValidators = metadataDTO.supportsMultipleValidators, + revshare = convertEnabled(metadataDTO.revshare), + fee = convertEnabled(metadataDTO.fee), + ) + } + + private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period { + return Yield.Metadata.Period( + days = periodDTO.days, + ) + } + + private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled { + return Yield.Metadata.Enabled( + enabled = enabledDTO.enabled, + ) + } + + private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator { + return Yield.Validator( + address = validatorDTO.address, + status = validatorDTO.status, + name = validatorDTO.name, + image = validatorDTO.image, + website = validatorDTO.website, + apr = validatorDTO.apr, + commission = validatorDTO.commission, + stakedBalance = validatorDTO.stakedBalance, + votingPower = validatorDTO.votingPower, + preferred = validatorDTO.preferred, + ) + } + + private fun convertRewardType(rewardTypeDTO: YieldDTO.RewardTypeDTO): Yield.RewardType { + return when (rewardTypeDTO) { + YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY + YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR + else -> Yield.RewardType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt new file mode 100644 index 0000000000..360ebbbbf6 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.data.staking.converters.action + +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO +import com.tangem.domain.staking.model.action.StakingActionStatus +import com.tangem.utils.converter.Converter + +class ActionStatusConverter : Converter { + override fun convert(value: StakingActionStatusDTO): StakingActionStatus { + return when (value) { + StakingActionStatusDTO.CANCELED -> StakingActionStatus.CANCELED + StakingActionStatusDTO.CREATED -> StakingActionStatus.CREATED + StakingActionStatusDTO.WAITING_FOR_NEXT -> StakingActionStatus.WAITING_FOR_NEXT + StakingActionStatusDTO.PROCESSING -> StakingActionStatus.PROCESSING + StakingActionStatusDTO.FAILED -> StakingActionStatus.FAILED + StakingActionStatusDTO.SUCCESS -> StakingActionStatus.SUCCESS + else -> StakingActionStatus.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt new file mode 100644 index 0000000000..888d805305 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt @@ -0,0 +1,28 @@ +package com.tangem.data.staking.converters.action + +import com.tangem.data.staking.converters.transaction.StakingTransactionConverter +import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.utils.converter.Converter + +class EnterActionResponseConverter( + private val actionStatusConverter: ActionStatusConverter, + private val stakingActionTypeConverter: StakingActionTypeConverter, + private val transactionConverter: StakingTransactionConverter, +) : Converter { + + override fun convert(value: EnterActionResponse): EnterAction { + return EnterAction( + id = value.id, + integrationId = value.integrationId, + status = actionStatusConverter.convert(value.status), + type = stakingActionTypeConverter.convert(value.type), + currentStepIndex = value.currentStepIndex, + amount = value.amount, + validatorAddress = value.validatorAddress, + validatorAddresses = value.validatorAddresses, + transactions = value.transactions?.map(transactionConverter::convert), + createdAt = value.createdAt, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt new file mode 100644 index 0000000000..941020c57d --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt @@ -0,0 +1,30 @@ +package com.tangem.data.staking.converters.action + +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO +import com.tangem.domain.staking.model.action.StakingActionType +import com.tangem.utils.converter.Converter + +@Suppress("CyclomaticComplexMethod") +class StakingActionTypeConverter : Converter { + + override fun convert(value: StakingActionTypeDTO): StakingActionType { + return when (value) { + StakingActionTypeDTO.STAKE -> StakingActionType.STAKE + StakingActionTypeDTO.UNSTAKE -> StakingActionType.UNSTAKE + StakingActionTypeDTO.CLAIM_REWARDS -> StakingActionType.CLAIM_REWARDS + StakingActionTypeDTO.RESTAKE_REWARDS -> StakingActionType.RESTAKE_REWARDS + StakingActionTypeDTO.WITHDRAW -> StakingActionType.WITHDRAW + StakingActionTypeDTO.RESTAKE -> StakingActionType.RESTAKE + StakingActionTypeDTO.CLAIM_UNSTAKED -> StakingActionType.CLAIM_UNSTAKED + StakingActionTypeDTO.UNLOCK_LOCKED -> StakingActionType.UNLOCK_LOCKED + StakingActionTypeDTO.STAKE_LOCKED -> StakingActionType.STAKE_LOCKED + StakingActionTypeDTO.VOTE -> StakingActionType.VOTE + StakingActionTypeDTO.REVOKE -> StakingActionType.REVOKE + StakingActionTypeDTO.VOTE_LOCKED -> StakingActionType.VOTE_LOCKED + StakingActionTypeDTO.REVOTE -> StakingActionType.REVOTE + StakingActionTypeDTO.REBOND -> StakingActionType.REBOND + StakingActionTypeDTO.MIGRATE -> StakingActionType.MIGRATE + StakingActionTypeDTO.UNKNOWN -> StakingActionType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt new file mode 100644 index 0000000000..3219d87359 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.data.staking.converters.transaction + +import com.tangem.data.staking.converters.TokenConverter +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO +import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.utils.converter.Converter + +class GasEstimateConverter( + private val tokenConverter: TokenConverter, +) : Converter { + + override fun convert(value: StakingGasEstimateDTO): StakingGasEstimate { + return StakingGasEstimate( + amount = value.amount, + token = tokenConverter.convert(value.token), + gasLimit = value.gasLimit, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt new file mode 100644 index 0000000000..1ad9ca3198 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt @@ -0,0 +1,33 @@ +package com.tangem.data.staking.converters.transaction + +import com.tangem.data.staking.converters.StakingNetworkTypeConverter +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO +import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.utils.converter.Converter + +class StakingTransactionConverter( + private val networkTypeConverter: StakingNetworkTypeConverter, + private val transactionStatusConverter: StakingTransactionStatusConverter, + private val transactionTypeConverter: StakingTransactionTypeConverter, + private val gasEstimateConverter: GasEstimateConverter, +) : Converter { + + override fun convert(value: StakingTransactionDTO): StakingTransaction { + return StakingTransaction( + id = value.id, + network = networkTypeConverter.convert(value.network), + status = transactionStatusConverter.convert(value.status), + type = transactionTypeConverter.convert(value.type), + hash = value.hash, + signedTransaction = value.signedTransaction, + unsignedTransaction = value.unsignedTransaction, + stepIndex = value.stepIndex, + error = value.error, + gasEstimate = value.gasEstimate?.let { gasEstimateConverter.convert(it) }, + stakeId = value.stakeId, + explorerUrl = value.explorerUrl, + ledgerHwAppId = value.ledgerHwAppId, + isMessage = value.isMessage, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt new file mode 100644 index 0000000000..9569f61534 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionStatusConverter.kt @@ -0,0 +1,24 @@ +package com.tangem.data.staking.converters.transaction + +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionStatusDTO +import com.tangem.domain.staking.model.transaction.StakingTransactionStatus +import com.tangem.utils.converter.Converter + +class StakingTransactionStatusConverter : Converter { + + override fun convert(value: StakingTransactionStatusDTO): StakingTransactionStatus { + return when (value) { + StakingTransactionStatusDTO.NOT_FOUND -> StakingTransactionStatus.NOT_FOUND + StakingTransactionStatusDTO.CREATED -> StakingTransactionStatus.CREATED + StakingTransactionStatusDTO.BLOCKED -> StakingTransactionStatus.BLOCKED + StakingTransactionStatusDTO.WAITING_FOR_SIGNATURE -> StakingTransactionStatus.WAITING_FOR_SIGNATURE + StakingTransactionStatusDTO.SIGNED -> StakingTransactionStatus.SIGNED + StakingTransactionStatusDTO.BROADCASTED -> StakingTransactionStatus.BROADCASTED + StakingTransactionStatusDTO.PENDING -> StakingTransactionStatus.PENDING + StakingTransactionStatusDTO.CONFIRMED -> StakingTransactionStatus.CONFIRMED + StakingTransactionStatusDTO.FAILED -> StakingTransactionStatus.FAILED + StakingTransactionStatusDTO.SKIPPED -> StakingTransactionStatus.SKIPPED + StakingTransactionStatusDTO.UNKNOWN -> StakingTransactionStatus.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt new file mode 100644 index 0000000000..15e156f691 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionTypeConverter.kt @@ -0,0 +1,54 @@ +package com.tangem.data.staking.converters.transaction + +import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionTypeDTO +import com.tangem.domain.staking.model.transaction.StakingTransactionType +import com.tangem.utils.converter.Converter + +@Suppress("CyclomaticComplexMethod") +class StakingTransactionTypeConverter : Converter { + + override fun convert(value: StakingTransactionTypeDTO): StakingTransactionType { + return when (value) { + StakingTransactionTypeDTO.SWAP -> StakingTransactionType.SWAP + StakingTransactionTypeDTO.DEPOSIT -> StakingTransactionType.DEPOSIT + StakingTransactionTypeDTO.APPROVAL -> StakingTransactionType.APPROVAL + StakingTransactionTypeDTO.STAKE -> StakingTransactionType.STAKE + StakingTransactionTypeDTO.CLAIM_UNSTAKED -> StakingTransactionType.CLAIM_UNSTAKED + StakingTransactionTypeDTO.CLAIM_REWARDS -> StakingTransactionType.CLAIM_REWARDS + StakingTransactionTypeDTO.RESTAKE_REWARDS -> StakingTransactionType.RESTAKE_REWARDS + StakingTransactionTypeDTO.UNSTAKE -> StakingTransactionType.UNSTAKE + StakingTransactionTypeDTO.SPLIT -> StakingTransactionType.SPLIT + StakingTransactionTypeDTO.MERGE -> StakingTransactionType.MERGE + StakingTransactionTypeDTO.LOCK -> StakingTransactionType.LOCK + StakingTransactionTypeDTO.UNLOCK -> StakingTransactionType.UNLOCK + StakingTransactionTypeDTO.SUPPLY -> StakingTransactionType.SUPPLY + StakingTransactionTypeDTO.BRIDGE -> StakingTransactionType.BRIDGE + StakingTransactionTypeDTO.VOTE -> StakingTransactionType.VOTE + StakingTransactionTypeDTO.REVOKE -> StakingTransactionType.REVOKE + StakingTransactionTypeDTO.RESTAKE -> StakingTransactionType.RESTAKE + StakingTransactionTypeDTO.REBOND -> StakingTransactionType.REBOND + StakingTransactionTypeDTO.WITHDRAW -> StakingTransactionType.WITHDRAW + StakingTransactionTypeDTO.CREATE_ACCOUNT -> StakingTransactionType.CREATE_ACCOUNT + StakingTransactionTypeDTO.REVEAL -> StakingTransactionType.REVEAL + StakingTransactionTypeDTO.MIGRATE -> StakingTransactionType.MIGRATE + StakingTransactionTypeDTO.UTXO_P_TO_C_IMPORT -> StakingTransactionType.UTXO_P_TO_C_IMPORT + StakingTransactionTypeDTO.UTXO_C_TO_P_IMPORT -> StakingTransactionType.UTXO_C_TO_P_IMPORT + StakingTransactionTypeDTO.UNFREEZE_LEGACY -> StakingTransactionType.UNFREEZE_LEGACY + StakingTransactionTypeDTO.UNFREEZE_LEGACY_BANDWIDTH -> StakingTransactionType.UNFREEZE_LEGACY_BANDWIDTH + StakingTransactionTypeDTO.UNFREEZE_LEGACY_ENERGY -> StakingTransactionType.UNFREEZE_LEGACY_ENERGY + StakingTransactionTypeDTO.UNFREEZE_BANDWIDTH -> StakingTransactionType.UNFREEZE_BANDWIDTH + StakingTransactionTypeDTO.UNFREEZE_ENERGY -> StakingTransactionType.UNFREEZE_ENERGY + StakingTransactionTypeDTO.FREEZE_BANDWIDTH -> StakingTransactionType.FREEZE_BANDWIDTH + StakingTransactionTypeDTO.FREEZE_ENERGY -> StakingTransactionType.FREEZE_ENERGY + StakingTransactionTypeDTO.UNDELEGATE_BANDWIDTH -> StakingTransactionType.UNDELEGATE_BANDWIDTH + StakingTransactionTypeDTO.UNDELEGATE_ENERGY -> StakingTransactionType.UNDELEGATE_ENERGY + StakingTransactionTypeDTO.P2P_NODE_REQUEST -> StakingTransactionType.P2P_NODE_REQUEST + StakingTransactionTypeDTO.LUGANODES_PROVISION -> StakingTransactionType.LUGANODES_PROVISION + StakingTransactionTypeDTO.LUGANODES_EXIT_REQUEST -> StakingTransactionType.LUGANODES_EXIT_REQUEST + StakingTransactionTypeDTO.INFSTONES_PROVISION -> StakingTransactionType.INFSTONES_PROVISION + StakingTransactionTypeDTO.INFSTONES_EXIT_REQUEST -> StakingTransactionType.INFSTONES_EXIT_REQUEST + StakingTransactionTypeDTO.INFSTONES_CLAIM_REQUEST -> StakingTransactionType.INFSTONES_CLAIM_REQUEST + StakingTransactionTypeDTO.UNKNOWN -> StakingTransactionType.UNKNOWN + } + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 0f5076bb01..b855fe3386 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -1,7 +1,10 @@ package com.tangem.data.staking.di +import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.staking.DefaultStakingRepository import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.token.StakingBalanceStore +import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -19,13 +22,19 @@ internal object StakingDataModule { @Singleton fun provideStakingRepository( stakeKitApi: StakeKitApi, - stakingFeatureToggles: StakingFeatureToggles, - coroutineDispatcherProvider: CoroutineDispatcherProvider, + stakingTokenStore: StakingYieldsStore, + stakingBalanceStore: StakingBalanceStore, + dispatchers: CoroutineDispatcherProvider, + stakingFeatureToggle: StakingFeatureToggles, + cacheRegistry: CacheRegistry, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, - stakingFeatureToggles = stakingFeatureToggles, - dispatchers = coroutineDispatcherProvider, + stakingYieldsStore = stakingTokenStore, + stakingBalanceStore = stakingBalanceStore, + dispatchers = dispatchers, + cacheRegistry = cacheRegistry, + stakingFeatureToggle = stakingFeatureToggle, ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index f31d0e4b5f..59ddeeb01d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -7,7 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.tokens.repository.* @@ -31,7 +31,7 @@ internal object TokensDataModule { userTokensStore: UserTokensStore, userWalletsStore: UserWalletsStore, walletManagersFacade: WalletManagersFacade, - assetsStore: AssetsStore, + expressAssetsStore: ExpressAssetsStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, ): CurrenciesRepository { @@ -41,7 +41,7 @@ internal object TokensDataModule { userTokensStore = userTokensStore, walletManagersFacade = walletManagersFacade, userWalletsStore = userWalletsStore, - assetsStore = assetsStore, + expressAssetsStore = expressAssetsStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, ) @@ -88,10 +88,10 @@ internal object TokensDataModule { @Provides @Singleton fun provideDefaultMarketCoinsRepository( - assetsStore: AssetsStore, + expressAssetsStore: ExpressAssetsStore, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): MarketCryptoCurrencyRepository { - return DefaultMarketCryptoCurrencyRepository(assetsStore, coroutineDispatcherProvider) + return DefaultMarketCryptoCurrencyRepository(expressAssetsStore, coroutineDispatcherProvider) } @Provides diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 9459984032..7951e83c17 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -15,7 +15,7 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.token.UserTokensStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.derivationStyleProvider @@ -46,7 +46,7 @@ internal class DefaultCurrenciesRepository( private val userTokensStore: UserTokensStore, private val userWalletsStore: UserWalletsStore, private val walletManagersFacade: WalletManagersFacade, - private val assetsStore: AssetsStore, + private val expressAssetsStore: ExpressAssetsStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, ) : CurrenciesRepository { @@ -61,6 +61,18 @@ internal class DefaultCurrenciesRepository( private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow( value = emptyMap(), ) + private val parallelTransactionsEnabledBlockchains = setOf( + Blockchain.Ethereum, + Blockchain.EthereumTestnet, + Blockchain.Polygon, + Blockchain.PolygonTestnet, + Blockchain.Arbitrum, + Blockchain.ArbitrumTestnet, + Blockchain.Binance, + Blockchain.BinanceTestnet, + Blockchain.Tron, + Blockchain.TronTestnet, + ) override suspend fun saveTokens( userWalletId: UserWalletId, @@ -368,18 +380,19 @@ internal class DefaultCurrenciesRepository( } } - override fun hasPendingTransactions( + override fun isSendBlockedByPendingTransactions( cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, ): Boolean { val blockchain = Blockchain.fromId(cryptoCurrencyStatus.currency.network.id.value) val isBitcoinBlockchain = blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet - - return if (cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain) { - val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing } - outgoingTransactions.isNotEmpty() - } else { - coinStatus?.value?.hasCurrentNetworkTransactions == true + return when { + cryptoCurrencyStatus.currency is CryptoCurrency.Coin && isBitcoinBlockchain -> { + val outgoingTransactions = cryptoCurrencyStatus.value.pendingTransactions.filter { it.isOutgoing } + outgoingTransactions.isNotEmpty() + } + parallelTransactionsEnabledBlockchains.contains(blockchain) -> false + else -> coinStatus?.value?.hasCurrentNetworkTransactions == true } } @@ -497,7 +510,7 @@ internal class DefaultCurrenciesRepository( ), ) - assetsStore.store(userWalletId, response.getOrThrow()) + expressAssetsStore.store(userWalletId, response.getOrThrow()) } } catch (e: Throwable) { Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index d4a0f8fd29..83f30079f3 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -1,7 +1,7 @@ package com.tangem.data.tokens.repository import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE -import com.tangem.datasource.local.token.AssetsStore +import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.wallets.models.UserWalletId @@ -9,7 +9,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext class DefaultMarketCryptoCurrencyRepository( - private val assetsStore: AssetsStore, + private val expressAssetsStore: ExpressAssetsStore, private val dispatchers: CoroutineDispatcherProvider, ) : MarketCryptoCurrencyRepository { @@ -22,7 +22,7 @@ class DefaultMarketCryptoCurrencyRepository( val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE - val asset = assetsStore.getSyncOrNull(userWalletId)?.find { + val asset = expressAssetsStore.getSyncOrNull(userWalletId)?.find { it.network == cryptoCurrency.network.backendId && it.contractAddress.equals(contractAddress, ignoreCase = true) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index c4a12be730..ce8d705b9f 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -124,6 +124,60 @@ internal class DefaultNetworksRepository( } } + override suspend fun getNetworkAddress( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): CryptoCurrencyAddress = withContext(dispatchers.io) { + CryptoCurrencyAddress( + cryptoCurrency = currency, + address = walletManagersFacade.getAddresses(userWalletId, currency.network) + .firstOrNull { it.type == AddressType.Default } + ?.value.orEmpty(), + ) + } + + override fun getNetworkAddressFlow( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Flow = channelFlow { + launch(dispatchers.io) { + send(getNetworkAddress(userWalletId, currency)) + } + } + + override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List = + withContext(dispatchers.io) { + // Get list of currencies matching [network] + val currencies = getCurrencies(userWalletId) + + // There is no currencies matching given [networks] in [userWalletId] + if (currencies.toList().isEmpty()) return@withContext emptyList() + + currencies.toList().map { currency -> + CryptoCurrencyAddress( + cryptoCurrency = currency, + address = walletManagersFacade.getAddresses(userWalletId, currency.network) + .firstOrNull { it.type == AddressType.Default } + ?.value.orEmpty(), + ) + } + } + + override fun getNetworkAddressesFlow( + userWalletId: UserWalletId, + network: Network, + ): Flow> = channelFlow { + launch(dispatchers.io) { + send(getNetworkAddresses(userWalletId, network)) + } + } + + override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> = channelFlow { + launch(dispatchers.io) { + send(getNetworkAddresses(userWalletId)) + } + } + private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, networks: Set, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt index d76681efa4..85c903ceec 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/QuotesConverter.kt @@ -13,7 +13,7 @@ internal class QuotesConverter : Converter { return Quote( rawCurrencyId = rawCurrencyId, fiatRate = responseQuote.price ?: BigDecimal.ZERO, - priceChange = (responseQuote.priceChange ?: BigDecimal.ZERO).movePointLeft(2), + priceChange = (responseQuote.priceChange24h ?: BigDecimal.ZERO).movePointLeft(2), ) } } \ No newline at end of file diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index 615758dcf6..41baa4a4ba 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { /** Tangem SDKs */ implementation(deps.tangem.blockchain) + implementation(deps.tangem.card.core) /** Core */ implementation(projects.core.datasource) @@ -25,6 +26,7 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) /** DI */ implementation(deps.hilt.android) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index d36cb4f74b..caffe5aca9 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -4,22 +4,29 @@ import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras import com.tangem.blockchain.blockchains.stellar.StellarMemo import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras import com.tangem.blockchain.blockchains.ton.TonTransactionExtras +import com.tangem.blockchain.blockchains.tron.TronTransactionExtras import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.common.extensions.hexToBytes import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal +import java.math.BigInteger +import com.tangem.blockchain.blockchains.tron.TransactionType as SdkTransactionType internal class DefaultTransactionRepository( private val walletManagersFacade: WalletManagersFacade, @@ -37,7 +44,7 @@ internal class DefaultTransactionRepository( isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, - ): TransactionData? = withContext(coroutineDispatcherProvider.io) { + ): TransactionData.Uncompiled? = withContext(coroutineDispatcherProvider.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, @@ -45,7 +52,7 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) - return@withContext walletManager?.createTransactionInternal( + return@withContext walletManager?.createTransactionDataInternal( amount = amount, fee = fee, memo = memo, @@ -78,7 +85,7 @@ internal class DefaultTransactionRepository( val validator = walletManager as? TransactionValidator if (validator != null) { - val transaction = walletManager.createTransactionInternal( + val transactionData = walletManager.createTransactionDataInternal( amount = amount, fee = fee ?: Fee.Common(amount = amount), memo = memo, @@ -89,7 +96,7 @@ internal class DefaultTransactionRepository( hash = hash, ) - validator.validate(transaction = transaction) + validator.validate(transactionData = transactionData) } else { Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation") Result.success(Unit) @@ -111,8 +118,41 @@ internal class DefaultTransactionRepository( (walletManager as TransactionSender).send(txData, signer) } + override fun createTransactionDataExtras( + data: String, + network: Network, + transactionType: TransactionType, + nonce: BigInteger?, + gasLimit: BigInteger?, + ): TransactionExtras { + val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) + ?: error("Blockchain not found") + return when { + blockchain.isEvm() -> { + EthereumTransactionExtras( + data = data.hexToBytes(), + gasLimit = gasLimit, + nonce = nonce, + ) + } + blockchain == Blockchain.Tron -> { + TronTransactionExtras( + data = data.hexToBytes(), + txType = convertToSdkTransactionType(transactionType), + ) + } + else -> error("Data extras not supported for $blockchain") + } + } + + private fun convertToSdkTransactionType(transactionType: TransactionType): SdkTransactionType { + return when (transactionType) { + TransactionType.APPROVE -> SdkTransactionType.APPROVE + } + } + @Suppress("LongParameterList") - private fun WalletManager.createTransactionInternal( + private fun WalletManager.createTransactionDataInternal( amount: Amount, fee: Fee, memo: String?, @@ -121,7 +161,7 @@ internal class DefaultTransactionRepository( isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, - ): TransactionData { + ): TransactionData.Uncompiled { // TODO: refactor workaround to use general mechanism in bsdk for build tx for DEX val txAmount = if (isSwap) { createAmountForSwap(amount) diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index a817781b53..e558b2def0 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -10,10 +10,8 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.visa.utils.VisaConfig -import com.tangem.data.visa.utils.VisaCurrencyFactory -import com.tangem.data.visa.utils.VisaTxDetailsFactory -import com.tangem.data.visa.utils.VisaTxHistoryPagingSource +import com.tangem.data.visa.config.VisaLibLoader +import com.tangem.data.visa.utils.* import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.userwallet.UserWalletsStore @@ -24,8 +22,6 @@ import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.domain.visa.repository.VisaRepository import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.lib.visa.VisaContractInfoProvider -import com.tangem.lib.visa.api.VisaApi import com.tangem.lib.visa.model.VisaTxHistoryResponse import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow @@ -35,9 +31,8 @@ import kotlinx.coroutines.withContext import java.math.BigDecimal internal class DefaultVisaRepository( - private val visaContractInfoProvider: VisaContractInfoProvider, + private val visaLibLoader: VisaLibLoader, private val tangemTechApi: TangemTechApi, - private val visaApi: VisaApi, private val cacheRegistry: CacheRegistry, private val userWalletsStore: UserWalletsStore, private val dispatchers: CoroutineDispatcherProvider, @@ -76,9 +71,11 @@ internal class DefaultVisaRepository( } private suspend fun fetchVisaCurrency(address: String) { + val contractInfoProvider = visaLibLoader.getOrCreateProvider() + parZip( dispatchers.io, - { visaContractInfoProvider.getContractInfo(address) }, + { contractInfoProvider.getContractInfo(address) }, { getFiatRate() }, { contractInfo, fiatRate -> fetchedCurrencies.update { value -> @@ -97,6 +94,7 @@ internal class DefaultVisaRepository( ): Flow> { val userWallet = findVisaUserWallet(userWalletId) val cardPubKey = getCardPubKey(userWallet) + val api = visaLibLoader.getOrCreateApi() val pager = Pager( config = PagingConfig( pageSize = pageSize, @@ -110,7 +108,7 @@ internal class DefaultVisaRepository( isRefresh = isRefresh, ), cacheRegistry = cacheRegistry, - visaApi = visaApi, + visaApi = api, fetchedItems = fetchedHistoryItems, dispatchers = dispatchers, ) @@ -137,7 +135,7 @@ internal class DefaultVisaRepository( } private suspend fun makeAddress(userWalletId: UserWalletId): String { - if (IS_DEMO_MODE_ENABLED) return DEMO_ADDRESS + if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress() val userWallet = findVisaUserWallet(userWalletId) val walletAddresses = makeWalletAddresses(userWallet) @@ -149,13 +147,13 @@ internal class DefaultVisaRepository( } private suspend fun getFiatRate(): BigDecimal? { - val fiatCurrencyId = VisaConfig.fiatCurrency.code.lowercase() + val fiatCurrencyId = VisaConstants.fiatCurrency.code.lowercase() val quotes = tangemTechApi.getQuotes( currencyId = fiatCurrencyId, - coinIds = VisaConfig.TOKEN_ID, + coinIds = VisaConstants.TOKEN_ID, ).getOrThrow() - return quotes.quotes[VisaConfig.TOKEN_ID]?.price + return quotes.quotes[VisaConstants.TOKEN_ID]?.price } private fun makeWalletAddresses(userWallet: UserWallet): Set

{ @@ -165,7 +163,7 @@ internal class DefaultVisaRepository( } private fun getCardPubKey(userWallet: UserWallet): String { - if (IS_DEMO_MODE_ENABLED) return DEMO_PUBLIC_KEY + if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoPublicKey() val cardWallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 @@ -189,12 +187,4 @@ internal class DefaultVisaRepository( private fun getVisaCurrencyKey(address: String): String { return "visa_currency_$address" } - - private companion object { - // Must be `false` in production - const val IS_DEMO_MODE_ENABLED = false - - const val DEMO_ADDRESS = "0x40d8194b7168723ece51fa34d16825c60ba03dfa" - const val DEMO_PUBLIC_KEY = "02C2BBA0DA1E066EA968C1EB129499F6DEBC5FD82D70D61DCAF691CDB69AF5D8B9" - } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt new file mode 100644 index 0000000000..8c5dba275a --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaConfig.kt @@ -0,0 +1,25 @@ +package com.tangem.data.visa.config + +import com.squareup.moshi.Json + +internal data class VisaConfig( + @Json(name = "testnet") + val testnet: Addresses, + @Json(name = "mainnet") + val mainnet: Addresses, + @Json(name = "txHistoryAPIAdditionalHeaders") + val header: Header, +) { + + data class Addresses( + @Json(name = "paymentAccountRegistry") + val paymentAccountRegistry: String, + @Json(name = "bridgeProcessor") + val bridgeProcessor: String, + ) + + data class Header( + @Json(name = "x-asn") + val xAsn: String, + ) +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt new file mode 100644 index 0000000000..e5062cf662 --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/config/VisaLibLoader.kt @@ -0,0 +1,86 @@ +package com.tangem.data.visa.config + +import com.squareup.moshi.Moshi +import com.tangem.data.visa.BuildConfig +import com.tangem.data.visa.utils.VisaConstants +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.lib.visa.VisaContractInfoProvider +import com.tangem.lib.visa.api.VisaApi +import com.tangem.lib.visa.api.VisaApiBuilder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import javax.inject.Inject + +internal class VisaLibLoader @Inject constructor( + private val assetLoader: AssetLoader, + @NetworkMoshi private val moshi: Moshi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + private val createMutex = Mutex() + + private var config: VisaConfig? = null + + private var provider: VisaContractInfoProvider? = null + private var api: VisaApi? = null + + suspend fun getOrCreateProvider(): VisaContractInfoProvider = provider ?: createProvider() + + suspend fun getOrCreateApi(): VisaApi = api ?: createApi() + + private suspend fun createProvider(): VisaContractInfoProvider = createMutex.withLock { + val config = getOrLoadConfig() + + provider = VisaContractInfoProvider.Builder( + useTestnetRpc = VisaConstants.USE_TEST_ENV, + bridgeProcessorAddress = if (VisaConstants.USE_TEST_ENV) { + config.testnet.bridgeProcessor + } else { + config.mainnet.bridgeProcessor + }, + paymentAccountRegistryAddress = if (VisaConstants.USE_TEST_ENV) { + config.testnet.paymentAccountRegistry + } else { + config.mainnet.paymentAccountRegistry + }, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + dispatchers = dispatchers, + ).build() + + return requireNotNull(provider) { + "Visa provider is not created" + } + } + + private suspend fun createApi(): VisaApi = createMutex.withLock { + val config = getOrLoadConfig() + + api = VisaApiBuilder( + useDevApi = VisaConstants.USE_TEST_ENV, + isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, + moshi = moshi, + headers = mapOf( + X_ASN_HEADER_NAME to config.header.xAsn, + ), + ).build() + + return requireNotNull(api) { + "Visa API is not created" + } + } + + private suspend fun getOrLoadConfig(): VisaConfig { + config = assetLoader.load(VISA_CONFIG_FILE_NAME) + + return requireNotNull(config) { + "Visa config is not found" + } + } + + companion object { + private const val VISA_CONFIG_FILE_NAME = "tangem-app-config/visa_config" + private const val X_ASN_HEADER_NAME = "x-asn" + } +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt index a3022a444a..60d487838e 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/di/ImplementedVisaDataModule.kt @@ -1,15 +1,11 @@ package com.tangem.data.visa.di -import com.squareup.moshi.Moshi import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.visa.BuildConfig import com.tangem.data.visa.DefaultVisaRepository +import com.tangem.data.visa.config.VisaLibLoader import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.visa.repository.VisaRepository -import com.tangem.lib.visa.VisaContractInfoProvider -import com.tangem.lib.visa.api.VisaApiBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -25,29 +21,16 @@ internal object ImplementedVisaDataModule { @Singleton @ImplementedVisaRepository fun provideVisaRepository( - @NetworkMoshi moshi: Moshi, + visaLibLoader: VisaLibLoader, tangemTechApi: TangemTechApi, cacheRegistry: CacheRegistry, userWalletsStore: UserWalletsStore, dispatchers: CoroutineDispatcherProvider, - ): VisaRepository { - val contractInfoProvider = VisaContractInfoProvider.Builder( - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - dispatchers = dispatchers, - ).build() - val visaApi = VisaApiBuilder( - useDevApi = true, - isNetworkLoggingEnabled = BuildConfig.LOG_ENABLED, - moshi = moshi, - ).build() - - return DefaultVisaRepository( - contractInfoProvider, - tangemTechApi, - visaApi, - cacheRegistry, - userWalletsStore, - dispatchers, - ) - } + ): VisaRepository = DefaultVisaRepository( + visaLibLoader, + tangemTechApi, + cacheRegistry, + userWalletsStore, + dispatchers, + ) } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt index b664ad4320..bcc9aeb311 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/CurrencyUtils.kt @@ -7,9 +7,9 @@ import java.util.Currency internal fun findCurrencyByNumericCode(code: Int): Currency { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { Currency.getAvailableCurrencies().firstOrNull { it.numericCode == code } - ?: Currency.getInstance(VisaConfig.fiatCurrency.code) + ?: Currency.getInstance(VisaConstants.fiatCurrency.code) } else { Timber.w("Unable to get currency by numeric code on API level ${Build.VERSION.SDK_INT}") - Currency.getInstance(VisaConfig.fiatCurrency.code) + Currency.getInstance(VisaConstants.fiatCurrency.code) } } \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt deleted file mode 100644 index 6b3e3f3f8b..0000000000 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.data.visa.utils - -import com.tangem.domain.appcurrency.model.AppCurrency - -internal object VisaConfig { - - const val NETWORK_NAME = "Polygon PoS" - - const val TOKEN_ID = "tether" - - val fiatCurrency = AppCurrency( - code = "EUR", - name = "Euro", - symbol = "€", - ) -} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt new file mode 100644 index 0000000000..27cd336588 --- /dev/null +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaConstants.kt @@ -0,0 +1,44 @@ +package com.tangem.data.visa.utils + +import com.tangem.domain.appcurrency.model.AppCurrency + +internal object VisaConstants { + + const val NETWORK_NAME = "Polygon PoS" + + const val TOKEN_ID = "tether" + + val fiatCurrency = AppCurrency( + code = "EUR", + name = "Euro", + symbol = "€", + ) + + /* + * Must be `false` in production + * Don't forget to change CardTypesResolver.isVisaWallet + * */ + const val IS_DEMO_MODE_ENABLED = false + + const val USE_TEST_ENV = true + + const val DEMO_TESTNET_ADDRESS = "0x51d034eb1563d0d2e66379ef37756d3c14936c44" + const val DEMO_TESTNET_PUBLIC_KEY = "03FA1122B809079F79C4E0F657FE11337FEC88C3FB3C6341B2CE2E4F5D9241DD86" + + const val DEMO_MAINNET_ADDRESS = "0x927e3ef2b3d85bacf9e520379f64f6627d323fcd" + const val DEMO_MAINNET_PUBLIC_KEY = "02AC61CD57B8011BEE8BB489FB744845CC113AD379132C56015EE70528B6A88E92" +} + +internal fun getDemoAddress(): String { + return if (VisaConstants.USE_TEST_ENV) { + VisaConstants.DEMO_TESTNET_ADDRESS + } else { + VisaConstants.DEMO_MAINNET_ADDRESS + } +} + +internal fun getDemoPublicKey(): String { + return if (VisaConstants.USE_TEST_ENV) { + VisaConstants.DEMO_TESTNET_PUBLIC_KEY + } else VisaConstants.DEMO_MAINNET_PUBLIC_KEY +} \ No newline at end of file diff --git a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt index 3c9f55e37f..6398dcec84 100644 --- a/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt +++ b/data/visa/src/debug/kotlin/com/tangem/data/visa/utils/VisaCurrencyFactory.kt @@ -21,10 +21,10 @@ internal class VisaCurrencyFactory { return VisaCurrency( symbol = contractInfo.token.symbol, - networkName = VisaConfig.NETWORK_NAME, + networkName = VisaConstants.NETWORK_NAME, decimals = contractInfo.token.decimals, fiatRate = fiatRate, - fiatCurrency = VisaConfig.fiatCurrency, + fiatCurrency = VisaConstants.fiatCurrency, balances = with(contractInfo) { VisaCurrency.Balances( total = balances.total, diff --git a/domain/core/build.gradle.kts b/domain/core/build.gradle.kts index 63325166f7..46d2670d68 100644 --- a/domain/core/build.gradle.kts +++ b/domain/core/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -7,4 +8,6 @@ dependencies { api(deps.kotlin.coroutines) api(deps.arrow.core) api(deps.arrow.fx) + + implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt index 25029586ad..0812a3d523 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt @@ -96,4 +96,15 @@ sealed class Lce { ifContent = ::identity, ifError = { null }, ) + + /** + * Returns the error of this [Lce] if it's a [Lce.Error], `null` otherwise. + * + * @return The error of this [Lce] or `null`. + */ + fun errorOrNull(): E? = fold( + ifLoading = { null }, + ifContent = { null }, + ifError = ::identity, + ) } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt index 90dbc6d172..2040b0774a 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt @@ -2,8 +2,10 @@ package com.tangem.domain.core.lce import arrow.atomic.Atomic import arrow.core.raise.Raise +import arrow.core.raise.RaiseDSL import arrow.core.raise.recover import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import kotlin.experimental.ExperimentalTypeInference @@ -18,18 +20,55 @@ class LceRaise @PublishedApi internal constructor( private val raise: Raise>, ) : Raise> by raise { + /** + * An [Atomic] boolean flag indicating whether a loading operation is in progress. + */ val isLoading: Atomic = Atomic(false) + /** + * Helper function to raise an [Lce.Error] state with the given error object. + * */ + @RaiseDSL + @JvmName(name = "raiseError") + fun raise(r: E): Nothing = raise(r = r.lceError()) + + /** + * Helper function to raise an [Lce.Loading] state. + */ + @RaiseDSL + fun raiseLoading(): Nothing = raise(r = lceLoading()) + + /** + * Execute the [Raise] context function resulting in [C] or any _logical error_ of type [OtherError], + * and transform any raised [OtherError] into [E], which is raised to the outer [Raise]. + * + * @see arrow.core.raise.withError + * */ + @RaiseDSL + @OptIn(ExperimentalTypeInference::class) + inline fun withError( + transform: (OtherError) -> E, + @BuilderInference block: LceRaise.() -> C, + ): C = recover( + block = { block(LceRaise(raise = this@recover)) }, + recover = { error -> + error.fold( + ifLoading = { raiseLoading() }, + ifError = { raise(transform(it)) }, + ifContent = { it }, + ) + }, + ) + /** * Binds the content of this [Lce] instance and handles its state. * If this is a [Lce.Loading] state, sets the [isLoading] flag to true and calls the [ifLoading] function. * If this is a [Lce.Content] state, returns the content. * If this is a [Lce.Error] state, raises the error. * - * @param ifLoading The function to call if this is a [Lce.Loading] state. - * By default, it raises a new [Lce.Loading] state. * @return The content of this [Lce] instance. */ + @RaiseDSL fun Lce.bind(): C = when (this) { is Lce.Loading -> { isLoading.set(true) @@ -48,6 +87,7 @@ class LceRaise @PublishedApi internal constructor( * * @return The content of this [Lce] instance. */ + @RaiseDSL fun Lce.bindOrNull(): C? = when (this) { is Lce.Loading -> { isLoading.set(true) diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt new file mode 100644 index 0000000000..ea7727681f --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigDecimalSerializer.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.core.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.math.BigDecimal + +internal object BigDecimalSerializer : KSerializer { + + override val descriptor = PrimitiveSerialDescriptor(serialName = "BigDecimal", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: BigDecimal) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): BigDecimal { + return BigDecimal(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt new file mode 100644 index 0000000000..3243892b18 --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigDecimal.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.core.serialization + +import kotlinx.serialization.Serializable +import java.math.BigDecimal + +typealias SerializedBigDecimal = @Serializable(with = BigDecimalSerializer::class) BigDecimal \ No newline at end of file diff --git a/domain/feedback/build.gradle.kts b/domain/feedback/build.gradle.kts index 6a21560035..28fe483e00 100644 --- a/domain/feedback/build.gradle.kts +++ b/domain/feedback/build.gradle.kts @@ -13,5 +13,6 @@ dependencies { implementation(deps.jodatime) implementation(projects.core.res) + implementation(projects.domain.models) implementation(projects.domain.wallets.models) } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackManager.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackManager.kt new file mode 100644 index 0000000000..bc18ec66ec --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackManager.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.feedback + +import com.tangem.domain.feedback.models.FeedbackEmailType + +interface FeedbackManager { + + fun sendEmail(type: FeedbackEmailType) +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt new file mode 100644 index 0000000000..9d9fc6995c --- /dev/null +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetCardInfoUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.feedback + +import arrow.core.Either +import arrow.core.Either.Companion.catch +import com.tangem.domain.feedback.models.CardInfo +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.models.scan.ScanResponse + +/** + * UseCase for creating 'CardInfo' + * + * @property feedbackRepository feedback repository + * +[REDACTED_AUTHOR] + */ +class GetCardInfoUseCase( + private val feedbackRepository: FeedbackRepository, +) { + + suspend operator fun invoke(scanResponse: ScanResponse): Either { + return catch { feedbackRepository.getCardInfo(scanResponse) } + } +} \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt index 50998a4ed8..acdcc492b7 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/GetFeedbackEmailUseCase.kt @@ -25,23 +25,21 @@ class GetFeedbackEmailUseCase( private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository) suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail { - val cardInfo = feedbackRepository.getCardInfo() - val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs()) return FeedbackEmail( - address = getAddress(cardInfo), - subject = emailSubjectResolver.resolve(type, cardInfo), - message = createMessage(type, cardInfo), + address = getAddress(type.cardInfo), + subject = emailSubjectResolver.resolve(type), + message = createMessage(type), file = feedbackRepository.createLogFile(logs = formattedLogs), ) } - private fun getAddress(cardInfo: CardInfo): String { - return if (cardInfo.isStart2Coin) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL + private fun getAddress(cardInfo: CardInfo?): String { + return if (cardInfo?.isStart2Coin == true) START2COIN_SUPPORT_EMAIL else TANGEM_SUPPORT_EMAIL } - private suspend fun createMessage(type: FeedbackEmailType, cardInfo: CardInfo): String { + private suspend fun createMessage(type: FeedbackEmailType): String { return StringBuilder().apply { val title = emailMessageTitleResolver.resolve(type) append(title) @@ -50,9 +48,7 @@ class GetFeedbackEmailUseCase( appendDisclaimerIfNeeded(type) - skipLine() - - val body = emailMessageBodyResolver.resolve(type, cardInfo) + val body = emailMessageBodyResolver.resolve(type) append(body) }.toString() } @@ -62,6 +58,7 @@ class GetFeedbackEmailUseCase( this } else { append(resources.getString(R.string.feedback_data_collection_message)) + skipLine() } } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt index 7ae84ac608..2e7e710cc1 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/CardInfo.kt @@ -1,6 +1,9 @@ package com.tangem.domain.feedback.models +import com.tangem.domain.wallets.models.UserWalletId + data class CardInfo( + val userWalletId: UserWalletId?, val cardId: String, val firmwareVersion: String, val cardBlockchain: String?, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 6bbd5a9670..1e4971e4cf 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -7,15 +7,19 @@ package com.tangem.domain.feedback.models */ sealed interface FeedbackEmailType { + val cardInfo: CardInfo? + /** User initiate request yourself. Example, button on DetailsScreen or OnboardingScreen */ - data object DirectUserRequest : FeedbackEmailType + data class DirectUserRequest(override val cardInfo: CardInfo) : FeedbackEmailType /** User rate the app as "can be better" */ - data object RateCanBeBetter : FeedbackEmailType + data class RateCanBeBetter(override val cardInfo: CardInfo) : FeedbackEmailType /** User has problem with scanning */ - data object ScanningProblem : FeedbackEmailType + data object ScanningProblem : FeedbackEmailType { + override val cardInfo: CardInfo? = null + } /** User has problem with sending transaction */ - data object TransactionSendingProblem : FeedbackEmailType + data class TransactionSendingProblem(override val cardInfo: CardInfo) : FeedbackEmailType } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index c9d6806eea..1b5de9c70c 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -1,23 +1,29 @@ package com.tangem.domain.feedback.repository import com.tangem.domain.feedback.models.* +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.models.UserWalletId import java.io.File interface FeedbackRepository { - suspend fun getUserWalletsInfo(): UserWalletsInfo + suspend fun getCardInfo(scanResponse: ScanResponse): CardInfo - suspend fun getCardInfo(): CardInfo + suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo - suspend fun getBlockchainInfoList(): List - - suspend fun getBlockchainInfo(blockchainId: String, derivationPath: String?): BlockchainInfo? + suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List fun getPhoneInfo(): PhoneInfo + suspend fun getBlockchainInfo( + userWalletId: UserWalletId, + blockchainId: String, + derivationPath: String?, + ): BlockchainInfo? + fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) - suspend fun getBlockchainErrorInfo(): BlockchainErrorInfo? + suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? suspend fun getAppLogs(): List diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index 8574fb1010..a9bb1a64dd 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -16,25 +16,33 @@ internal class EmailMessageBodyResolver( private val feedbackRepository: FeedbackRepository, ) { - /** Resolve email message body by [type] using [cardInfo] */ - suspend fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String = with(FeedbackDataBuilder()) { + /** Resolve email message body by [type] */ + suspend fun resolve(type: FeedbackEmailType): String = with(FeedbackDataBuilder()) { when (type) { - FeedbackEmailType.DirectUserRequest -> addUserRequestBody(cardInfo) - FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(cardInfo) - FeedbackEmailType.ScanningProblem -> addScanningProblemBody() - FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(cardInfo) + is FeedbackEmailType.DirectUserRequest -> addUserRequestBody(type.cardInfo) + is FeedbackEmailType.RateCanBeBetter -> addCardAndPhoneInfo(type.cardInfo) + is FeedbackEmailType.ScanningProblem -> addScanningProblemBody() + is FeedbackEmailType.TransactionSendingProblem -> addTransactionSendingProblemBody(type.cardInfo) } return build() } private suspend fun FeedbackDataBuilder.addUserRequestBody(cardInfo: CardInfo) { - addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo()) + addUserWalletsInfo(userWalletsInfo = feedbackRepository.getUserWalletsInfo(cardInfo.userWalletId)) addDelimiter() addCardInfo(cardInfo) addDelimiter() - addBlockchainInfoList(blockchainInfoList = feedbackRepository.getBlockchainInfoList()) - addDelimiter() + + if (cardInfo.userWalletId != null) { + val blockchainInfoList = feedbackRepository.getBlockchainInfoList(cardInfo.userWalletId) + + if (blockchainInfoList.isNotEmpty()) { + addBlockchainInfoList(blockchainInfoList = blockchainInfoList) + addDelimiter() + } + } + addPhoneInfo(phoneInfo = feedbackRepository.getPhoneInfo()) } @@ -46,9 +54,11 @@ internal class EmailMessageBodyResolver( addCardInfo(cardInfo) addDelimiter() - val blockchainError = feedbackRepository.getBlockchainErrorInfo() + val userWalletId = requireNotNull(cardInfo.userWalletId) { "UserWalletId must be not null" } + val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) val blockchainInfo = blockchainError?.let { feedbackRepository.getBlockchainInfo( + userWalletId = userWalletId, blockchainId = blockchainError.blockchainId, derivationPath = blockchainError.derivationPath, ) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 38c983c617..6ab001b775 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -16,10 +16,10 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { /** Resolve email message title by [type] */ fun resolve(type: FeedbackEmailType): String { return when (type) { - FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support - FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative - FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed - FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed + is FeedbackEmailType.DirectUserRequest -> R.string.feedback_preface_support + is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_preface_rate_negative + is FeedbackEmailType.ScanningProblem -> R.string.feedback_preface_scan_failed + is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_preface_tx_failed } .let(resources::getString) } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index 392fa7d2f4..beb89739bd 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -2,7 +2,6 @@ package com.tangem.domain.feedback.utils import android.content.res.Resources import com.tangem.domain.feedback.R -import com.tangem.domain.feedback.models.CardInfo import com.tangem.domain.feedback.models.FeedbackEmailType /** @@ -14,19 +13,19 @@ import com.tangem.domain.feedback.models.FeedbackEmailType */ internal class EmailSubjectResolver(private val resources: Resources) { - /** Resolve email message body by [type] using [cardInfo] */ - fun resolve(type: FeedbackEmailType, cardInfo: CardInfo): String { + /** Resolve email message body by [type] */ + fun resolve(type: FeedbackEmailType): String { return when (type) { - FeedbackEmailType.DirectUserRequest -> { - if (cardInfo.isStart2Coin) { + is FeedbackEmailType.DirectUserRequest -> { + if (type.cardInfo.isStart2Coin) { R.string.feedback_subject_support } else { R.string.feedback_subject_support_tangem } } - FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative - FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed - FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed + is FeedbackEmailType.RateCanBeBetter -> R.string.feedback_subject_rate_negative + is FeedbackEmailType.ScanningProblem -> R.string.feedback_subject_scan_failed + is FeedbackEmailType.TransactionSendingProblem -> R.string.feedback_subject_tx_failed } .let(resources::getString) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 9373ae2a5b..9dbf3c435b 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -19,6 +19,6 @@ object NetworkLogConfig { } object AnalyticsHandlersLogConfig { - const val firebase: Boolean = false + val firebase: Boolean = BuildConfig.LOG_ENABLED val amplitude: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt index 3e0b022afd..68c6988923 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt @@ -8,7 +8,7 @@ import java.math.BigDecimal sealed interface LegacyAction : Action { - object SendEmailRateCanBeBetter : LegacyAction + data class SendEmailRateCanBeBetter(val scanResponse: ScanResponse) : LegacyAction /** * Initiate an onboarding process. @@ -34,5 +34,6 @@ sealed interface LegacyAction : Action { val fee: BigDecimal?, val destinationAddress: String?, val errorMessage: String, + val scanResponse: ScanResponse, ) : LegacyAction } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt index 80e03d9347..afa5e3bf04 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt @@ -11,5 +11,5 @@ interface ReduxStateHolder { suspend fun onUserWalletSelected(userWallet: UserWallet) - fun sendFeedbackEmail() + fun dispatchDialogShow(dialog: StateDialog) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt new file mode 100644 index 0000000000..e4beb677bd --- /dev/null +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.redux + +interface StateDialog { + + data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog + + enum class ScanFailsSource { + MAIN, SIGN_IN, SETTINGS, INTRO; + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 25dae1f2dd..68f4060640 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -45,7 +45,7 @@ import timber.log.Timber import java.math.BigDecimal import java.util.EnumSet -@Suppress("LargeClass", "TooManyFunctions", "LongParameterList") +@Suppress("LargeClass", "TooManyFunctions") // FIXME: Move to its own module and make internal @Deprecated("Inject the WalletManagerFacade interface using DI instead") class DefaultWalletManagersFacade( @@ -474,7 +474,7 @@ class DefaultWalletManagersFacade( amount: Amount, userWalletId: UserWalletId, network: Network, - ): Result? { + ): Result? = withContext(dispatchers.io) { val blockchain = Blockchain.fromId(network.id.value) val walletManager = getOrCreateWalletManager( userWalletId = userWalletId, @@ -484,7 +484,7 @@ class DefaultWalletManagersFacade( val destination = estimationFeeAddressFactory.makeAddress(blockchain) - return (walletManager as? TransactionSender)?.estimateFee( + (walletManager as? TransactionSender)?.estimateFee( amount = amount, destination = destination, ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt index cd756efd3c..22ae5adecc 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt @@ -17,9 +17,9 @@ import java.math.BigDecimal internal class TransactionDataToTxHistoryItemConverter( private val walletAddresses: Set
, private val feePaidCurrency: FeePaidCurrency, -) : Converter { +) : Converter { - override fun convert(value: TransactionData): TxHistoryItem? { + override fun convert(value: TransactionData.Uncompiled): TxHistoryItem? { val hash = value.hash ?: return null val millis = value.date?.timeInMillis ?: return null val amount = getTransactionAmountValue(value.amount, value.fee?.amount) ?: return null diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt index 84b194166b..df5aa57530 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/UpdateWalletManagerResultFactory.kt @@ -90,7 +90,7 @@ internal class UpdateWalletManagerResultFactory { private fun getCurrentTransactions( txHistoryItemConverter: TransactionDataToTxHistoryItemConverter, - recentTransactions: Set, + recentTransactions: Set, ): Set { val unconfirmedTransactions = recentTransactions.filter { it.status == TransactionStatus.Unconfirmed @@ -122,7 +122,7 @@ internal class UpdateWalletManagerResultFactory { private fun createCurrencyTransaction( txHistoryItemConverter: TransactionDataToTxHistoryItemConverter, - data: TransactionData, + data: TransactionData.Uncompiled, ): CryptoCurrencyTransaction? { return when (val type = data.amount.type) { is AmountType.Coin -> { diff --git a/domain/markets/.gitignore b/domain/markets/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/markets/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts new file mode 100644 index 0000000000..5529922be2 --- /dev/null +++ b/domain/markets/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.domain.markets" +} + + +dependencies { + api(projects.domain.markets.models) + api(projects.domain.core) + api(projects.core.pagination) + + implementation(deps.kotlin.serialization) + implementation(projects.domain.tokens.models) +} \ No newline at end of file diff --git a/domain/markets/models/.gitignore b/domain/markets/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/markets/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/markets/models/build.gradle.kts b/domain/markets/models/build.gradle.kts new file mode 100644 index 0000000000..7558c5ca61 --- /dev/null +++ b/domain/markets/models/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(projects.domain.core) + + implementation(deps.kotlin.serialization) + implementation(deps.jodatime) +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt new file mode 100644 index 0000000000..ff9b0ba89a --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/PriceChangeInterval.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.markets + +enum class PriceChangeInterval { + H24, WEEK, MONTH, MONTH3, MONTH6, YEAR, ALL_TIME +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt new file mode 100644 index 0000000000..d239ae395a --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenChart.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenChart( + val interval: PriceChangeInterval, + val priceY: List, + val timeStamp: List, +) { + init { + require(priceY.size == timeStamp.size) + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt new file mode 100644 index 0000000000..c9587db1f9 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenMarket( + val id: String, + val name: String, + val symbol: String, + val marketRating: Int?, + val marketCap: BigDecimal?, + val tokenQuotes: TokenQuotes, + val tokenCharts: Charts, + private val imageHost: String, +) { + + data class Charts( + val h24: TokenChart?, + val week: TokenChart?, + val month: TokenChart?, + ) + + // 25x25 + val imageUrlThumb = + "$imageHost/:thumb/:$id.png" + + // 50x50 + val imageUrlSmall = + "$imageHost/:small/:$id.png" + + // 250x250 + val imageUrlLarge = + "$imageHost/:large/:$id.png" +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt new file mode 100644 index 0000000000..ad952091b6 --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.markets + +data class TokenMarketListConfig( + val fiatPriceCurrency: String, + val searchText: String?, + val showUnder100kMarketCapTokens: Boolean, + val priceChangeInterval: Interval, + val order: Order, +) { + + enum class Order { + ByRating, Trending, Buyers, TopGainers, TopLosers + } + + enum class Interval { + H24, WEEK, MONTH, + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt new file mode 100644 index 0000000000..bcac52fddb --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketUpdateRequest.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.markets + +sealed class TokenMarketUpdateRequest { + + data class UpdateQuotes( + val currencyId: String, + ) : TokenMarketUpdateRequest() + + data class UpdateChart( + val interval: PriceChangeInterval, + val currency: String, + ) : TokenMarketUpdateRequest() +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt new file mode 100644 index 0000000000..e26b6abb9f --- /dev/null +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenQuotes.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.markets + +import java.math.BigDecimal + +data class TokenQuotes( + val currentPrice: BigDecimal, + val priceChanges: Map, +) \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt new file mode 100644 index 0000000000..97aeb80c43 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/repositories/MarketsTokenRepository.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.markets.repositories + +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.TokenMarketUpdateRequest +import com.tangem.pagination.BatchFlow +import com.tangem.pagination.BatchingContext + +interface MarketsTokenRepository { + + suspend fun getTokenListFlow( + batchingContext: BatchingContext, + ): BatchFlow, TokenMarketUpdateRequest> +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt new file mode 100644 index 0000000000..bf300cb779 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/DelayPermissionRequestUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class DelayPermissionRequestUseCase( + private val repository: PermissionRepository, +) { + + suspend operator fun invoke(permission: String): Either = Either.catch { + repository.delayPermissionAsking(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt new file mode 100644 index 0000000000..000bfd7ed1 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/IsFirstTimeAskingPermissionUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class IsFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { + + suspend operator fun invoke(permission: String): Either = Either.catch { + repository.isFirstTimeAskingPermission(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt new file mode 100644 index 0000000000..84d654dc42 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/NeverRequestPermissionUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class NeverRequestPermissionUseCase( + private val repository: PermissionRepository, +) { + + suspend operator fun invoke(permission: String): Either = Either.catch { + repository.neverAskPermission(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt new file mode 100644 index 0000000000..fa5d057e77 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/SetFirstTimeAskingPermissionUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class SetFirstTimeAskingPermissionUseCase(private val repository: PermissionRepository) { + + suspend operator fun invoke(permission: String): Either = Either.catch { + repository.setFirstTimeAskingPermission(permission, false) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt new file mode 100644 index 0000000000..44f063119a --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldAskPermissionUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.PermissionRepository + +class ShouldAskPermissionUseCase( + private val repository: PermissionRepository, +) { + + suspend operator fun invoke(permission: String): Boolean = repository.shouldAskPermission(permission) +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt new file mode 100644 index 0000000000..b7d5a6f1ca --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldInitiallyAskPermissionUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import arrow.core.Either +import com.tangem.domain.settings.repositories.PermissionRepository + +class ShouldInitiallyAskPermissionUseCase(private val repository: PermissionRepository) { + + suspend operator fun invoke(permission: String): Either = Either.catch { + repository.shouldInitiallyShowPermissionScreen(permission) + } +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt new file mode 100644 index 0000000000..fd136cf651 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/PermissionRepository.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.settings.repositories + +interface PermissionRepository { + + /** + * Return true if should display screen ONCE asking to allow [permission]. + * False otherwise or screen already was displayed + */ + suspend fun shouldInitiallyShowPermissionScreen(permission: String): Boolean + + /** + * Indicates which time [permission] was asked via platform dialog. + * NOTE: Use this method to indicate either reroute to settings or display platform dialog. + */ + suspend fun isFirstTimeAskingPermission(permission: String): Boolean + + /** + * Sets value indicating that [permission] was asked via platform dialog. + * NOTE: Use this method to indicate either reroute to settings or display platform dialog. + */ + suspend fun setFirstTimeAskingPermission(permission: String, value: Boolean) + + /** + * Is clear to ask [permission]. + * User could already granted or permanently denied permission + * Or there is an active delay before next request + */ + suspend fun shouldAskPermission(permission: String): Boolean + + /** + * Permanently deny [permission] and never request again + */ + suspend fun neverAskPermission(permission: String) + + /** + * Delay next [permission] request for some time or active sessions + */ + suspend fun delayPermissionAsking(permission: String) +} \ No newline at end of file diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index f6a93898bb..e92bc23b4c 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -1,9 +1,23 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) id("configuration") } +android { + namespace = "com.tangem.domain.staking" +} + + dependencies { - implementation(deps.kotlin.coroutines) - implementation(deps.arrow.core) + api(projects.domain.staking.models) + + api(projects.domain.core) + implementation(deps.kotlin.serialization) + + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + implementation(projects.features.staking.api) } \ No newline at end of file diff --git a/domain/staking/models/.gitignore b/domain/staking/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/staking/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/staking/models/build.gradle.kts b/domain/staking/models/build.gradle.kts new file mode 100644 index 0000000000..7558c5ca61 --- /dev/null +++ b/domain/staking/models/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(projects.domain.core) + + implementation(deps.kotlin.serialization) + implementation(deps.jodatime) +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt new file mode 100644 index 0000000000..4359b0b39c --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/NetworkType.kt @@ -0,0 +1,71 @@ +package com.tangem.domain.staking.model + +enum class NetworkType { + AVALANCHE_C, + AVALANCHE_ATOMIC, + AVALANCHE_P, + ARBITRUM, + BINANCE, + CELO, + ETHEREUM, + ETHEREUM_GOERLI, + ETHEREUM_HOLESKY, + FANTOM, + HARMONY, + OPTIMISM, + POLYGON, + GNOSIS, + MOONRIVER, + OKC, + ZKSYNC, + VICTION, + AGORIC, + AKASH, + AXELAR, + BAND_PROTOCOL, + BITSONG, + CANTO, + CHIHUAHUA, + COMDEX, + COREUM, + COSMOS, + CRESCENT, + CRONOS, + CUDOS, + DESMOS, + DYDX, + EVMOS, + FETCH_AI, + GRAVITY_BRIDGE, + INJECTIVE, + IRISNET, + JUNO, + KAVA, + KI_NETWORK, + MARS_PROTOCOL, + NYM, + OKEX_CHAIN, + ONOMY, + OSMOSIS, + PERSISTENCE, + QUICKSILVER, + REGEN, + SECRET, + SENTINEL, + SOMMELIER, + STAFI, + STARGAZE, + STRIDE, + TERITORI, + TGRADE, + UMEE, + POLKADOT, + KUSAMA, + WESTEND, + BINANCEBEACON, + NEAR, + SOLANA, + TEZOS, + TRON, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt new file mode 100644 index 0000000000..023b7ba52b --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/Yield.kt @@ -0,0 +1,118 @@ +package com.tangem.domain.staking.model + +import com.tangem.domain.core.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +@Serializable +data class Yield( + val id: String, + val token: Token, + val tokens: List, + val args: Args, + val status: Status, + val apy: SerializedBigDecimal, + val rewardRate: Double, + val rewardType: RewardType, + val metadata: Metadata, + val validators: List, + val isAvailable: Boolean, +) { + + @Serializable + data class Status( + val enter: Boolean, + val exit: Boolean?, + ) + + @Serializable + data class Args( + val enter: Enter, + val exit: Enter?, + ) { + + @Serializable + data class Enter( + val addresses: Addresses, + val args: Map, + ) { + + @Serializable + data class Addresses( + val address: AddressArgument, + val additionalAddresses: Map? = null, + ) + } + } + + @Serializable + data class Validator( + val address: String, + val status: String, + val name: String, + val image: String?, + val website: String?, + val apr: SerializedBigDecimal?, + val commission: Double?, + val stakedBalance: String?, + val votingPower: Double?, + val preferred: Boolean, + ) + + @Serializable + data class Metadata( + val name: String, + val logoUri: String, + val description: String, + val documentation: String?, + val gasFeeToken: Token, + val token: Token, + val tokens: List, + val type: String, + val rewardSchedule: String, + val cooldownPeriod: Period, + val warmupPeriod: Period, + val rewardClaiming: String, + val defaultValidator: String?, + val minimumStake: Int?, + val supportsMultipleValidators: Boolean, + val revshare: Enabled, + val fee: Enabled, + ) { + + @Serializable + data class Period( + val days: Int, + ) + + @Serializable + data class Enabled( + val enabled: Boolean, + ) + } + + enum class RewardType { + APY, // compound rate + APR, // simple rate + UNKNOWN, + } +} + +@Serializable +data class Token( + val name: String, + val network: NetworkType, + val symbol: String, + val decimals: Int, + val address: String?, + val coinGeckoId: String?, + val logoURI: String?, + val isPoints: Boolean?, +) + +@Serializable +data class AddressArgument( + val required: Boolean, + val network: String? = null, + val minimum: Double? = null, + val maximum: Double? = null, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt new file mode 100644 index 0000000000..6ed53d2486 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalance.kt @@ -0,0 +1,51 @@ +package com.tangem.domain.staking.model + +import java.math.BigDecimal + +sealed class YieldBalance { + + data class Data( + val balance: YieldBalanceItem, + ) : YieldBalance() { + fun getTotalStakingBalance(): BigDecimal { + return balance.items + .filterNot { it.type == BalanceType.REWARDS } + .sumOf { it.amount * it.pricePerShare } + } + + fun getRewardStakingBalance(): BigDecimal { + return balance.items + .filter { it.type == BalanceType.REWARDS } + .sumOf { it.amount * it.pricePerShare } + } + } + + data object Empty : YieldBalance() + + data object Error : YieldBalance() +} + +data class YieldBalanceItem( + val items: List, + val integrationId: String?, +) + +data class BalanceItem( + val type: BalanceType, + val amount: BigDecimal, + val pricePerShare: BigDecimal, + val rawCurrencyId: String?, + val validatorAddress: String?, +) + +enum class BalanceType { + AVAILABLE, + STAKED, + UNSTAKING, + UNSTAKED, + PREPARING, + REWARDS, + LOCKED, + UNLOCKING, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt new file mode 100644 index 0000000000..c75d5c9f10 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/YieldBalanceList.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.staking.model + +sealed class YieldBalanceList { + + data class Data( + val balances: List, + ) : YieldBalanceList() { + fun getBalance(rawCurrencyId: String?): YieldBalance { + return balances.firstOrNull { yield -> + (yield as? YieldBalance.Data)?.balance?.items + ?.any { it.rawCurrencyId == rawCurrencyId } == true + } ?: YieldBalance.Error + } + } + + data object Empty : YieldBalanceList() + + data object Error : YieldBalanceList() +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt new file mode 100644 index 0000000000..c8b8a639f5 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/EnterAction.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.model.action + +import com.tangem.domain.staking.model.transaction.StakingTransaction +import org.joda.time.DateTime +import java.math.BigDecimal + +data class EnterAction( + val id: String, + val integrationId: String, + val status: StakingActionStatus, + val type: StakingActionType, + val currentStepIndex: Int, + val amount: BigDecimal, + val validatorAddress: String?, + val validatorAddresses: List?, + val transactions: List?, + val createdAt: DateTime, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt new file mode 100644 index 0000000000..a3e5da8836 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionStatus.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.staking.model.action + +enum class StakingActionStatus { + CANCELED, + CREATED, + WAITING_FOR_NEXT, + PROCESSING, + FAILED, + SUCCESS, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt new file mode 100644 index 0000000000..7346957e59 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/action/StakingActionType.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.staking.model.action + +enum class StakingActionType { + STAKE, + UNSTAKE, + CLAIM_REWARDS, + RESTAKE_REWARDS, + WITHDRAW, + RESTAKE, + CLAIM_UNSTAKED, + UNLOCK_LOCKED, + STAKE_LOCKED, + VOTE, + REVOKE, + VOTE_LOCKED, + REVOTE, + REBOND, + MIGRATE, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt new file mode 100644 index 0000000000..292adc35d5 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingGasEstimate.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.staking.model.transaction + +import com.tangem.domain.staking.model.Token +import java.math.BigDecimal + +data class StakingGasEstimate( + val amount: BigDecimal, + val token: Token, + val gasLimit: String?, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt new file mode 100644 index 0000000000..0e937d1243 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransaction.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.staking.model.transaction + +import com.tangem.domain.staking.model.NetworkType + +data class StakingTransaction( + val id: String, + val network: NetworkType, + val status: StakingTransactionStatus, + val type: StakingTransactionType, + val hash: String?, + val signedTransaction: String?, + val unsignedTransaction: String?, + val stepIndex: Int, + val error: String?, + val gasEstimate: StakingGasEstimate?, + val stakeId: String?, + val explorerUrl: String?, + val ledgerHwAppId: String?, + val isMessage: Boolean, +) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt new file mode 100644 index 0000000000..9fc6d4464f --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionStatus.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.staking.model.transaction + +enum class StakingTransactionStatus { + NOT_FOUND, + CREATED, + BLOCKED, + WAITING_FOR_SIGNATURE, + SIGNED, + BROADCASTED, + PENDING, + CONFIRMED, + FAILED, + SKIPPED, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt new file mode 100644 index 0000000000..cb52db3b39 --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/transaction/StakingTransactionType.kt @@ -0,0 +1,44 @@ +package com.tangem.domain.staking.model.transaction + +enum class StakingTransactionType { + SWAP, + DEPOSIT, + APPROVAL, + STAKE, + CLAIM_UNSTAKED, + CLAIM_REWARDS, + RESTAKE_REWARDS, + UNSTAKE, + SPLIT, + MERGE, + LOCK, + UNLOCK, + SUPPLY, + BRIDGE, + VOTE, + REVOKE, + RESTAKE, + REBOND, + WITHDRAW, + CREATE_ACCOUNT, + REVEAL, + MIGRATE, + UTXO_P_TO_C_IMPORT, + UTXO_C_TO_P_IMPORT, + UNFREEZE_LEGACY, + UNFREEZE_LEGACY_BANDWIDTH, + UNFREEZE_LEGACY_ENERGY, + UNFREEZE_BANDWIDTH, + UNFREEZE_ENERGY, + FREEZE_BANDWIDTH, + FREEZE_ENERGY, + UNDELEGATE_BANDWIDTH, + UNDELEGATE_ENERGY, + P2P_NODE_REQUEST, + LUGANODES_PROVISION, + LUGANODES_EXIT_REQUEST, + INFSTONES_PROVISION, + INFSTONES_EXIT_REQUEST, + INFSTONES_CLAIM_REQUEST, + UNKNOWN, +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt new file mode 100644 index 0000000000..47e9960974 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingTokensUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.staking.error.StakingTokensError +import com.tangem.domain.staking.repositories.StakingRepository + +/** + * Use case for getting enabled tokens + */ +class FetchStakingTokensUseCase( + private val stakingRepository: StakingRepository, +) { + suspend operator fun invoke(isRefresh: Boolean = false): Either { + return either { + catch( + block = { stakingRepository.fetchEnabledYields(isRefresh) }, + catch = { StakingTokensError.DataError(it) }, + ) + } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt new file mode 100644 index 0000000000..525fd569cb --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.wallets.models.UserWalletId + +class FetchStakingYieldBalanceUseCase( + private val stakingRepository: StakingRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + refresh: Boolean = false, + ): Either = Either.catch { + stakingRepository.fetchSingleYieldBalance( + userWalletId = userWalletId, + address = address, + refresh = refresh, + ) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt index 18717a31f7..2cc658f615 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingAvailabilityUseCase.kt @@ -2,15 +2,16 @@ package com.tangem.domain.staking import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency /** - * Use case for getting info about staking availability for certain blockchain. + * Use case for getting info about staking capability in tangem app. */ class GetStakingAvailabilityUseCase( private val stakingRepository: StakingRepository, ) { - operator fun invoke(blockchainNetworkId: String): StakingAvailability { - return stakingRepository.getStakingAvailability(blockchainNetworkId) + suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingAvailability { + return stakingRepository.getStakingAvailabilityForActions(cryptoCurrencyId, symbol) } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt new file mode 100644 index 0000000000..f2e7c1440a --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingYieldBalanceUseCase.kt @@ -0,0 +1,28 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +class GetStakingYieldBalanceUseCase( + private val stakingRepository: StakingRepository, +) { + + operator fun invoke( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + ): EitherFlow { + return stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + address = address, + ).map> { it.right() } + .catch { emit(it.left()) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt new file mode 100644 index 0000000000..a63dc81cdc --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetYieldUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency + +/** + * Use case for getting staking yield for staking scenario start. + */ +class GetYieldUseCase(private val stakingRepository: StakingRepository) { + + suspend operator fun invoke(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Either { + return Either.catch { stakingRepository.getYield(cryptoCurrencyId, symbol) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt new file mode 100644 index 0000000000..a12b75f7ae --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InitializeStakingProcessUseCase.kt @@ -0,0 +1,45 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.Token +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.repositories.StakingRepository +import kotlinx.coroutines.delay +import java.math.BigDecimal + +/** + * Use case for creating enter action + */ +class InitializeStakingProcessUseCase(private val stakingRepository: StakingRepository) { + + suspend operator fun invoke( + integrationId: String, + amount: BigDecimal, + address: String, + validatorAddress: String, + token: Token, + ): Either> { + return Either.catch { + val createAction = stakingRepository.createEnterAction( + integrationId = integrationId, + amount = amount, + address = address, + validatorAddress = validatorAddress, + token = token, + ) + + // workaround, sometimes transaction is not created immediately after actions/enter + delay(PATCH_TRANSACTION_REQUEST_DELAY) + + val createdTransaction = createAction.transactions?.get(0) ?: error("No available transaction to patch") + val patchedTransaction = stakingRepository.constructTransaction(createdTransaction.id) + + createAction to patchedTransaction + } + } + + companion object { + private const val PATCH_TRANSACTION_REQUEST_DELAY = 1000L + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt b/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt new file mode 100644 index 0000000000..d97a592cbd --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/error/StakingTokensError.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.staking.error + +sealed class StakingTokensError { + + data class DataError(val cause: Throwable) : StakingTokensError() +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt index bf40ee3b58..d7eaa306e0 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingAvailability.kt @@ -5,4 +5,6 @@ sealed class StakingAvailability { data class Available(val integrationId: String) : StakingAvailability() data object Unavailable : StakingAvailability() + + data object TemporaryDisabled : StakingAvailability() } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt new file mode 100644 index 0000000000..5da29043d1 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingToken.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.staking.model + +data class StakingToken( + val name: String, + val symbol: String, + val decimals: Int, + val contractAddress: String?, + val coinGeckoId: String?, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt new file mode 100644 index 0000000000..833cb6ae49 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingTokenWithYield.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.staking.model + +data class StakingTokenWithYield( + val token: StakingToken, + val availableYieldIds: List, +) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 763377ac5e..854aa92a93 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -2,10 +2,71 @@ package com.tangem.domain.staking.repositories import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.Token +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.transaction.StakingTransaction +import java.math.BigDecimal +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.staking.model.* +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow interface StakingRepository { - fun getStakingAvailability(blockchainId: String): StakingAvailability + fun isStakingSupported(currencyId: String): Boolean + + suspend fun fetchEnabledYields(refresh: Boolean) suspend fun getEntryInfo(integrationId: String): StakingEntryInfo + + suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield + + suspend fun getStakingAvailabilityForActions( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): StakingAvailability + + suspend fun fetchSingleYieldBalance( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + refresh: Boolean = false, + ) + + fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, address: CryptoCurrencyAddress): Flow + + suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, address: CryptoCurrencyAddress): YieldBalance + + suspend fun fetchMultiYieldBalance( + userWalletId: UserWalletId, + addresses: List, + refresh: Boolean = false, + ) + + fun getMultiYieldBalanceFlow( + userWalletId: UserWalletId, + addresses: List, + ): Flow + + fun getMultiYieldBalanceLce( + userWalletId: UserWalletId, + addresses: List, + ): LceFlow + + suspend fun getMultiYieldBalanceSync( + userWalletId: UserWalletId, + addresses: List, + ): YieldBalanceList + + suspend fun createEnterAction( + integrationId: String, + amount: BigDecimal, + address: String, + validatorAddress: String, + token: Token, + ): EnterAction + + suspend fun constructTransaction(transactionId: String): StakingTransaction } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index de3ef835c5..989a7e9c5d 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { api(projects.domain.core) implementation(projects.domain.models) implementation(projects.domain.legacy) + implementation(projects.domain.staking) implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) @@ -23,7 +24,10 @@ dependencies { implementation(projects.domain.settings) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + + /** Project - Api */ implementation(projects.features.send.api) + implementation(projects.features.staking.api) /** Project - Other */ implementation(projects.core.utils) diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index 948bad320b..c51f155acf 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) - id("kotlin-parcelize") + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -10,11 +10,20 @@ android { } dependencies { - implementation(projects.domain.txhistory.models) + /** Project - Core */ implementation(projects.core.analytics.models) + + /** Project - Domain */ + implementation(projects.domain.txhistory.models) + implementation(projects.domain.staking.models) + + /** SDK dependencies */ implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } + + /** Other dependencies */ + implementation(deps.kotlin.serialization) implementation(deps.jodatime) implementation(deps.timber) } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt index db909d08f3..a0468f3100 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrency.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable /** * Represents a generic cryptocurrency. @@ -14,8 +13,8 @@ import kotlinx.parcelize.Parcelize * @property iconUrl Optional URL of the cryptocurrency icon. `null` if not found. * @property isCustom Indicates whether the currency is a custom user-added currency or not. */ -@Parcelize -sealed class CryptoCurrency : Parcelable { +@Serializable +sealed class CryptoCurrency { abstract val id: ID abstract val network: Network @@ -28,6 +27,7 @@ sealed class CryptoCurrency : Parcelable { /** * Represents a native coin in the blockchain network. */ + @Serializable data class Coin( override val id: ID, override val network: Network, @@ -48,6 +48,7 @@ sealed class CryptoCurrency : Parcelable { * * @property contractAddress Address of the contract managing the token. */ + @Serializable data class Token( override val id: ID, override val network: Network, @@ -75,12 +76,12 @@ sealed class CryptoCurrency : Parcelable { * @property rawCurrencyId Represents not unique currency ID from the blockchain network. `null` if * its ID of the custom token. */ - @Parcelize + @Serializable data class ID( private val prefix: Prefix, private val body: Body, private val suffix: Suffix, - ) : Parcelable { + ) { val value: String get() = buildString { @@ -121,12 +122,13 @@ sealed class CryptoCurrency : Parcelable { * * The body can be either a raw network ID or a raw network ID with a network derivation path. */ - @Parcelize - sealed class Body : Parcelable { + @Serializable + sealed class Body { /** The value of the body. */ abstract val value: String + @Serializable /** Represents a raw network ID. */ data class NetworkId(val rawId: String) : Body() { override val value: String get() = rawId @@ -137,6 +139,7 @@ sealed class CryptoCurrency : Parcelable { * * Should be used for a cryptocurrencies with custom derivation path. * */ + @Serializable data class NetworkIdWithDerivationPath( val rawId: String, val derivationPath: String, @@ -155,13 +158,14 @@ sealed class CryptoCurrency : Parcelable { * * The suffix can either be a raw ID or a contract address. */ - @Parcelize - sealed class Suffix : Parcelable { + @Serializable + sealed class Suffix { /** The value of the suffix, which could be either a raw ID or a contract address. */ abstract val value: String /** Represents a raw ID suffix. */ + @Serializable data class RawID(val rawId: String, val contractAddress: String? = null) : Suffix() { override val value: String get() = buildString { @@ -174,6 +178,7 @@ sealed class CryptoCurrency : Parcelable { } /** Represents a contract address suffix. */ + @Serializable data class ContractAddress(val contractAddress: String) : Suffix() { override val value: String get() = contractAddress } @@ -183,11 +188,11 @@ sealed class CryptoCurrency : Parcelable { return "ID(value='$value')" } - private companion object { + companion object { // should use delimiters that could be used in URL not like path or query delimiters - const val PREFIX_DELIMITER = '_' - const val SUFFIX_DELIMITER = ';' - const val DERIVATION_PATH_DELIMITER = 'd' + private const val PREFIX_DELIMITER = '_' + private const val SUFFIX_DELIMITER = ';' + private const val DERIVATION_PATH_DELIMITER = 'd' } } diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index 6b098d5be3..cda5592ee6 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal @@ -45,6 +46,9 @@ data class CryptoCurrencyStatus( /** The network address */ open val networkAddress: NetworkAddress? = null + + /** Staking yield balance */ + open val yieldBalance: YieldBalance? = null } /** Represents the Loading state of a cryptocurrency, typically while fetching its details. */ @@ -107,6 +111,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: BigDecimal, override val fiatRate: BigDecimal, override val priceChange: BigDecimal, + override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, @@ -128,6 +133,7 @@ data class CryptoCurrencyStatus( override val fiatAmount: BigDecimal?, override val fiatRate: BigDecimal?, override val priceChange: BigDecimal?, + override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, @@ -143,6 +149,7 @@ data class CryptoCurrencyStatus( */ data class NoQuote( override val amount: BigDecimal, + override val yieldBalance: YieldBalance?, override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt index 5dcdb275ef..59d8d3a517 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable /** * Represents a blockchain network, identified by a unique ID, a human-readable name, and its standard type. @@ -20,7 +19,7 @@ import kotlinx.parcelize.Parcelize * that cannot be represented in a fiat currency. * (For those blockchains that have FeeResource instead of a standard type of fee) */ -@Parcelize +@Serializable data class Network( val id: ID, val backendId: String, @@ -30,7 +29,7 @@ data class Network( val isTestnet: Boolean, val standardType: StandardType, val hasFiatFeeRate: Boolean, -) : Parcelable { +) { init { require(name.isNotBlank()) { "Network name must not be blank" } @@ -42,8 +41,8 @@ data class Network( * @property value The string representation of the network ID. */ @JvmInline - @Parcelize - value class ID(val value: String) : Parcelable { + @Serializable + value class ID(val value: String) { init { require(value.isNotBlank()) { "Network ID must not be blank" } @@ -56,8 +55,8 @@ data class Network( * This class represents such paths in a generic manner, allowing for predefined card-based paths, * custom paths, or even no derivation path at all. */ - @Parcelize - sealed class DerivationPath : Parcelable { + @Serializable + sealed class DerivationPath { /** The actual derivation path value, if any. */ abstract val value: String? @@ -67,6 +66,7 @@ data class Network( * * @property value The derivation path string. */ + @Serializable data class Card(override val value: String) : DerivationPath() /** @@ -74,12 +74,14 @@ data class Network( * * @property value The derivation path string. */ + @Serializable data class Custom(override val value: String) : DerivationPath() /** * Represents a lack of derivation path. */ - object None : DerivationPath() { + @Serializable + data object None : DerivationPath() { override val value: String? get() = null } } @@ -93,31 +95,36 @@ data class Network( * * @property name The human-readable name of the standard type. */ - @Parcelize - sealed class StandardType : Parcelable { + @Serializable + sealed class StandardType { abstract val name: String /** Represents the ERC20 token standard, common on the Ethereum network. */ - object ERC20 : StandardType() { + @Serializable + data object ERC20 : StandardType() { override val name: String get() = "ERC20" } /** Represents the TRC20 token standard, common on the TRON network. */ - object TRC20 : StandardType() { + @Serializable + data object TRC20 : StandardType() { override val name: String get() = "TRC20" } /** Represents the BEP20 token standard, common on the Binance Smart Chain network. */ - object BEP20 : StandardType() { + @Serializable + data object BEP20 : StandardType() { override val name: String get() = "BEP20" } /** Represents the BEP2 token standard, common on the Binance Chain network. */ - object BEP2 : StandardType() { + @Serializable + data object BEP2 : StandardType() { override val name: String get() = "BEP2" } /** Represents a network that does not adhere to a predefined standard type. */ + @Serializable data class Unspecified(override val name: String) : StandardType() } } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index 9f5dfbe387..3db9280b76 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -8,12 +8,9 @@ import java.math.BigDecimal * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. * @property fiatRate The current fiat exchange rate for the cryptocurrency. * @property priceChange The price change for the cryptocurrency. - * @property values The values representing the cryptocurrency's price changes over a 24-hour period, - * suitable for chart plotting. */ data class Quote( val rawCurrencyId: String, val fiatRate: BigDecimal, val priceChange: BigDecimal, - val values: List? = null, ) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index 7448714472..ca9266b2d1 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.raise.Raise import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -19,6 +20,7 @@ class FetchCardTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val networksRepository: NetworksRepository, private val quotesRepository: QuotesRepository, + private val stakingRepository: StakingRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { @@ -39,8 +41,13 @@ class FetchCardTokenListUseCase( refresh = refresh, ) } - - awaitAll(fetchStatuses, fetchQuotes) + val yieldBalances = async { + fetchYieldBalances( + userWalletId = userWalletId, + refresh = refresh, + ) + } + awaitAll(fetchStatuses, fetchQuotes, yieldBalances) } } } @@ -69,4 +76,12 @@ class FetchCardTokenListUseCase( catch = { /* Ignore error */ }, ) } + + private suspend fun fetchYieldBalances(userWalletId: UserWalletId, refresh: Boolean) { + val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) + catch( + block = { stakingRepository.fetchMultiYieldBalance(userWalletId, networkAddresses, refresh) }, + catch = { /* Ignore error */ }, + ) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt index 31b6ccc631..698c3be4f5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCardTokensListUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens import arrow.core.left import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -21,6 +22,7 @@ class GetCardTokensListUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, ) { @OptIn(ExperimentalCoroutinesApi::class) @@ -43,6 +45,7 @@ class GetCardTokensListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCardCurrenciesStatusesFlow() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index e521d035af..8cb5dbfc99 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -2,6 +2,8 @@ package com.tangem.domain.tokens import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -28,6 +30,7 @@ class GetCryptoCurrencyActionsUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -40,6 +43,7 @@ class GetCryptoCurrencyActionsUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWallet.walletId, ) val networkId = cryptoCurrencyStatus.currency.network.id @@ -121,6 +125,17 @@ class GetCryptoCurrencyActionsUseCase( activeList.add(TokenActionsState.ActionState.Receive(scenario)) } + // staking + if (isStakingAvailable(cryptoCurrency)) { + activeList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None)) + } else { + disabledList.add( + TokenActionsState.ActionState.Stake( + unavailabilityReason = ScenarioUnavailabilityReason.StakingUnavailable(cryptoCurrency.name), + ), + ) + } + // send val sendUnavailabilityReason = getSendUnavailabilityReason( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -234,6 +249,7 @@ class GetCryptoCurrencyActionsUseCase( } actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } + actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable)) actionsList.add(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) return actionsList } @@ -246,7 +262,7 @@ class GetCryptoCurrencyActionsUseCase( cryptoCurrencyStatus.value.amount.isNullOrZero() -> { ScenarioUnavailabilityReason.EmptyBalance(ScenarioUnavailabilityReason.WithdrawalScenario.SEND) } - currenciesRepository.hasPendingTransactions( + currenciesRepository.isSendBlockedByPendingTransactions( cryptoCurrencyStatus = cryptoCurrencyStatus, coinStatus = coinStatus, ) -> { @@ -264,4 +280,11 @@ class GetCryptoCurrencyActionsUseCase( private fun isAddressAvailable(networkAddress: NetworkAddress?): Boolean { return networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty() } + + private suspend fun isStakingAvailable(cryptoCurrency: CryptoCurrency): Boolean { + return stakingRepository.getStakingAvailabilityForActions( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) is StakingAvailability.Available + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt index 8150cfc734..9c87e00297 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusSyncUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency @@ -16,6 +17,7 @@ class GetCryptoCurrencyStatusSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, + internal val stakingRepository: StakingRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { @@ -29,6 +31,7 @@ class GetCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrencyStatusSync(cryptoCurrencyId, isSingleWalletWithTokens) @@ -41,6 +44,7 @@ class GetCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getPrimaryCurrencyStatusSync() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt index cdf58b3080..4c3e6b1b71 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyStatusesSyncUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -15,6 +16,7 @@ class GetCryptoCurrencyStatusesSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, + internal val stakingRepository: StakingRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { @@ -24,6 +26,7 @@ class GetCryptoCurrencyStatusesSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrenciesStatusesSync() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt index 92e660af07..dceb8551cf 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyStatusUpdatesUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrency @@ -24,6 +25,7 @@ class GetCurrencyStatusUpdatesUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -60,6 +62,7 @@ class GetCurrencyStatusUpdatesUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 93ff397011..29592489a4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.HederaWarnings @@ -26,6 +27,7 @@ class GetCurrencyWarningsUseCase( private val networksRepository: NetworksRepository, private val swapRepository: SwapRepository, private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, + private val stakingRepository: StakingRepository, private val promoRepository: PromoRepository, private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val dispatchers: CoroutineDispatcherProvider, @@ -43,6 +45,7 @@ class GetCurrencyWarningsUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) return combine( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt index d38fa6a951..473b7c10b9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetFeePaidCryptoCurrencyStatusSyncUseCase.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens import arrow.core.Either import arrow.core.raise.either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -16,6 +17,7 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase( internal val currenciesRepository: CurrenciesRepository, internal val quotesRepository: QuotesRepository, internal val networksRepository: NetworksRepository, + internal val stakingRepository: StakingRepository, internal val dispatchers: CoroutineDispatcherProvider, ) { @@ -30,6 +32,7 @@ class GetFeePaidCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return either { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt index 8b03aef79d..bd77e54e3b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNetworkCoinStatusUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -17,6 +18,7 @@ class GetNetworkCoinStatusUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -49,6 +51,7 @@ class GetNetworkCoinStatusUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) val maybeCurrency = if (isSingleWalletWithTokens) { @@ -69,6 +72,7 @@ class GetNetworkCoinStatusUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) val networkFlow = if (isSingleWalletWithTokens) { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt index f1216be10f..fdc6dfaf8a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens import arrow.core.Either +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.error.mapper.mapToCurrencyError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -24,6 +25,7 @@ class GetPrimaryCurrencyStatusUpdatesUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -46,6 +48,7 @@ class GetPrimaryCurrencyStatusUpdatesUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, userWalletId = userWalletId, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt index afb4450d1c..ef0b96e10b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt @@ -6,6 +6,7 @@ import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.core.utils.toLce +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.error.mapper.mapToTokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -26,6 +27,7 @@ class GetTokenListUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, ) { @OptIn(ExperimentalCoroutinesApi::class) @@ -35,6 +37,7 @@ class GetTokenListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrenciesStatusesFlow().transformLatest { maybeTokens -> @@ -55,6 +58,7 @@ class GetTokenListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrenciesStatuses(userWalletId).transformLatest { maybeCurrencies -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 36da5cae5c..ce783509f9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -6,6 +6,7 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance @@ -23,6 +24,7 @@ class GetWalletTotalBalanceUseCase( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, ) { suspend operator fun invoke( @@ -72,6 +74,7 @@ class GetWalletTotalBalanceUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) return operations.getCurrenciesStatuses( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt index 6b5e9429d8..000588f24b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/CurrencyStatusErrorMappers.kt @@ -6,6 +6,7 @@ import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations internal fun CurrenciesStatusesOperations.Error.mapToCurrencyError(): CurrencyStatusError { return when (this) { is CurrenciesStatusesOperations.Error.DataError -> CurrencyStatusError.DataError(this.cause) + is CurrenciesStatusesOperations.Error.EmptyYieldBalances, is CurrenciesStatusesOperations.Error.EmptyNetworksStatuses, is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 27b4e3956f..4f918d6a79 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -11,6 +11,7 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList is CurrenciesStatusesOperations.Error.EmptyQuotes, is CurrenciesStatusesOperations.Error.EmptyCurrencies, is CurrenciesStatusesOperations.Error.UnableToCreateCurrencyStatus, + is CurrenciesStatusesOperations.Error.EmptyYieldBalances, -> TokenListError.EmptyTokens } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt index 54db16bf92..340ca7d60d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/ScenarioUnavailabilityReason.kt @@ -3,6 +3,9 @@ package com.tangem.domain.tokens.model sealed class ScenarioUnavailabilityReason { data object None : ScenarioUnavailabilityReason() + // staking-specific + data class StakingUnavailable(val cryptoCurrencyName: String) : ScenarioUnavailabilityReason() + // send&sell-specific data class PendingTransaction( val withdrawalScenario: WithdrawalScenario, @@ -24,6 +27,6 @@ sealed class ScenarioUnavailabilityReason { data object UnassociatedAsset : ScenarioUnavailabilityReason() enum class WithdrawalScenario { - SELL, SEND + SELL, SEND // TODO staking create&process STAKING } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt index 372f1e51d5..a3dbea0959 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenActionsState.kt @@ -20,6 +20,8 @@ data class TokenActionsState( data class Receive(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Stake(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() + data class Swap(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() data class Send(override val unavailabilityReason: ScenarioUnavailabilityReason) : ActionState() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 3dd2b0582b..a4265dd621 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -7,6 +7,9 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -20,6 +23,7 @@ internal class CurrenciesStatusesLceOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, ) { fun getCurrenciesStatuses( @@ -65,11 +69,18 @@ internal class CurrenciesStatusesLceOperations( val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + val addresses = networksRepository.getNetworkAddresses(userWalletId) combine( getQuotes(currenciesIds), getNetworksStatuses(userWalletId, networks), - ) { maybeQuotes, maybeNetworksStatuses -> - val statuses = createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + getYieldBalances(userWalletId, addresses), + ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> + val statuses = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = maybeQuotes, + maybeNetworkStatuses = maybeNetworksStatuses, + maybeYieldBalances = maybeYieldBalances, + ) emit(statuses) }.collect() } @@ -84,9 +95,10 @@ internal class CurrenciesStatusesLceOperations( lceLoading() } else { createCurrenciesStatuses( - nonEmptyCurrencies, + currencies = nonEmptyCurrencies, maybeNetworkStatuses = null, maybeQuotes = null, + maybeYieldBalances = null, ) } @@ -113,6 +125,7 @@ internal class CurrenciesStatusesLceOperations( currencies: NonEmptyList, maybeQuotes: Either>?, maybeNetworkStatuses: Lce>?, + maybeYieldBalances: Lce?, ): Lce> = lce { isLoading.set(maybeNetworkStatuses == null) @@ -127,11 +140,20 @@ internal class CurrenciesStatusesLceOperations( null } + val yieldBalances = maybeYieldBalances?.getOrNull() + currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } + val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) - createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) + createCurrencyStatus( + currency = currency, + quote = quote, + networkStatus = networkStatus, + yieldBalance = yieldBalance, + ignoreQuote = quotesRetrievingFailed, + ) } } @@ -139,12 +161,14 @@ internal class CurrenciesStatusesLceOperations( currency: CryptoCurrency, quote: Quote?, networkStatus: NetworkStatus?, + yieldBalance: YieldBalance?, ignoreQuote: Boolean, ): CryptoCurrencyStatus { val currencyStatusOperations = CurrencyStatusOperations( currency = currency, quote = quote, networkStatus = networkStatus, + yieldBalance = yieldBalance, ignoreQuote = ignoreQuote, ) @@ -167,6 +191,18 @@ internal class CurrenciesStatusesLceOperations( } } + private fun getYieldBalances( + userWalletId: UserWalletId, + addresses: List, + ): LceFlow { + return stakingRepository.getMultiYieldBalanceLce( + userWalletId = userWalletId, + addresses = addresses, + ).map { maybeBalances -> + maybeBalances.mapError { TokenListError.DataError(it) } + } + } + private fun getIds(currencies: List): Pair, NonEmptySet> { val currencyIdToNetworkId = currencies.associate { currency -> currency.id to currency.network diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index de1768de08..24bb880d94 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -3,6 +3,9 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.staking.model.YieldBalanceList +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository @@ -17,6 +20,7 @@ internal class CurrenciesStatusesOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val stakingRepository: StakingRepository, private val userWalletId: UserWalletId, ) { @@ -42,6 +46,7 @@ internal class CurrenciesStatusesOperations( currencies = nonEmptyCurrencies, maybeNetworkStatuses = null, maybeQuotes = null, + maybeYieldBalances = null, ) emit(maybeLoadingCurrenciesStatuses) @@ -51,8 +56,14 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> - createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + getYieldBalances(), + ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> + createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = maybeQuotes, + maybeNetworkStatuses = maybeNetworksStatuses, + maybeYieldBalances = maybeYieldBalances, + ) } emitAll(currenciesFlow) @@ -70,7 +81,9 @@ internal class CurrenciesStatusesOperations( val quotes = quotesRepository.getQuotesSync(currenciesIds, false).right() val networkStatuses = networksRepository.getNetworkStatusesSync(userWalletId, networks, false).right() - return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses) + val yieldBalances = getYieldBalancesSync() + + return createCurrenciesStatuses(nonEmptyCurrencies, quotes, networkStatuses, yieldBalances) }, catch = { raise(Error.DataError(it)) }, ) @@ -98,7 +111,9 @@ internal class CurrenciesStatusesOperations( ).firstOrNull { it.network == currency.network }.right() - return createCurrencyStatus(currency, quotes, networkStatuses) + val yieldBalances = getYieldBalanceSync(currency) + + return createCurrencyStatus(currency, quotes, networkStatuses, yieldBalances) }, catch = { raise(Error.DataError(it)) }, ) @@ -142,8 +157,9 @@ internal class CurrenciesStatusesOperations( }, catch = { Error.DataError(it).left() }, ) + val yieldBalances = getYieldBalanceSync(currency) - return createCurrencyStatus(currency, quotes, networkStatus) + return createCurrencyStatus(currency, quotes, networkStatus, yieldBalances) } fun getCardCurrenciesStatusesFlow(): Flow>> { @@ -167,6 +183,7 @@ internal class CurrenciesStatusesOperations( currencies = nonEmptyCurrencies, maybeNetworkStatuses = null, maybeQuotes = null, + maybeYieldBalances = null, ) emit(maybeLoadingCurrenciesStatuses) @@ -176,8 +193,9 @@ internal class CurrenciesStatusesOperations( val currenciesFlow = combine( getQuotes(currenciesIds), getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> - createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses) + getYieldBalances(), + ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> + createCurrenciesStatuses(nonEmptyCurrencies, maybeQuotes, maybeNetworksStatuses, maybeYieldBalances) } emitAll(currenciesFlow) @@ -255,8 +273,10 @@ internal class CurrenciesStatusesOperations( } } - return combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus -> - createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus) + val yieldBalanceFlow = getYieldBalance(currency) + + return combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> + createCurrencyStatus(currency, maybeQuote, maybeNetworkStatus, maybeYieldBalance) } } @@ -264,6 +284,7 @@ internal class CurrenciesStatusesOperations( currencies: NonEmptyList, maybeQuotes: Either>?, maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, ): Either> = either { var quotesRetrievingFailed = false @@ -281,11 +302,19 @@ internal class CurrenciesStatusesOperations( }, ) + val yieldBalances = maybeYieldBalances?.getOrNull() + currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - - createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quotesRetrievingFailed) + val yieldBalance = (yieldBalances as? YieldBalanceList.Data)?.getBalance(currency.id.rawCurrencyId) + createCurrencyStatus( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = quotesRetrievingFailed, + yieldBalance = yieldBalance, + ) } } @@ -293,6 +322,7 @@ internal class CurrenciesStatusesOperations( currency: CryptoCurrency, maybeQuote: Either, maybeNetworkStatus: Either, + maybeYieldBalance: Either?, ): Either = either { var quoteRetrievingFailed = false @@ -301,8 +331,15 @@ internal class CurrenciesStatusesOperations( quoteRetrievingFailed = true null } + val yieldBalance = maybeYieldBalance?.getOrNull() - createCurrencyStatus(currency, quote, networkStatus, ignoreQuote = quoteRetrievingFailed) + createCurrencyStatus( + currency = currency, + quote = quote, + networkStatus = networkStatus, + ignoreQuote = quoteRetrievingFailed, + yieldBalance = yieldBalance, + ) } private fun createCurrencyStatus( @@ -310,12 +347,14 @@ internal class CurrenciesStatusesOperations( quote: Quote?, networkStatus: NetworkStatus?, ignoreQuote: Boolean, + yieldBalance: YieldBalance?, ): CryptoCurrencyStatus { val currencyStatusOperations = CurrencyStatusOperations( currency = currency, quote = quote, networkStatus = networkStatus, ignoreQuote = ignoreQuote, + yieldBalance = yieldBalance, ) return currencyStatusOperations.createTokenStatus() @@ -396,6 +435,65 @@ internal class CurrenciesStatusesOperations( .onEmpty { emit(Error.EmptyNetworksStatuses.left()) } } + @OptIn(ExperimentalCoroutinesApi::class) + private fun getYieldBalances(): EitherFlow { + return networksRepository.getNetworkAddressesFlow(userWalletId).flatMapLatest { addresses -> + stakingRepository.getMultiYieldBalanceFlow( + userWalletId = userWalletId, + addresses = addresses, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } + } + } + + private suspend fun getYieldBalancesSync(): Either { + return catch( + block = { + val networkAddresses = networksRepository.getNetworkAddresses(userWalletId) + stakingRepository.getMultiYieldBalanceSync( + userWalletId, + networkAddresses, + ).right() + }, + catch = { + Error.EmptyYieldBalances.left() + }, + ) + } + + private suspend fun getYieldBalanceSync( + cryptoCurrency: CryptoCurrency, + ): Either { + return catch( + block = { + val address = networksRepository.getNetworkAddress(userWalletId, cryptoCurrency) + stakingRepository.getSingleYieldBalanceSync( + userWalletId, + address, + ).right() + }, + catch = { + Error.EmptyYieldBalances.left() + }, + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun getYieldBalance(cryptoCurrency: CryptoCurrency): EitherFlow { + return networksRepository.getNetworkAddressFlow( + userWalletId, + cryptoCurrency, + ).flatMapLatest { address -> + stakingRepository.getSingleYieldBalanceFlow( + userWalletId = userWalletId, + address = address, + ).map> { it.right() } + .catch { emit(Error.DataError(it).left()) } + .onEmpty { emit(Error.EmptyYieldBalances.left()) } + } + } + private fun getIds( currencies: NonEmptyList, ): Pair, NonEmptySet> { @@ -413,14 +511,16 @@ internal class CurrenciesStatusesOperations( sealed class Error { - object EmptyCurrencies : Error() + data object EmptyCurrencies : Error() - object EmptyQuotes : Error() + data object EmptyQuotes : Error() - object EmptyNetworksStatuses : Error() + data object EmptyNetworksStatuses : Error() - object UnableToCreateCurrencyStatus : Error() + data object UnableToCreateCurrencyStatus : Error() data class DataError(val cause: Throwable) : Error() + + data object EmptyYieldBalances : Error() } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 8284d1667e..ee8fa082a7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.operations +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.* import java.math.BigDecimal @@ -7,6 +8,7 @@ internal class CurrencyStatusOperations( private val currency: CryptoCurrency, private val quote: Quote?, private val networkStatus: NetworkStatus?, + private val yieldBalance: YieldBalance?, private val ignoreQuote: Boolean, ) { @@ -18,7 +20,7 @@ internal class CurrencyStatusOperations( is NetworkStatus.MissedDerivation -> createMissedDerivationStatus() is NetworkStatus.Unreachable -> createUnreachableStatus(status) is NetworkStatus.NoAccount -> createNoAccountStatus(status) - is NetworkStatus.Verified -> createStatus(status) + is NetworkStatus.Verified -> createStatus(status, yieldBalance) } } @@ -42,7 +44,7 @@ internal class CurrencyStatusOperations( networkAddress = status.address, ) - private fun createStatus(status: NetworkStatus.Verified): CryptoCurrencyStatus.Value { + private fun createStatus(status: NetworkStatus.Verified, yieldBalance: YieldBalance?): CryptoCurrencyStatus.Value { val amount = when (val amount = status.amounts[currency.id]) { null -> { return CryptoCurrencyStatus.Loading @@ -62,6 +64,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, + yieldBalance = yieldBalance, ) currency is CryptoCurrency.Token && currency.isCustom -> CryptoCurrencyStatus.Custom( amount = amount, @@ -71,6 +74,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, + yieldBalance = yieldBalance, ) quote == null -> CryptoCurrencyStatus.Loading else -> CryptoCurrencyStatus.Loaded( @@ -81,6 +85,7 @@ internal class CurrencyStatusOperations( hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, networkAddress = status.address, + yieldBalance = yieldBalance, ) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index 2ff01885c9..8f28b74ecc 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -1,8 +1,10 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.utils.extensions.orZero import java.math.BigDecimal internal class TokenListFiatBalanceOperations( @@ -56,10 +58,13 @@ internal class TokenListFiatBalanceOperations( currentBalance: TotalFiatBalance, ): TotalFiatBalance { return with(currentBalance) { + val stakingBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val fiatStakingBalance = status.fiatRate.times(stakingBalance) + (this as? TotalFiatBalance.Loaded)?.copy( - amount = this.amount + status.fiatAmount, + amount = this.amount + status.fiatAmount + fiatStakingBalance, ) ?: TotalFiatBalance.Loaded( - amount = status.fiatAmount, + amount = status.fiatAmount + fiatStakingBalance, isAllAmountsSummarized = true, ) } @@ -71,12 +76,13 @@ internal class TokenListFiatBalanceOperations( ): TotalFiatBalance { return with(currentBalance) { val isTokenAmountCanBeSummarized = status.fiatAmount != null - + val yieldBalance = (status.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val fiatYieldBalance = status.fiatRate?.times(yieldBalance).orZero() (this as? TotalFiatBalance.Loaded)?.copy( - amount = this.amount + (status.fiatAmount ?: BigDecimal.ZERO), + amount = this.amount + status.fiatAmount.orZero() + fiatYieldBalance, isAllAmountsSummarized = isTokenAmountCanBeSummarized, ) ?: TotalFiatBalance.Loaded( - amount = status.fiatAmount ?: BigDecimal.ZERO, + amount = status.fiatAmount.orZero() + fiatYieldBalance, isAllAmountsSummarized = isTokenAmountCanBeSummarized, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 5bdf7b4067..782c16153e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -194,12 +194,15 @@ interface CurrenciesRepository { fun getMissedAddressesCryptoCurrencies(userWalletId: UserWalletId): Flow> /** - * Determines whether the currency has pending transaction or currency network has pending transaction + * Determines whether the currency sending is blocked by network pending transaction * * @param cryptoCurrencyStatus currency status * @param coinStatus main currency status in [cryptoCurrencyStatus] network */ - fun hasPendingTransactions(cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?): Boolean + fun isSendBlockedByPendingTransactions( + cryptoCurrencyStatus: CryptoCurrencyStatus, + coinStatus: CryptoCurrencyStatus?, + ): Boolean /** * Retrieves fee paid currency for specific [currency]. diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index a2033bb707..a2a99ddea2 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus @@ -62,8 +63,33 @@ interface NetworksRepository { fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean + /** + * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] + */ + fun getNetworkAddressesFlow(userWalletId: UserWalletId, network: Network): Flow> + /** * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] */ suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List + + /** + * Returns address of [cryptoCurrency] in selected wallet [userWalletId] + */ + suspend fun getNetworkAddress(userWalletId: UserWalletId, currency: CryptoCurrency): CryptoCurrencyAddress + + /** + * Returns address of [cryptoCurrency] in selected wallet [userWalletId] + */ + fun getNetworkAddressFlow(userWalletId: UserWalletId, currency: CryptoCurrency): Flow + + /** + * Returns list of addresses and crypto currency info in selected wallet [userWalletId] + */ + fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> + + /** + * Returns list of addresses and crypto currency info in selected wallet [userWalletId] + */ + suspend fun getNetworkAddresses(userWalletId: UserWalletId): List } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt index b5c027ac68..4f52744205 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetPrimaryCurrencyStatusUpdatesUseCaseTest.kt @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.domain.core.error.DataError +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.mock.MockNetworks import com.tangem.domain.tokens.mock.MockQuotes @@ -13,6 +14,7 @@ import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository +import com.tangem.domain.tokens.repository.MockStakingRepository import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import junit.framework.TestCase.assertEquals @@ -121,6 +123,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + yieldBalance = YieldBalance.Error, ), ) } @@ -171,5 +174,6 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { ), quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(statuses), + stakingRepository = MockStakingRepository(), ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt index 613246a85c..b5cd747c50 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/GetTokenListUseCaseTest.kt @@ -16,6 +16,7 @@ import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.MockCurrenciesRepository import com.tangem.domain.tokens.repository.MockNetworksRepository import com.tangem.domain.tokens.repository.MockQuotesRepository +import com.tangem.domain.tokens.repository.MockStakingRepository import com.tangem.domain.wallets.models.UserWalletId import junit.framework.TestCase.assertEquals import kotlinx.coroutines.delay @@ -316,5 +317,6 @@ internal class GetTokenListUseCaseTest { ), quotesRepository = MockQuotesRepository(quotes), networksRepository = MockNetworksRepository(statuses), + stakingRepository = MockStakingRepository(), ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index e08a5348dd..3f15752baf 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -49,7 +49,8 @@ internal object MockTokenLists { val loadingUngroupedTokenList = with(failedUngroupedTokenList) { copy( - currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptyListOrNull() ?: emptyList(), + currencies = currencies.map { it.copy(value = CryptoCurrencyStatus.Loading) }.toNonEmptyListOrNull() + ?: emptyList(), totalFiatBalance = TotalFiatBalance.Loading, ) } @@ -60,7 +61,9 @@ internal object MockTokenLists { groups = groups.map { group -> group.copy( currencies = group.currencies - .map { it.copy(value = CryptoCurrencyStatus.Loading) }, + .map { it.copy(value = CryptoCurrencyStatus.Loading) } + .toNonEmptyListOrNull() + ?: emptyList(), ) }.toNonEmptyListOrNull()!!, ) @@ -109,7 +112,7 @@ internal object MockTokenLists { val sortedGroupedTokenList: TokenList.GroupedByNetwork get() { - val groups = sortedNetworksGroups + val groups = sortedNetworksGroups.toNonEmptyList() return unsortedGroupedTokenList.copy( groups = groups, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index 7490827a6c..e3f88d9b52 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress @@ -151,6 +152,7 @@ internal object MockTokensStates { pendingTransactions = emptySet(), hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, + yieldBalance = YieldBalance.Error, ), ) } @@ -166,6 +168,7 @@ internal object MockTokensStates { .first { it.network == status.currency.network } .value as? NetworkStatus.Verified, ).address, + yieldBalance = YieldBalance.Error, ), ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index d4d616d88a..e81b9e83d1 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -122,7 +122,7 @@ internal class MockCurrenciesRepository( return isSortedByBalance.map { it.getOrElse { e -> throw e } } } - override fun hasPendingTransactions( + override fun isSendBlockedByPendingTransactions( cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, ): Boolean { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 78e06e7087..5b5019dfd6 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -3,13 +3,15 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError -import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.toLce +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -44,10 +46,38 @@ internal class MockNetworksRepository( } override fun isNeedToCreateAccountWithoutReserve(network: Network) = false + + override fun getNetworkAddressesFlow( + userWalletId: UserWalletId, + network: Network, + ): Flow> = channelFlow { + send(emptyList()) + } + + override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> = channelFlow { + send(emptyList()) + } + override suspend fun getNetworkAddresses( userWalletId: UserWalletId, network: Network, ): List { return emptyList() } + + override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List { + return emptyList() + } + + override suspend fun getNetworkAddress( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): CryptoCurrencyAddress = CryptoCurrencyAddress(currency, "") + + override fun getNetworkAddressFlow( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Flow = channelFlow { + send(CryptoCurrencyAddress(currency, "")) + } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt new file mode 100644 index 0000000000..e6436b5f62 --- /dev/null +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -0,0 +1,203 @@ +package com.tangem.domain.tokens.repository + +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.staking.model.* +import com.tangem.domain.staking.model.action.EnterAction +import com.tangem.domain.staking.model.action.StakingActionStatus +import com.tangem.domain.staking.model.action.StakingActionType +import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.staking.model.transaction.StakingTransactionStatus +import com.tangem.domain.staking.model.transaction.StakingTransactionType +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAddress +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import org.joda.time.DateTime +import java.math.BigDecimal + +class MockStakingRepository : StakingRepository { + override fun isStakingSupported(currencyId: String): Boolean = true + + override suspend fun fetchEnabledYields(refresh: Boolean) { /* no-op */ + } + + override suspend fun getEntryInfo(integrationId: String): StakingEntryInfo = StakingEntryInfo( + interestRate = 1.toBigDecimal(), + periodInDays = 2, + tokenSymbol = "SOL", + ) + + override suspend fun getYield(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): Yield = Yield( + id = "1", + token = Token( + name = "Solana", + network = NetworkType.SOLANA, + symbol = "SOL", + decimals = 18, + address = null, + coinGeckoId = "solana", + logoURI = null, + isPoints = null, + ), + tokens = listOf(), + args = Yield.Args( + enter = Yield.Args.Enter( + addresses = Yield.Args.Enter.Addresses( + address = AddressArgument( + required = false, + network = null, + minimum = null, + maximum = null, + ), + additionalAddresses = mapOf(), + ), + args = mapOf(), + ), + exit = null, + ), + status = Yield.Status(enter = false, exit = null), + apy = 1.toBigDecimal(), + rewardRate = 2.3, + rewardType = Yield.RewardType.APR, + metadata = Yield.Metadata( + name = "Yield", + logoUri = "", + description = "", + documentation = null, + gasFeeToken = Token( + name = "Solana", + network = NetworkType.SOLANA, + symbol = "SOL", + decimals = 18, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + token = Token( + name = "Solana", + network = NetworkType.SOLANA, + symbol = "SOL", + decimals = 18, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + tokens = listOf(), + type = "auto", + rewardSchedule = "1", + cooldownPeriod = Yield.Metadata.Period(days = 1), + warmupPeriod = Yield.Metadata.Period(days = 1), + rewardClaiming = "1", + defaultValidator = null, + minimumStake = null, + supportsMultipleValidators = false, + revshare = Yield.Metadata.Enabled(enabled = false), + fee = Yield.Metadata.Enabled(enabled = false), + ), + validators = listOf(), + isAvailable = false, + ) + + override suspend fun getStakingAvailabilityForActions( + cryptoCurrencyId: CryptoCurrency.ID, + symbol: String, + ): StakingAvailability = StakingAvailability.Unavailable + + override suspend fun fetchSingleYieldBalance( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + refresh: Boolean, + ) { + /* no-op */ + } + + override fun getSingleYieldBalanceFlow( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + ): Flow = channelFlow { + send(YieldBalance.Error) + } + + override suspend fun getSingleYieldBalanceSync( + userWalletId: UserWalletId, + address: CryptoCurrencyAddress, + ): YieldBalance = YieldBalance.Error + + override suspend fun fetchMultiYieldBalance( + userWalletId: UserWalletId, + addresses: List, + refresh: Boolean, + ) { + /* no-op */ + } + + override fun getMultiYieldBalanceFlow( + userWalletId: UserWalletId, + addresses: List, + ): Flow = channelFlow { + send( + YieldBalanceList.Data( + balances = listOf(YieldBalance.Error), + ), + ) + } + + override fun getMultiYieldBalanceLce( + userWalletId: UserWalletId, + addresses: List, + ): LceFlow = lceFlow { + send( + YieldBalanceList.Data( + balances = listOf(YieldBalance.Error), + ), + ) + } + + override suspend fun getMultiYieldBalanceSync( + userWalletId: UserWalletId, + addresses: List, + ): YieldBalanceList = YieldBalanceList.Data( + balances = listOf(YieldBalance.Error), + ) + + override suspend fun createEnterAction( + integrationId: String, + amount: BigDecimal, + address: String, + validatorAddress: String, + token: Token, + ): EnterAction = EnterAction( + id = "quis", + integrationId = "persequeris", + status = StakingActionStatus.PROCESSING, + type = StakingActionType.CLAIM_REWARDS, + currentStepIndex = 8701, + amount = BigDecimal.ZERO, + validatorAddress = null, + validatorAddresses = listOf(), + transactions = listOf(), + createdAt = DateTime.now(), + ) + + override suspend fun constructTransaction(transactionId: String): StakingTransaction = StakingTransaction( + id = "id", + network = NetworkType.SOLANA, + status = StakingTransactionStatus.SIGNED, + type = StakingTransactionType.FREEZE_ENERGY, + hash = null, + signedTransaction = null, + unsignedTransaction = null, + stepIndex = 9368, + error = null, + gasEstimate = null, + stakeId = null, + explorerUrl = null, + ledgerHwAppId = null, + isMessage = false, + ) +} \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index d4014c6f47..b54aeca082 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) + implementation(projects.domain.transaction.models) implementation(projects.domain.demo) implementation(projects.domain.card) } \ No newline at end of file diff --git a/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/TransactionType.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/TransactionType.kt new file mode 100644 index 0000000000..fdd9a7ec17 --- /dev/null +++ b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/models/TransactionType.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.transaction.models + +enum class TransactionType { + APPROVE, +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 52b707e8d1..2228ec19e5 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -4,7 +4,9 @@ import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.models.TransactionType import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigInteger interface TransactionRepository { @@ -19,7 +21,7 @@ interface TransactionRepository { isSwap: Boolean, txExtras: TransactionExtras?, hash: String?, - ): TransactionData? + ): TransactionData.Uncompiled? @Suppress("LongParameterList") suspend fun validateTransaction( @@ -40,4 +42,12 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): com.tangem.blockchain.extensions.Result + + fun createTransactionDataExtras( + data: String, + network: Network, + transactionType: TransactionType, + nonce: BigInteger?, + gasLimit: BigInteger?, + ): TransactionExtras } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt new file mode 100644 index 0000000000..52d686e403 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateTransactionDataExtrasUseCase.kt @@ -0,0 +1,30 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.transaction.models.TransactionType +import java.math.BigInteger + +class CreateTransactionDataExtrasUseCase( + private val transactionRepository: TransactionRepository, +) { + + operator fun invoke( + data: String, + network: Network, + transactionType: TransactionType, + gasLimit: BigInteger? = null, + nonce: BigInteger? = null, + ) = Either.catch { + requireNotNull( + transactionRepository.createTransactionDataExtras( + data = data, + network = network, + transactionType = transactionType, + nonce = nonce, + gasLimit = gasLimit, + ), + ) { "Failed to create transaction" } + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt index a33088d612..541f04b900 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt @@ -12,10 +12,8 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn import java.math.BigDecimal /** @@ -23,7 +21,6 @@ import java.math.BigDecimal */ class EstimateFeeUseCase( private val walletManagersFacade: WalletManagersFacade, - private val dispatcher: CoroutineDispatcherProvider, ) { suspend operator fun invoke( amount: BigDecimal, @@ -43,7 +40,7 @@ class EstimateFeeUseCase( null -> GetFeeError.UnknownError.left() } emit(maybeFee) - }.flowOn(dispatcher.io) + } } private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt index 8be00045ba..61b2f26817 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateTransactionUseCase.kt @@ -16,7 +16,7 @@ class ValidateTransactionUseCase( @Suppress("LongParameterList") suspend operator fun invoke( amount: Amount, - fee: Fee, + fee: Fee?, memo: String?, destination: String, userWalletId: UserWalletId, diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index bd175a01fa..9972fdc9c7 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { // endregion // region Domain modules + api(projects.domain.core) implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) @@ -28,9 +29,4 @@ dependencies { implementation(deps.tangem.blockchain) // android-library implementation(deps.tangem.card.core) // endregion - - // region Other libraries - implementation(deps.arrow.core) - implementation(deps.kotlin.coroutines) - // endregion } \ No newline at end of file diff --git a/domain/wallets/models/build.gradle.kts b/domain/wallets/models/build.gradle.kts index 537d724d84..1b3ce4dfaf 100644 --- a/domain/wallets/models/build.gradle.kts +++ b/domain/wallets/models/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -11,4 +12,8 @@ dependencies { // region Domain modules implementation(project(":domain:models")) // endregion + + // region Other libraries + implementation(deps.kotlin.serialization) + // endregion } \ No newline at end of file diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt index 970ee3ad9f..4ea8ab824c 100644 --- a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/UserWalletId.kt @@ -2,9 +2,10 @@ package com.tangem.domain.wallets.models import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString -import java.io.Serializable +import kotlinx.serialization.Serializable -data class UserWalletId(val stringValue: String) : Serializable { +@Serializable +data class UserWalletId(val stringValue: String) { val value = stringValue.hexToBytes() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index 9323fd5423..a8b14fe269 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -1,12 +1,17 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either +import arrow.core.left import arrow.core.raise.either import arrow.core.raise.ensureNotNull +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.transformLatest class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListManager) { @@ -17,4 +22,13 @@ class GetUserWalletUseCase(private val userWalletsListManager: UserWalletsListMa raise(GetUserWalletError.UserWalletNotFound) } } + + @OptIn(ExperimentalCoroutinesApi::class) + fun invokeFlow(userWalletId: UserWalletId): EitherFlow { + return userWalletsListManager.userWallets.transformLatest { userWallets -> + userWallets.firstOrNull { it.walletId == userWalletId } + ?.let { emit(it.right()) } + ?: emit(GetUserWalletError.UserWalletNotFound.left()) + } + } } \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 67b386e4e9..2d59c37cd6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -63,18 +63,20 @@ platform :android do "android.injected.signing.store.password" => options[:store_password], "android.injected.signing.key.alias" => options[:key_alias], "android.injected.signing.key.password" => options[:key_password], - }) - gradle( - task: "assemble", - build_type: "Release", - properties: { - 'versionCode' => options[:versionCode], - 'versionName' => options[:versionName], - "android.injected.signing.store.file" => options[:keystore], - "android.injected.signing.store.password" => options[:store_password], - "android.injected.signing.key.alias" => options[:key_alias], - "android.injected.signing.key.password" => options[:key_password], - }) + } + ) + gradle( + task: "assemble", + build_type: "Release", + properties: { + 'versionCode' => options[:versionCode], + 'versionName' => options[:versionName], + "android.injected.signing.store.file" => options[:keystore], + "android.injected.signing.store.password" => options[:store_password], + "android.injected.signing.key.alias" => options[:key_alias], + "android.injected.signing.key.password" => options[:key_password], + } + ) end desc "Submit a new Beta Build to Firebase App Distribution" @@ -85,4 +87,20 @@ platform :android do groups: options[:groups]) end + desc "Publish internal and external builds to Firebase App Distribution" + lane :publishToFirebase do |options| + gradle( + task: "clean assemble", + build_type: "Internal", + properties: { + 'versionCode' => ENV['versionCode'], + 'versionName' => ENV['versionName'], + } + ) + firebase_app_distribution( + app: ENV['app_id_internal'], + apk_path: ENV['apk_path_internal'], + groups: ENV['groups'] + ) + end end diff --git a/fastlane/README.md b/fastlane/README.md index 9df376cd1d..44d6ef904a 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -37,7 +37,7 @@ Build a signed release APK [bundle exec] fastlane android build ``` -Build internal and release APKs +Build external and release APKs ### android beta @@ -47,6 +47,16 @@ Build internal and release APKs Submit a new Beta Build to Firebase App Distribution +### android publishToFirebase + +```sh +[bundle exec] fastlane android publishToFirebase +``` + +Publish internal and external builds to Firebase App Distribution + + + ---- This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. diff --git a/features/details/api/build.gradle.kts b/features/details/api/build.gradle.kts index 5010f64d85..ce267fe5d1 100644 --- a/features/details/api/build.gradle.kts +++ b/features/details/api/build.gradle.kts @@ -15,7 +15,5 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) - - /* AndroidX */ - implementation(deps.androidx.fragment.ktx) + implementation(projects.core.ui) } \ No newline at end of file diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt deleted file mode 100644 index d3ac9fe6f7..0000000000 --- a/features/details/api/src/main/kotlin/com/tangem/features/details/DetailsEntryPoint.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.details - -import androidx.fragment.app.Fragment - -interface DetailsEntryPoint { - - fun entryFragment(): Fragment - - companion object { - - const val USER_WALLET_ID_KEY = "user_wallet_id" - } -} \ No newline at end of file diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt new file mode 100644 index 0000000000..f2546ffc26 --- /dev/null +++ b/features/details/api/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.details.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface DetailsComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + data class Params( + val userWalletId: UserWalletId, + ) +} \ No newline at end of file diff --git a/features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt b/features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt new file mode 100644 index 0000000000..7908aa5ede --- /dev/null +++ b/features/details/api/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.details.component + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface UserWalletListComponent : ComposableContentComponent { + + interface Factory { + fun create(context: AppComponentContext): UserWalletListComponent + } +} \ No newline at end of file diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 5060249621..c1f2899eff 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /* Project - API */ implementation(projects.features.details.api) + implementation(projects.features.disclaimer.api) implementation(projects.features.tester.api) /* Project - Core */ @@ -23,11 +24,25 @@ dependencies { implementation(projects.core.featuretoggles) implementation(projects.core.navigation) implementation(projects.core.analytics.models) + implementation(projects.common.routing) /* Project - Domain */ + implementation(projects.domain.models) + implementation(projects.domain.feedback) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.card) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.walletConnect) implementation(projects.domain.legacy) + /* SDK */ + // TODO: For TangemError model, should be removed after card domain scanning refactoring + implementation(deps.tangem.card.core) + /* AndroidX */ implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.activity.compose) @@ -47,4 +62,5 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt deleted file mode 100644 index 28db59fc3d..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsFragment.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.features.details - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.fragment.app.Fragment -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.di.RootAppComponentContext -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.message.EventMessageEffect -import com.tangem.core.ui.message.EventMessageHandler -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.features.details.component.DetailsComponent -import com.tangem.features.details.component.preview.PreviewDetailsComponent -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -// TODO: Remove after [REDACTED_JIRA] -@AndroidEntryPoint -internal class DetailsFragment : ComposeFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - // @Inject - // internal lateinit var componentFactory: DetailsComponent.Factory - - @Inject - internal lateinit var detailsRouter: DetailsRouter - - @Inject - @RootAppComponentContext - internal lateinit var rootContext: AppComponentContext - - private val component: DetailsComponent by lazy { initComponent() } - - private val messageHandler = EventMessageHandler() - - @Composable - override fun ScreenContent(modifier: Modifier) { - component.View(modifier = modifier) - - EventMessageEffect( - messageHandler = messageHandler, - snackbarHostState = component.snackbarHostState, - ) - } - - private fun initComponent(): DetailsComponent { - // TODO: Uncomment in [REDACTED_JIRA] - // val selectedUserWalletId = arguments?.getString(DetailsEntryPoint.USER_WALLET_ID_KEY) - // ?.let(::UserWalletId) - // - // - // requireNotNull(selectedUserWalletId) { "UserWalletId must be provided" } - // - // val context = rootContext.childByContext( - // componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - // messageHandler = messageHandler, - // router = detailsRouter, - // ) - // - // return componentFactory.create( - // context = context, - // params = DetailsComponent.Params( - // selectedUserWalletId = selectedUserWalletId, - // ), - // ) - - return PreviewDetailsComponent() - } - - companion object : DetailsEntryPoint { - - override fun entryFragment(): Fragment = DetailsFragment() - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt deleted file mode 100644 index fbd703a49a..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/DetailsRouter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.features.details - -import com.tangem.core.decompose.navigation.Route -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.features.details.routing.DetailsRoute -import com.tangem.features.tester.api.TesterRouter -import javax.inject.Inject - -// TODO: Remove after [REDACTED_JIRA] -internal class DetailsRouter @Inject constructor( - private val reduxNavController: ReduxNavController, - private val reduxStateHolder: ReduxStateHolder, - private val testerRouter: TesterRouter, -) : Router { - - override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - if (route is DetailsRoute) { - when (route) { - is DetailsRoute.Screen -> { - reduxNavController.navigate(NavigationAction.NavigateTo(route.screen, bundle = route.params)) - } - is DetailsRoute.Feedback -> { - reduxStateHolder.sendFeedbackEmail() - } - is DetailsRoute.TesterMenu -> { - testerRouter.startTesterScreen() - } - is DetailsRoute.Url -> { - reduxNavController.navigate(NavigationAction.OpenUrl(route.url)) - } - } - onComplete(true) - } else { - onComplete(false) - } - } - - override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { - reduxNavController.popBackStack() - onComplete(true) - } - - override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - if (route is DetailsRoute.Screen) { - reduxNavController.popBackStack(route.screen) - onComplete(true) - } else { - reduxNavController.getBackStack() - onComplete(false) - } - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt deleted file mode 100644 index 9731597b9b..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/DetailsComponent.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.details.component - -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.wallets.models.UserWalletId - -interface DetailsComponent { - - val snackbarHostState: SnackbarHostState - - @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - fun View(modifier: Modifier) - - interface Factory { - - fun create(context: AppComponentContext, params: Params): DetailsComponent - } - - data class Params( - val selectedUserWalletId: UserWalletId, - ) -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt deleted file mode 100644 index 0a76b5fcd1..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/UserWalletListComponent.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.details.component - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.decompose.context.AppComponentContext - -interface UserWalletListComponent { - - @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - fun View(modifier: Modifier) - - interface Factory { - fun create(context: AppComponentContext): UserWalletListComponent - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt deleted file mode 100644 index c8a317814b..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/WalletConnectComponent.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.features.details.component - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.domain.wallets.models.UserWalletId - -interface WalletConnectComponent { - - suspend fun checkIsAvailable(): Boolean - - @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - fun View(modifier: Modifier) - - interface Factory { - fun create(context: AppComponentContext, params: Params): WalletConnectComponent - } - - data class Params( - val userWalletId: UserWalletId, - ) -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt new file mode 100644 index 0000000000..9a9c5575d1 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultDetailsComponent.kt @@ -0,0 +1,45 @@ +package com.tangem.features.details.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.details.component.UserWalletListComponent +import com.tangem.features.details.model.DetailsModel +import com.tangem.features.details.ui.DetailsScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultDetailsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: DetailsComponent.Params, + userWalletListComponentFactory: UserWalletListComponent.Factory, +) : DetailsComponent, AppComponentContext by context { + + private val model: DetailsModel = getOrCreateModel(params) + + private val userWalletListComponent = userWalletListComponentFactory.create( + context = child(key = "user_wallet_list"), + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + DetailsScreen( + modifier = modifier, + state = state, + userWalletListBlockContent = userWalletListComponent, + ) + } + + @AssistedFactory + interface Factory : DetailsComponent.Factory { + override fun create(context: AppComponentContext, params: DetailsComponent.Params): DefaultDetailsComponent + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultUserWalletListComponent.kt new file mode 100644 index 0000000000..53279cde55 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/impl/DefaultUserWalletListComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.details.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.details.component.UserWalletListComponent +import com.tangem.features.details.model.UserWalletListModel +import com.tangem.features.details.ui.UserWalletListBlock +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultUserWalletListComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, +) : UserWalletListComponent, AppComponentContext by context { + + private val model: UserWalletListModel = getOrCreateModel() + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + UserWalletListBlock( + modifier = modifier, + state = state, + ) + } + + @AssistedFactory + interface Factory : UserWalletListComponent.Factory { + + override fun create(context: AppComponentContext): DefaultUserWalletListComponent + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 9c6aff4521..5d2d2b67d0 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -1,8 +1,9 @@ package com.tangem.features.details.component.preview -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.core.decompose.navigation.DummyRouter +import com.tangem.core.navigation.url.DummyUrlOpener import com.tangem.features.details.component.DetailsComponent import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.entity.DetailsUM @@ -13,18 +14,15 @@ import kotlinx.coroutines.runBlocking internal class PreviewDetailsComponent : DetailsComponent { - override val snackbarHostState: SnackbarHostState = SnackbarHostState() - private val previewBlocks = runBlocking { ItemsBuilder( - walletConnectComponent = PreviewWalletConnectComponent(), - userWalletListComponent = PreviewUserWalletListComponent(), - router = PreviewRouter(), - ).buldAll() + router = DummyRouter(), + urlOpener = DummyUrlOpener(), + ).buildAll(isWalletConnectAvailable = true, onSupportClick = {}) } private val previewFooter = DetailsFooterUM( - socials = SocialsBuilder(PreviewRouter()).buildAll(), + socials = SocialsBuilder(DummyUrlOpener()).buildAll(), appVersion = "1.0.0-preview", ) @@ -36,11 +34,11 @@ internal class PreviewDetailsComponent : DetailsComponent { @Composable @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - override fun View(modifier: Modifier) { + override fun Content(modifier: Modifier) { DetailsScreen( modifier = modifier, state = previewState, - snackbarHostState = snackbarHostState, + userWalletListBlockContent = PreviewUserWalletListComponent(), ) } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt deleted file mode 100644 index 49366db923..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewRouter.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.details.component.preview - -import com.tangem.core.decompose.navigation.Route -import com.tangem.core.decompose.navigation.Router - -internal class PreviewRouter : Router { - override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } - - override fun pop(onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } - - override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) { - /* no-op */ - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index c7628c2619..09950611e8 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -46,7 +46,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { @Composable @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - override fun View(modifier: Modifier) { + override fun Content(modifier: Modifier) { UserWalletListBlock(state = previewState, modifier = modifier) } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt deleted file mode 100644 index 4f94585d78..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewWalletConnectComponent.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.details.component.preview - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.features.details.component.WalletConnectComponent -import com.tangem.features.details.ui.WalletConnectBlock - -internal class PreviewWalletConnectComponent : WalletConnectComponent { - - override suspend fun checkIsAvailable(): Boolean = true - - @Composable - @Suppress("TopLevelComposableFunctions") // TODO: Remove this check - override fun View(modifier: Modifier) { - WalletConnectBlock(onClick = { /* no-op */ }, modifier = modifier) - } -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ComponentModule.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ComponentModule.kt new file mode 100644 index 0000000000..27fd842ab3 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ComponentModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.details.di + +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.details.component.UserWalletListComponent +import com.tangem.features.details.component.impl.DefaultDetailsComponent +import com.tangem.features.details.component.impl.DefaultUserWalletListComponent +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 ComponentModule { + + @Binds + @Singleton + fun bindDetailsComponentFactory(factory: DefaultDetailsComponent.Factory): DetailsComponent.Factory + + @Binds + @Singleton + fun bindUserWalletListComponentFactory( + factory: DefaultUserWalletListComponent.Factory, + ): UserWalletListComponent.Factory +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt index f9a437ee25..44a655a33f 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/FeatureModule.kt @@ -2,9 +2,7 @@ package com.tangem.features.details.di import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.features.details.DefaultDetailsFeatureToggles -import com.tangem.features.details.DetailsEntryPoint import com.tangem.features.details.DetailsFeatureToggles -import com.tangem.features.details.DetailsFragment import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,10 +18,4 @@ internal object FeatureModule { fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles { return DefaultDetailsFeatureToggles(featureTogglesManager) } - - @Provides - @Singleton - fun provideEntryPoint(): DetailsEntryPoint { - return DetailsFragment.Companion - } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt index 8b429e0438..63481527c2 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/di/ModelModule.kt @@ -3,6 +3,7 @@ package com.tangem.features.details.di import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.decompose.model.Model import com.tangem.features.details.model.DetailsModel +import com.tangem.features.details.model.UserWalletListModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -17,4 +18,9 @@ internal interface ModelModule { @IntoMap @ClassKey(DetailsModel::class) fun provideDetailsModel(model: DetailsModel): Model + + @Binds + @IntoMap + @ClassKey(UserWalletListModel::class) + fun provideUserWalletListModel(model: UserWalletListModel): Model } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt index 267acbe9d0..c614a5af8d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsFooterUM.kt @@ -1,8 +1,10 @@ package com.tangem.features.details.entity import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.ImmutableList +@Immutable internal data class DetailsFooterUM( val appVersion: String, val socials: ImmutableList, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt index 1aa5a52ab5..99c416a63a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/DetailsItemUM.kt @@ -1,10 +1,7 @@ package com.tangem.features.details.entity -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.ui.Modifier -import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.components.block.model.BlockUM import kotlinx.collections.immutable.ImmutableList @Immutable @@ -19,23 +16,15 @@ internal sealed class DetailsItemUM { data class Item( val id: String, - val title: TextReference, - @DrawableRes - val iconRes: Int, - val onClick: () -> Unit, + val block: BlockUM, ) } - data class Component( - override val id: String, - val content: Content, - ) : DetailsItemUM() { + data class WalletConnect(val onClick: () -> Unit) : DetailsItemUM() { + override val id: String = "wallet_connect" + } - fun interface Content { - - @Composable - @Suppress("TopLevelComposableFunctions", "ComposableFunctionName") - operator fun invoke(modifier: Modifier) - } + data object UserWalletList : DetailsItemUM() { + override val id: String = "user_wallet_list" } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 918c27eb97..8bd54c48e9 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -1,10 +1,103 @@ package com.tangem.features.details.model +import arrow.core.getOrElse +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.decompose.navigation.Router +import com.tangem.domain.feedback.FeedbackManager +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.details.component.DetailsComponent +import com.tangem.features.details.entity.DetailsFooterUM +import com.tangem.features.details.entity.DetailsItemUM +import com.tangem.features.details.entity.DetailsUM +import com.tangem.features.details.utils.ItemsBuilder +import com.tangem.features.details.utils.SocialsBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.version.AppVersionProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject -// TODO: Will be implemented later +@ComponentScoped +@Suppress("LongParameterList") internal class DetailsModel @Inject constructor( + private val socialsBuilder: SocialsBuilder, + private val itemsBuilder: ItemsBuilder, + private val appVersionProvider: AppVersionProvider, + private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, + private val router: Router, + private val paramsContainer: ParamsContainer, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val feedbackManager: FeedbackManager, override val dispatchers: CoroutineDispatcherProvider, -) : Model() \ No newline at end of file +) : Model() { + + private val params: DetailsComponent.Params = paramsContainer.require() + + private val items: MutableStateFlow> = MutableStateFlow(value = persistentListOf()) + + val state: MutableStateFlow = MutableStateFlow( + value = DetailsUM( + items = items.value, + footer = DetailsFooterUM( + socials = socialsBuilder.buildAll(), + appVersion = getAppVersion(), + ), + popBack = router::pop, + ), + ) + + init { + items + .onEach(::updateState) + .launchIn(modelScope) + + checkWalletConnectAvailability() + } + + private fun checkWalletConnectAvailability() = modelScope.launch { + val isWalletConnectAvailable = checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { + Timber.w("Unable to check WalletConnect availability: $it") + + false + } + + items.value = itemsBuilder.buildAll( + isWalletConnectAvailable = isWalletConnectAvailable, + onSupportClick = ::sendFeedback, + ) + } + + private fun sendFeedback() { + modelScope.launch { + val scanResponse = getSelectedWalletSyncUseCase().getOrNull()?.scanResponse + ?: error("Selected wallet is null") + + val cardInfo = getCardInfoUseCase(scanResponse = scanResponse).getOrNull() + ?: error("CardInfo must be not null") + + feedbackManager.sendEmail(type = FeedbackEmailType.DirectUserRequest(cardInfo)) + } + } + + private suspend fun updateState(items: ImmutableList) { + state.update { prevState -> + prevState.copy( + items = items, + ) + } + } + + private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})" +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt new file mode 100644 index 0000000000..202758486c --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -0,0 +1,74 @@ +package com.tangem.features.details.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase +import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM +import com.tangem.features.details.impl.R +import com.tangem.features.details.utils.UserWalletSaver +import com.tangem.features.details.utils.UserWalletsFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@ComponentScoped +internal class UserWalletListModel @Inject constructor( + private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, + private val userWalletSaver: UserWalletSaver, + private val userWalletsFetcher: UserWalletsFetcher, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) + + private val userWallets: SharedFlow> = userWalletsFetcher + .userWallets + .share() + + private val shouldSaveUserWallets: SharedFlow = shouldSaveUserWalletsUseCase() + .distinctUntilChanged() + .share() + + val state: MutableStateFlow = MutableStateFlow( + value = UserWalletListUM( + userWallets = persistentListOf(), + isWalletSavingInProgress = false, + addNewWalletText = TextReference.EMPTY, + onAddNewWalletClick = ::addUserWallet, + ), + ) + + init { + combine( + userWallets, + shouldSaveUserWallets, + isWalletSavingInProgress, + transform = ::updateState, + ).launchIn(modelScope) + } + + private suspend fun updateState( + userWallets: ImmutableList, + shouldSaveUserWallets: Boolean, + isWalletSavingInProgress: Boolean, + ) = state.update { value -> + value.copy( + userWallets = userWallets, + isWalletSavingInProgress = isWalletSavingInProgress, + addNewWalletText = if (shouldSaveUserWallets) { + resourceReference(R.string.user_wallet_list_add_button) + } else { + resourceReference(R.string.scan_card_settings_button) + }, + ) + } + + private fun addUserWallet() = withProgress(isWalletSavingInProgress) { + userWalletSaver.scanAndSaveUserWallet() + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt deleted file mode 100644 index 878987aca5..0000000000 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/routing/DetailsRoute.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.details.routing - -import android.os.Bundle -import com.tangem.core.decompose.navigation.Route -import com.tangem.core.navigation.AppScreen - -// TODO: Remove after [REDACTED_JIRA] -internal sealed class DetailsRoute : Route { - - data class Screen( - val screen: AppScreen, - val params: Bundle? = null, - ) : DetailsRoute() - - data class Url(val url: String) : DetailsRoute() - - data object Feedback : DetailsRoute() - - data object TesterMenu : DetailsRoute() -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt index 0b03305816..c1db3e35bd 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/DetailsScreen.kt @@ -15,11 +15,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SystemBarsEffect +import com.tangem.core.ui.components.appbar.models.TopAppBarMedium +import com.tangem.core.ui.components.block.BlockItem import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalSnackbarHostState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.preview.PreviewDetailsComponent @@ -28,18 +30,16 @@ import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.entity.DetailsUM import com.tangem.features.details.impl.R -private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f - @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun DetailsScreen(state: DetailsUM, snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) { +internal fun DetailsScreen( + state: DetailsUM, + userWalletListBlockContent: ComposableContentComponent, + modifier: Modifier = Modifier, +) { val backgroundColor = TangemTheme.colors.background.secondary val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - SystemBarsEffect { - setSystemBarsColor(backgroundColor) - } - BackHandler(onBack = state.popBack) Scaffold( @@ -48,68 +48,31 @@ internal fun DetailsScreen(state: DetailsUM, snackbarHostState: SnackbarHostStat snackbarHost = { TangemSnackbarHost( modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - hostState = snackbarHostState, + hostState = LocalSnackbarHostState.current, + ) + }, + topBar = { + TopAppBarMedium( + title = resourceReference(R.string.details_title), + scrollBehavior = scrollBehavior, + onBackClick = state.popBack, ) }, - topBar = { TopBar(state, scrollBehavior) }, ) { paddingValues -> Content( modifier = Modifier.padding(paddingValues), state = state, + userWalletListBlockContent = userWalletListBlockContent, ) } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(state: DetailsUM, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier) { - MediumTopAppBar( - modifier = modifier, - scrollBehavior = scrollBehavior, - colors = TopAppBarColors( - containerColor = TangemTheme.colors.background.secondary, - scrolledContainerColor = TangemTheme.colors.background.secondary, - navigationIconContentColor = TangemTheme.colors.icon.primary1, - titleContentColor = TangemTheme.colors.text.primary1, - actionIconContentColor = TangemTheme.colors.icon.primary1, - ), - title = { - val collapsedStyle = TangemTheme.typography.subtitle1 - val expandedStyle = TangemTheme.typography.h1 - val style by remember(scrollBehavior.state.collapsedFraction) { - derivedStateOf { - if (scrollBehavior.state.collapsedFraction >= COLLAPSED_APP_BAR_THRESHOLD) { - collapsedStyle - } else { - expandedStyle - } - } - } - - Text( - text = stringResource(id = R.string.details_title), - style = style, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - navigationIcon = { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = state.popBack, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - ) - } - }, - ) -} - -@Composable -private fun Content(state: DetailsUM, modifier: Modifier = Modifier) { +private fun Content( + state: DetailsUM, + userWalletListBlockContent: ComposableContentComponent, + modifier: Modifier = Modifier, +) { LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), @@ -125,6 +88,7 @@ private fun Content(state: DetailsUM, modifier: Modifier = Modifier) { Block( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), model = block, + userWalletListBlockContent = userWalletListBlockContent, ) } @@ -138,7 +102,11 @@ private fun Content(state: DetailsUM, modifier: Modifier = Modifier) { } @Composable -private fun Block(model: DetailsItemUM, modifier: Modifier = Modifier) { +private fun Block( + model: DetailsItemUM, + userWalletListBlockContent: ComposableContentComponent, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .fillMaxWidth() @@ -149,22 +117,28 @@ private fun Block(model: DetailsItemUM, modifier: Modifier = Modifier) { horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.Top, ) { + val itemModifier = Modifier.fillMaxWidth() + when (model) { is DetailsItemUM.Basic -> { model.items.forEach { item -> key(item.id) { BlockItem( - modifier = Modifier.fillMaxWidth(), - model = item, + modifier = itemModifier, + model = item.block, ) } } } - is DetailsItemUM.Component -> { - model.content( - modifier = Modifier.fillMaxWidth(), + is DetailsItemUM.WalletConnect -> { + WalletConnectBlock( + modifier = itemModifier, + onClick = model.onClick, ) } + is DetailsItemUM.UserWalletList -> { + userWalletListBlockContent.Content(modifier = itemModifier) + } } } } @@ -222,7 +196,7 @@ private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) { @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun Preview_DetailsScreen() { TangemThemePreview { - PreviewDetailsComponent().View(modifier = Modifier.fillMaxSize()) + PreviewDetailsComponent().Content(modifier = Modifier.fillMaxSize()) } } // endregion Preview \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index 254c434047..cb235d20fa 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -1,7 +1,9 @@ package com.tangem.features.details.ui +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -11,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -104,12 +107,24 @@ private fun AddWalletButton( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - Icon( + AnimatedContent( modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_plus_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) + targetState = isInProgress, + ) { isInProgress -> + if (isInProgress) { + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size24), + color = TangemTheme.colors.icon.accent, + ) + } else { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_plus_24), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + } Text( text = text.resolveReference(), diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt index 11a28b3a75..b2e05c0504 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/WalletConnectBlock.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.features.details.impl.R diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt index aa18331772..7461386ac3 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/ItemsBuilder.kt @@ -1,61 +1,57 @@ package com.tangem.features.details.utils +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.navigation.Router -import com.tangem.core.navigation.AppScreen +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.details.component.UserWalletListComponent -import com.tangem.features.details.component.WalletConnectComponent import com.tangem.features.details.entity.DetailsItemUM import com.tangem.features.details.impl.BuildConfig import com.tangem.features.details.impl.R -import com.tangem.features.details.routing.DetailsRoute import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import javax.inject.Inject -internal class ItemsBuilder( - private val walletConnectComponent: WalletConnectComponent, - private val userWalletListComponent: UserWalletListComponent, +@ComponentScoped +internal class ItemsBuilder @Inject constructor( private val router: Router, + private val urlOpener: UrlOpener, ) { - suspend fun buldAll(): ImmutableList = buildList { - buildWalletConnectBlock()?.let(::add) - buildUserWalletListBlock().let(::add) - buildShopBlock().let(::add) - buildSettingsBlock().let(::add) - buildSupportBlock().let(::add) - }.toImmutableList() + fun buildAll(isWalletConnectAvailable: Boolean, onSupportClick: () -> Unit): ImmutableList = + buildList { + buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add) + buildUserWalletListBlock().let(::add) + buildShopBlock().let(::add) + buildSettingsBlock().let(::add) + buildSupportBlock(onSupportClick).let(::add) + }.toImmutableList() - private suspend fun buildWalletConnectBlock(): DetailsItemUM? { - return if (walletConnectComponent.checkIsAvailable()) { - DetailsItemUM.Component( - id = "wallet_connect", - content = { - walletConnectComponent.View(modifier = it) - }, + private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? { + return if (isWalletConnectAvailable) { + DetailsItemUM.WalletConnect( + onClick = { router.push(AppRoute.WalletConnectSessions) }, ) } else { null } } - private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.Component( - id = "user_wallet_list", - content = { - userWalletListComponent.View(modifier = it) - }, - ) + private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.UserWalletList private fun buildShopBlock(): DetailsItemUM = DetailsItemUM.Basic( id = "shop", items = persistentListOf( DetailsItemUM.Basic.Item( id = "buy_tangem_wallet", - title = stringReference("Buy Tangem Wallet"), // TODO: Move to resources in [REDACTED_TASK_KEY] - iconRes = R.drawable.ic_tangem_24, - onClick = { router.push(DetailsRoute.Url(BUY_TANGEM_URL)) }, + block = BlockUM( + text = resourceReference(R.string.details_buy_wallet), + iconRes = R.drawable.ic_tangem_24, + onClick = { urlOpener.openUrl(BUY_TANGEM_URL) }, + ), ), ), ) @@ -65,36 +61,44 @@ internal class ItemsBuilder( items = buildList { DetailsItemUM.Basic.Item( id = "app_settings", - title = resourceReference(R.string.app_settings_title), - iconRes = R.drawable.ic_settings_24, - onClick = { router.push(DetailsRoute.Screen(AppScreen.AppSettings)) }, + block = BlockUM( + text = resourceReference(R.string.app_settings_title), + iconRes = R.drawable.ic_settings_24, + onClick = { router.push(AppRoute.AppSettings) }, + ), ).let(::add) if (BuildConfig.TESTER_MENU_ENABLED) { DetailsItemUM.Basic.Item( id = "tester_menu", - title = stringReference(value = "Tester menu"), - iconRes = R.drawable.ic_alert_24, - onClick = { router.push(DetailsRoute.TesterMenu) }, + block = BlockUM( + text = stringReference(value = "Tester menu"), + iconRes = R.drawable.ic_alert_24, + onClick = { router.push(AppRoute.TesterMenu) }, + ), ).let(::add) } }.toImmutableList(), ) - private fun buildSupportBlock(): DetailsItemUM = DetailsItemUM.Basic( + private fun buildSupportBlock(onClick: () -> Unit): DetailsItemUM = DetailsItemUM.Basic( id = "support", items = persistentListOf( DetailsItemUM.Basic.Item( id = "send_feedback", - title = stringReference("Send feedback"), // TODO: Move to resources in [REDACTED_TASK_KEY] - iconRes = R.drawable.ic_comment_24, - onClick = { router.push(DetailsRoute.Feedback) }, + block = BlockUM( + text = resourceReference(R.string.details_send_feedback), + iconRes = R.drawable.ic_comment_24, + onClick = onClick, + ), ), DetailsItemUM.Basic.Item( id = "disclaimer", - title = resourceReference(R.string.disclaimer_title), - iconRes = R.drawable.ic_text_24, - onClick = { router.push(DetailsRoute.Screen(AppScreen.Disclaimer)) }, + block = BlockUM( + text = resourceReference(R.string.disclaimer_title), + iconRes = R.drawable.ic_text_24, + onClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) }, + ), ), ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt index 6896f1e7d0..8282bbd41c 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/SocialsBuilder.kt @@ -1,15 +1,17 @@ package com.tangem.features.details.utils import androidx.compose.ui.text.intl.Locale -import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.details.entity.DetailsFooterUM import com.tangem.features.details.impl.R -import com.tangem.features.details.routing.DetailsRoute import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import javax.inject.Inject -internal class SocialsBuilder( - private val router: Router, +@ComponentScoped +internal class SocialsBuilder @Inject constructor( + private val urlOpener: UrlOpener, ) { fun buildAll(): ImmutableList = Social.all.map { social -> @@ -29,7 +31,7 @@ internal class SocialsBuilder( social.url } - router.push(DetailsRoute.Url(url)) + urlOpener.openUrl(url) } private enum class Social( diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt new file mode 100644 index 0000000000..992fee20eb --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletMappers.kt @@ -0,0 +1,84 @@ +package com.tangem.features.details.utils + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM +import com.tangem.features.details.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal fun List.toUiModels( + onClick: (UserWalletId) -> Unit, + appCurrency: AppCurrency? = null, + balances: Map = emptyMap(), +): ImmutableList = this.map { model -> + val balance = balances[model.walletId] + model.mapToUiModel( + balance = balance, + appCurrency = appCurrency, + onClick = { onClick(model.walletId) }, + ) +}.toImmutableList() + +private fun UserWallet.mapToUiModel( + balance: TotalFiatBalance?, + appCurrency: AppCurrency?, + onClick: () -> Unit, +): UserWalletUM = UserWalletUM( + id = walletId, + name = name, + information = getInfo(appCurrency, balance), + imageResId = resolveImage(), + onClick = onClick, +) + +private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBalance?): TextReference { + val cardCount = getCardCount() + val cardCountRef = TextReference.PluralRes( + id = R.plurals.card_label_card_count, + count = cardCount, + formatArgs = wrappedList(cardCount), + ) + val amount = when (balance) { + is TotalFiatBalance.Loaded -> balance.amount.takeIf { balance.isAllAmountsSummarized } + is TotalFiatBalance.Failed, + is TotalFiatBalance.Loading, + null, + -> null + } + + return if (amount != null && appCurrency != null) { + val divider = stringReference(value = " • ") + val formattedAmount = BigDecimalFormatter.formatFiatAmount( + fiatAmount = amount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + val amountRef = stringReference(formattedAmount) + TextReference.Combined(wrappedList(cardCountRef, divider, amountRef)) + } else { + cardCountRef + } +} + +private fun UserWallet.getCardCount() = when (val status = scanResponse.card.backupStatus) { + is CardDTO.BackupStatus.Active -> status.cardCount.inc() + is CardDTO.BackupStatus.CardLinked -> status.cardCount.inc() + is CardDTO.BackupStatus.NoBackup, + null, + -> 1 +} + +@DrawableRes +private fun UserWallet.resolveImage(): Int { + // TODO: Implement image resolving [REDACTED_JIRA] + return R.drawable.ill_card_wallet_2_211_343 +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt new file mode 100644 index 0000000000..f1bb1fd398 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -0,0 +1,139 @@ +package com.tangem.features.details.utils + +import arrow.core.raise.* +import arrow.core.recover +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.navigation.popTo +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.isNullOrEmpty +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.wallets.builder.UserWalletBuilder +import com.tangem.domain.wallets.models.SaveWalletError +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.domain.wallets.usecase.SelectWalletUseCase +import com.tangem.features.details.impl.R +import javax.inject.Inject + +@ComponentScoped +@Suppress("LongParameterList") +internal class UserWalletSaver @Inject constructor( + private val scanCardProcessor: ScanCardProcessor, + private val saveWalletUseCase: SaveWalletUseCase, + private val generateWalletNameUseCase: GenerateWalletNameUseCase, + private val selectWalletUseCase: SelectWalletUseCase, + private val reduxStateHolder: ReduxStateHolder, + private val messageSender: UiMessageSender, + private val router: Router, +) { + + suspend fun scanAndSaveUserWallet() = recover( + block = { + val response = scanCard() + val userWallet = createUserWallet(response) + + saveWallet(userWallet) + + router.popTo() + }, + recover = { error -> + val message = error.message + + if (!message.isNullOrEmpty()) { + messageSender.send(SnackbarMessage(message)) + } + }, + ) + + private suspend fun Raise.saveWallet(userWallet: UserWallet) { + saveWalletUseCase(userWallet).recover { error -> + when (error) { + is SaveWalletError.WalletAlreadySaved -> selectUserWallet(userWallet) + is SaveWalletError.DataError -> { + val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) { + Error.Unkonwn + } + + raise(Error.Message(messageRef)) + } + } + }.bind() + + reduxStateHolder.onUserWalletSelected(userWallet) + } + + private suspend fun Raise.selectUserWallet(userWallet: UserWallet) { + withError({ Error.Unkonwn }) { + selectWalletUseCase(userWallet.walletId).bind() + } + + router.popTo() + } + + private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { + val userWallet = UserWalletBuilder(response, generateWalletNameUseCase).build() + + return ensureNotNull(userWallet) { Error.Unkonwn } + } + + private suspend fun Raise.scanCard(): ScanResponse { + var response: ScanResponse? = null + + scanCardProcessor.scan( + analyticsSource = AnalyticsParam.ScreensSources.Settings, + onWalletNotCreated = { + raise(Error.WalletNotCreated) + }, + disclaimerWillShow = { + router.pop() + raise(Error.DisclaimerWillShow) + }, + onSuccess = { + response = it + }, + onFailure = { tangemError -> + val error = if (!tangemError.silent) { + val message = tangemError.messageResId + ?.let(::resourceReference) + ?: stringReference(tangemError.customMessage) + + Error.Message(message) + } else { + Error.Silent + } + + raise(error) + }, + ) + + return response!! + } + + sealed class Error { + + open val message: TextReference? = null + + data object WalletNotCreated : Error() + + data object DisclaimerWillShow : Error() + + data object Silent : Error() + + data class Message(override val message: TextReference) : Error() + + data object Unkonwn : Error() { + + override val message: TextReference = resourceReference(R.string.common_unknown_error) + } + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt new file mode 100644 index 0000000000..e606030df5 --- /dev/null +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletsFetcher.kt @@ -0,0 +1,96 @@ +package com.tangem.features.details.utils + +import arrow.core.Either +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.lce +import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.core.utils.toLce +import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TotalFiatBalance +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM +import com.tangem.features.details.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +@ComponentScoped +internal class UserWalletsFetcher @Inject constructor( + private val getWalletsUseCase: GetWalletsUseCase, + private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val router: Router, + private val messageSender: UiMessageSender, +) { + + val userWallets: Flow> = getWalletsUseCase() + .distinctUntilChanged() + .transform { wallets -> + if (wallets.isEmpty()) { + error("Wallets must not be empty") + } else { + emit(wallets.toUiModels(onClick = ::navigateToWalletSettings)) + } + + combine( + getSelectedAppCurrencyUseCase(), + getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)), + ) { maybeAppCurrency, maybeBalances -> + val models = createUiModels(wallets, maybeAppCurrency, maybeBalances).getOrElse( + ifLoading = { return@combine }, + ifError = { + val message = resourceReference(R.string.common_unknown_error) + messageSender.send(SnackbarMessage(message)) + + return@combine + }, + ) + + emit(models) + }.collect() + } + + private fun createUiModels( + wallets: List, + maybeAppCurrency: Either, + maybeBalances: Lce>, + ): Lce> = lce { + val balances = withError( + transform = { Error.UnableToGetBalances }, + block = { maybeBalances.bind() }, + ) + val appCurrency = withError( + transform = { Error.UnableToGetAppCurrency }, + block = { maybeAppCurrency.toLce().bind() }, + ) + + wallets.toUiModels( + appCurrency = appCurrency, + balances = balances, + onClick = ::navigateToWalletSettings, + ) + } + + private fun navigateToWalletSettings(userWalletId: UserWalletId) { + router.push(AppRoute.WalletSettings(userWalletId)) + } + + sealed class Error { + + data object UnableToGetAppCurrency : Error() + + data object UnableToGetBalances : Error() + } +} \ No newline at end of file diff --git a/features/disclaimer/api/.gitignore b/features/disclaimer/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/disclaimer/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/disclaimer/api/build.gradle.kts b/features/disclaimer/api/build.gradle.kts new file mode 100644 index 0000000000..7b97fa25c1 --- /dev/null +++ b/features/disclaimer/api/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.disclaimer.api" +} + +dependencies { + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) +} \ No newline at end of file diff --git a/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt new file mode 100644 index 0000000000..df556bb776 --- /dev/null +++ b/features/disclaimer/api/src/main/java/com/tangem/features/disclaimer/api/components/DisclaimerComponent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.disclaimer.api.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface DisclaimerComponent : ComposableContentComponent { + interface Factory : ComponentFactory + + data class Params( + val isTosAccepted: Boolean, + ) +} \ No newline at end of file diff --git a/features/disclaimer/impl/.gitignore b/features/disclaimer/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/disclaimer/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts new file mode 100644 index 0000000000..bb7ee06e94 --- /dev/null +++ b/features/disclaimer/impl/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.disclaimer.impl" +} + +dependencies { + /* AndroidX */ + implementation(deps.lifecycle.compose) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.permission) + implementation(deps.compose.material3) + implementation(deps.compose.material) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.utils) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + implementation(projects.core.decompose) + implementation(projects.common.routing) + + /** Domain modules */ + implementation(projects.domain.models) + implementation(projects.domain.card) + implementation(projects.domain.settings) + + /** Feature modules */ + implementation(projects.features.disclaimer.api) + implementation(projects.features.pushNotifications.api) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt new file mode 100644 index 0000000000..f281084c55 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/component/impl/DefaultDisclaimerComponent.kt @@ -0,0 +1,48 @@ +package com.tangem.features.disclaimer.impl.component.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.navigation.finisher.AppFinisher +import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.disclaimer.impl.model.DisclaimerModel +import com.tangem.features.disclaimer.impl.ui.DisclaimerScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultDisclaimerComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: DisclaimerComponent.Params, + private val appFinisher: AppFinisher, +) : DisclaimerComponent, AppComponentContext by context { + + private val model: DisclaimerModel = getOrCreateModel(params) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + BackHandler { + if (params.isTosAccepted) { + state.popBack() + } else { + appFinisher.finish() + } + } + DisclaimerScreen(state = state) + } + + @AssistedFactory + interface Factory : DisclaimerComponent.Factory { + + override fun create( + context: AppComponentContext, + params: DisclaimerComponent.Params, + ): DefaultDisclaimerComponent + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ComponentModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..1605d6bbd6 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.disclaimer.impl.di + +import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.disclaimer.impl.component.impl.DefaultDisclaimerComponent +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 ComponentModule { + + @Binds + @Singleton + fun bindDisclaimerComponentFactory(factory: DefaultDisclaimerComponent.Factory): DisclaimerComponent.Factory +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ModelModule.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ModelModule.kt new file mode 100644 index 0000000000..a589336c78 --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.disclaimer.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.disclaimer.impl.model.DisclaimerModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(DisclaimerModel::class) + fun provideDisclaimerModel(model: DisclaimerModel): Model +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/entity/DisclaimerUM.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/entity/DisclaimerUM.kt new file mode 100644 index 0000000000..155a91269b --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/entity/DisclaimerUM.kt @@ -0,0 +1,18 @@ +package com.tangem.features.disclaimer.impl.entity + +internal data class DisclaimerUM( + val url: String, + val isTosAccepted: Boolean, + val onAccept: (Boolean) -> Unit, + val popBack: () -> Unit, +) + +internal object DummyDisclaimer { + + val state = DisclaimerUM( + url = "https://tangem.com/tangem_tos.html", + isTosAccepted = false, + onAccept = {}, + popBack = {}, + ) +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt new file mode 100644 index 0000000000..e3007b5a5a --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -0,0 +1,49 @@ +package com.tangem.features.disclaimer.impl.model + +import com.tangem.common.routing.AppRoute +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.decompose.navigation.Router +import com.tangem.domain.card.repository.CardRepository +import com.tangem.features.disclaimer.api.components.DisclaimerComponent +import com.tangem.features.disclaimer.impl.entity.DisclaimerUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class DisclaimerModel @Inject constructor( + private val cardRepository: CardRepository, + private val router: Router, + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: DisclaimerComponent.Params = paramsContainer.require() + + val state: MutableStateFlow = MutableStateFlow( + DisclaimerUM( + onAccept = ::onAccept, + url = DISCLAIMER_URL, + isTosAccepted = params.isTosAccepted, + popBack = router::pop, + ), + ) + + private fun onAccept(shouldAskPushPermission: Boolean) { + modelScope.launch { + cardRepository.acceptTangemTOS() + if (shouldAskPushPermission) { + router.push(AppRoute.PushNotification) + } else { + router.push(AppRoute.Home) + } + } + } + + private companion object { + const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html" + } +} \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt new file mode 100644 index 0000000000..7567197f2d --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -0,0 +1,170 @@ +package com.tangem.features.disclaimer.impl.ui + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.viewinterop.AndroidView +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons +import com.tangem.core.ui.components.appbar.models.AdditionalButton +import com.tangem.core.ui.components.buttons.common.TangemButtonColors +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.disclaimer.impl.R +import com.tangem.features.disclaimer.impl.entity.DisclaimerUM +import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer +import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull + +@Composable +internal fun DisclaimerScreen(state: DisclaimerUM) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val bottomPadding = if (state.isTosAccepted) { + bottomBarHeight + TangemTheme.dimens.size16 + } else { + bottomBarHeight + TangemTheme.dimens.size64 + } + val backgroundColor = if (state.isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6 + val (textColor, iconColor) = if (state.isTosAccepted) { + TangemTheme.colors.text.primary1 to TangemTheme.colors.icon.primary1 + } else { + TangemColorPalette.Light4 to TangemColorPalette.Light4 + } + Box( + modifier = Modifier + .background(backgroundColor) + .statusBarsPadding(), + ) { + Column(modifier = Modifier.padding(bottom = bottomPadding)) { + AppBarWithAdditionalButtons( + text = resourceReference(R.string.disclaimer_title), + startButton = AdditionalButton( + iconRes = R.drawable.ic_back_24, + onIconClicked = state.popBack, + ).takeIf { state.isTosAccepted }, + textColor = textColor, + iconColor = iconColor, + ) + DisclaimerContent(state.url, state.isTosAccepted) + } + + if (!state.isTosAccepted) { + BottomFade(Modifier.align(Alignment.BottomCenter), backgroundColor = backgroundColor) + DisclaimerButton(state.onAccept) + } else { + NavigationBar3ButtonsScrim() + } + } +} + +@SuppressLint("SetJavaScriptEnabled") +@Composable +private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { + val progressState = remember { mutableStateOf(ProgressState.Loading) } + val webClient = remember { DisclaimerWebViewClient(progressState) } + val transparent = Color.Transparent + val backgroundColor = if (isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6 + Box( + modifier = Modifier, + ) { + AndroidView( + factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setBackgroundColor(transparent.toArgb()) + settings.allowFileAccess = false + // to inject css style to display only in dark theme + settings.javaScriptEnabled = !isTosAccepted + overScrollMode = View.OVER_SCROLL_NEVER + webViewClient = webClient + + clearHistory() + clearFormData() + clearCache(true) + + loadUrl(url) + } + }, + ) + + when (progressState.value) { + ProgressState.Loading -> { + Box( + modifier = Modifier + .fillMaxSize() + .background(backgroundColor), + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, + modifier = Modifier + .align(Alignment.Center) + .padding(TangemTheme.dimens.spacing8), + ) + } + } + else -> Unit + } + } +} + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) { + val isPermissionGranted = getPushPermissionOrNull()?.let { permission -> + rememberPermissionState(permission = permission).status.isGranted + } ?: true + PrimaryButton( + text = stringResource(id = R.string.common_accept), + onClick = { onAccept(!isPermissionGranted) }, + colors = TangemButtonColors( + backgroundColor = TangemColorPalette.Light4, + contentColor = TangemColorPalette.Dark6, + disabledBackgroundColor = TangemColorPalette.Light4, + disabledContentColor = TangemColorPalette.Dark6, + ), + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .fillMaxWidth(), + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360, heightDp = 800) +@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DisclaimerScreen_Preview() { + TangemThemePreview { + DisclaimerScreen(state = DummyDisclaimer.state) + } +} +// endregion \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt new file mode 100644 index 0000000000..5ad8aedb8f --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt @@ -0,0 +1,82 @@ +package com.tangem.features.disclaimer.impl.ui + +import android.graphics.Bitmap +import android.webkit.* +import androidx.compose.runtime.MutableState + +internal enum class ProgressState { + Loading, + Done, + Error, +} + +/** + * Workaround to display web view with ToS only in dark theme + */ +private fun WebView.injectCSS() { + val code = "javascript:(function() {" + + "var node = document.createElement('style');" + + "node.type = 'text/css';" + + " node.innerHTML = 'body, label,th,p,a, td, tr,li,ul,span,table,h1,h2,h3,h4,h5,h6,h7,div,small {" + + " color: #C9C9C9;" + + "background-color: #1E1E1E;" + + " } ';" + + " document.head.appendChild(node);})();" + + evaluateJavascript(code, null) +} + +internal class DisclaimerWebViewClient(private val progressState: MutableState) : WebViewClient() { + + private var loadingUrl: String? = null + private var loadedUrl: String? = null + + fun reset() { + loadingUrl = null + loadedUrl = null + progressState.value = ProgressState.Loading + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + view?.injectCSS() + super.onPageStarted(view, url, favicon) + + if (loadingUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Loading + loadingUrl = url + } + + override fun onPageFinished(view: WebView?, url: String?) { + view?.injectCSS() + super.onPageFinished(view, url) + + if (loadedUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Done + loadedUrl = url + } + + override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) { + view?.injectCSS() + super.onReceivedError(view, resourceRequest, error) + error?.let { progressState.value = ProgressState.Error } + } + + override fun onReceivedHttpError( + view: WebView?, + resourceRequest: WebResourceRequest?, + errorResponse: WebResourceResponse?, + ) { + view?.injectCSS() + super.onReceivedHttpError(view, resourceRequest, errorResponse) + + if (resourceRequest != null && errorResponse != null) { + val isDifferentUrl = resourceRequest.url?.toString() != loadingUrl + val isSuccessCode = errorResponse.statusCode < RESPONSE_USER_ERROR_STATUS_CODE + val isNotDone = progressState.value != ProgressState.Done + if (isDifferentUrl || isSuccessCode || isNotDone) return + progressState.value = ProgressState.Error + } + } + + companion object { + private const val RESPONSE_USER_ERROR_STATUS_CODE = 400 + } +} \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt deleted file mode 100644 index e29173b5fc..0000000000 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/featuretoggles/ManageTokensFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.managetokens.featuretoggles - -interface ManageTokensFeatureToggles { - val isRedesignedScreenEnabled: Boolean -} \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ExpandableState.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ExpandableState.kt deleted file mode 100644 index 34048e6f11..0000000000 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ExpandableState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.features.managetokens.navigation - -enum class ExpandableState { - EXPANDED, - COLLAPSED, -} \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensUi.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensUi.kt deleted file mode 100644 index f200ab6a3c..0000000000 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/navigation/ManageTokensUi.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.managetokens.navigation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.unit.Dp - -interface ManageTokensUi { - - @Suppress("TopLevelComposableFunctions") - @Composable - fun Content(onHeaderSizeChange: (Dp) -> Unit, state: State) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt deleted file mode 100644 index 7552eb21df..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensFeatureTogglesModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.managetokens.di - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles -import com.tangem.managetokens.featuretoggles.DefaultManageTokensFeatureToggles -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object ManageTokensFeatureTogglesModule { - - @Provides - @Singleton - fun provideWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): ManageTokensFeatureToggles { - return DefaultManageTokensFeatureToggles(featureTogglesManager = featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt deleted file mode 100644 index e0ca63c3b2..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/di/ManageTokensRouterModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.di - -import com.tangem.features.managetokens.navigation.ManageTokensUi -import com.tangem.managetokens.presentation.router.ManageTokensUiImpl -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 ManageTokensRouterModule { - - @Binds - @Singleton - fun provideManageTokensRouter(manageTokensUiImpl: ManageTokensUiImpl): ManageTokensUi -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt deleted file mode 100644 index 967a54e950..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/featuretoggles/DefaultManageTokensFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.managetokens.featuretoggles - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles - -internal class DefaultManageTokensFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : ManageTokensFeatureToggles { - override val isRedesignedScreenEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_MANAGE_TOKENS_SCREEN_ENABLED") -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRoute.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRoute.kt deleted file mode 100644 index fad644416a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRoute.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.router - -/** - * Add Custom Tokens screens - * @property route route string representation - */ -internal sealed class AddCustomTokenRoute(val route: String) { - object Main : AddCustomTokenRoute("$BASE_ROUTE/main") - object ChooseNetwork : AddCustomTokenRoute("$BASE_ROUTE/choose_network") - object ChooseWallet : AddCustomTokenRoute("$BASE_ROUTE/choose_wallet") - object ChooseDerivation : AddCustomTokenRoute("$BASE_ROUTE/choose_derivation") -} - -private const val BASE_ROUTE = "manage_tokens/add_custom_token" \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRouter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRouter.kt deleted file mode 100644 index 4185ccddd4..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/router/AddCustomTokenRouter.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.router - -import androidx.compose.runtime.Stable -import androidx.navigation.NavController - -@Stable -internal class AddCustomTokenRouter( - private val navController: NavController, -) { - /** Pop back stack */ - fun popBackStack() { - navController.popBackStack() - } - - /** Open custom token choose network screen */ - fun openCustomTokenChooseNetwork() { - navController.navigate(AddCustomTokenRoute.ChooseNetwork.route) - } - - /** Open custom token choose derivation screen */ - fun openCustomTokenChooseDerivation() { - navController.navigate(AddCustomTokenRoute.ChooseDerivation.route) - } - - /** Open custom token choose wallet screen */ - fun openCustomTokenChooseWallet() { - navController.navigate(AddCustomTokenRoute.ChooseWallet.route) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenState.kt deleted file mode 100644 index 0eafc00654..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenState.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.event.consumedEvent -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.Event -import kotlinx.collections.immutable.ImmutableSet - -internal data class AddCustomTokenState( - val chooseWalletState: ChooseWalletState, - val chooseNetworkState: ChooseNetworkState, - val chooseDerivationState: ChooseDerivationState?, - val tokenData: CustomTokenData?, - val warnings: ImmutableSet, - val addTokenButton: ButtonState, - val showChooseWalletScreen: Boolean = false, - val event: StateEvent = consumedEvent(), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenWarning.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenWarning.kt deleted file mode 100644 index 690178c20d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/AddCustomTokenWarning.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.managetokens.impl.R - -/** - * Warning model of add custom token screen - * - * @property title warning description - */ -internal sealed class AddCustomTokenWarning(val title: TextReference, val subtitle: TextReference? = null) { - - object PotentialScamToken : AddCustomTokenWarning( - title = resourceReference(R.string.custom_token_validation_error_not_found_title), - subtitle = resourceReference(R.string.custom_token_validation_error_not_found_description), - ) - - object InvalidContractAddress : AddCustomTokenWarning( - title = resourceReference(R.string.custom_token_creation_error_invalid_contract_address), - ) - - object WrongDecimals : AddCustomTokenWarning( - title = - resourceReference(R.string.custom_token_creation_error_wrong_decimals, wrappedList(MAXIMUM_DECIMAL_NUMBER)), - ) - - private companion object { - const val MAXIMUM_DECIMAL_NUMBER = 30 - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ButtonState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ButtonState.kt deleted file mode 100644 index 42b515ea20..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ButtonState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal data class ButtonState( - val isEnabled: Boolean, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseDerivationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseDerivationState.kt deleted file mode 100644 index 047fae161d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseDerivationState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -import kotlinx.collections.immutable.ImmutableList - -internal data class ChooseDerivationState( - val derivations: ImmutableList, - val selectedDerivation: Derivation?, - val enterCustomDerivationState: EnterCustomDerivationState?, - val onChooseDerivationClick: () -> Unit, - val onCloseChoosingDerivationClick: () -> Unit, - val onEnterCustomDerivation: () -> Unit, - val show: Boolean = false, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseNetworkState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseNetworkState.kt deleted file mode 100644 index 56b993010b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/ChooseNetworkState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import kotlinx.collections.immutable.ImmutableList - -internal data class ChooseNetworkState( - val networks: ImmutableList, - val selectedNetwork: NetworkItemState?, - val onChooseNetworkClick: () -> Unit, - val onCloseChoosingNetworkClick: () -> Unit, - val show: Boolean = false, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/CustomTokenData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/CustomTokenData.kt deleted file mode 100644 index b481d6767a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/CustomTokenData.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal data class CustomTokenData( - val contractAddressTextField: TextFieldState, - val nameTextField: TextFieldState, - val symbolTextField: TextFieldState, - val decimalsTextField: TextFieldState, -) { - - fun isRequiredInformationProvided(): Boolean { - return contractAddressTextField.isInputValid() && nameTextField.isInputValid() && - symbolTextField.isInputValid() && decimalsTextField.isInputValid() - } - - fun isNameSymbolDecimalsDisabled(): Boolean { - return nameTextField.isDisabled() && symbolTextField.isDisabled() && decimalsTextField.isDisabled() - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/Derivation.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/Derivation.kt deleted file mode 100644 index 69f41a5f08..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/Derivation.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal data class Derivation( - val networkName: String, - val standardType: String?, - val path: String, - val networkId: String?, - val onDerivationSelected: (Derivation) -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/EnterCustomDerivationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/EnterCustomDerivationState.kt deleted file mode 100644 index 3b6ca1c57a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/EnterCustomDerivationState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal data class EnterCustomDerivationState( - val value: String, - val onValueChange: (String) -> Unit, - val confirmButtonEnabled: Boolean, - val derivationIncorrect: Boolean, - val onConfirmButtonClick: () -> Unit, - val onDismiss: () -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/TextFieldState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/TextFieldState.kt deleted file mode 100644 index 3068a0b6e9..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/TextFieldState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state - -internal sealed class TextFieldState { - object Loading : TextFieldState() - - data class Editable( - val value: String, - val isEnabled: Boolean, - val error: AddCustomTokenWarning? = null, - val onValueChange: (String) -> Unit, - val onFocusExit: () -> Unit, - ) : TextFieldState() - - fun isInputValid(): Boolean = this is Editable && value.isNotBlank() && error == null - - fun isDisabled() = this is Editable && !this.isEnabled - - fun copySealed( - value: String = (this as? Editable)?.value ?: "", - isEnabled: Boolean = (this as? Editable)?.isEnabled ?: true, - error: AddCustomTokenWarning? = (this as? Editable)?.error, - onValueChange: (String) -> Unit = (this as? Editable)?.onValueChange ?: {}, - ): TextFieldState { - return when (this) { - is Editable -> this.copy( - value = value, - isEnabled = isEnabled, - error = error, - onValueChange = onValueChange, - ) - is Loading -> this - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt deleted file mode 100644 index 6ad1ac8e68..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateFactory.kt +++ /dev/null @@ -1,395 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.tokens.error.AddCustomTokenError -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.managetokens.presentation.common.state.* -import com.tangem.managetokens.presentation.addcustomtoken.state.* -import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents -import com.tangem.utils.Provider -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentSetOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.collections.immutable.toPersistentSet - -@Suppress("LargeClass") -internal class AddCustomTokenStateFactory( - private val currentStateProvider: Provider, - private val clickIntents: AddCustomTokenClickIntents, -) { - - fun getInitialState(): AddCustomTokenState { - return AddCustomTokenState( - chooseWalletState = ChooseWalletState.NoSelection, - chooseNetworkState = ChooseNetworkState( - networks = persistentListOf(), - selectedNetwork = null, - onChooseNetworkClick = clickIntents::onChooseNetworkClick, - onCloseChoosingNetworkClick = clickIntents::onCloseChoosingNetworkClick, - ), - chooseDerivationState = ChooseDerivationState( - derivations = persistentListOf(), - selectedDerivation = null, - enterCustomDerivationState = null, - onChooseDerivationClick = clickIntents::onChooseDerivationClick, - onCloseChoosingDerivationClick = clickIntents::onCloseChoosingDerivationClick, - onEnterCustomDerivation = clickIntents::onEnterCustomDerivation, - ), - tokenData = null, - warnings = persistentSetOf(), - addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick), - ) - } - - fun getFullState( - suitableUserWallets: List, - allUserWallets: List, - selectedWalletId: UserWalletId?, - supportedNetworks: List, - ): AddCustomTokenState { - val derivations = getListOfDerivations(supportedNetworks) - - val chooseDerivationState = createChooseDerivationState(derivations) - val networkConverter = NetworkToNetworkItemStateConverter(clickIntents::onNetworkSelected) - val networks = supportedNetworks.map { networkConverter.convert(it) } - - val chooseWalletState = getNewChooseWalletState(allUserWallets, suitableUserWallets, selectedWalletId) - - return AddCustomTokenState( - chooseWalletState = chooseWalletState, - chooseNetworkState = ChooseNetworkState( - networks = networks.toPersistentList(), - selectedNetwork = null, - onChooseNetworkClick = clickIntents::onChooseNetworkClick, - onCloseChoosingNetworkClick = clickIntents::onBack, - ), - chooseDerivationState = chooseDerivationState, - tokenData = null, - warnings = persistentSetOf(), - addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick), - ) - } - - private fun getListOfDerivations( - networksListToGenerateDerivations: List, - filterOnlyHardenedDerivations: Boolean = false, - ): List { - return networksListToGenerateDerivations.mapNotNull { network -> - network.derivationPath.value?.let { rawPath -> - Derivation( - networkName = network.name, - standardType = network.standardType.name, - path = rawPath, - networkId = network.backendId, - onDerivationSelected = clickIntents::onDerivationSelected, - ) - }.takeIf { derivation -> - if (filterOnlyHardenedDerivations) { - derivation?.let { allNodesHardened(createDerivationPathOrNull(it.path)) } ?: false - } else { - true - } - } - } - } - - private fun createChooseDerivationState(derivations: List): ChooseDerivationState? { - return if (derivations.isNotEmpty()) { - ChooseDerivationState( - derivations = derivations.toPersistentList(), - selectedDerivation = null, - enterCustomDerivationState = null, - onChooseDerivationClick = clickIntents::onChooseDerivationClick, - onCloseChoosingDerivationClick = clickIntents::onBack, - onEnterCustomDerivation = clickIntents::onEnterCustomDerivation, - ) - } else { - null - } - } - - fun updateWithNewWalletSelected( - selectedWalletId: UserWalletId, - supportedNetworks: List, - ): AddCustomTokenState { - val derivations = getListOfDerivations(supportedNetworks) - - val chooseDerivationState = createChooseDerivationState(derivations) - val networkConverter = NetworkToNetworkItemStateConverter(clickIntents::onNetworkSelected) - val networks = supportedNetworks.map { networkConverter.convert(it) } - - val currentWalletState = requireNotNull( - currentStateProvider().chooseWalletState as? ChooseWalletState.Choose, - ) { - "If user wallet was chosen, ChooseWalletState type must be Choose" - } - val selectedWalletState = currentWalletState.wallets.find { it.walletId == selectedWalletId.stringValue } - val chooseWalletState = currentWalletState.copy(selectedWallet = selectedWalletState) - - return AddCustomTokenState( - chooseWalletState = chooseWalletState, - chooseNetworkState = ChooseNetworkState( - networks = networks.toPersistentList(), - selectedNetwork = null, - onChooseNetworkClick = clickIntents::onChooseNetworkClick, - onCloseChoosingNetworkClick = clickIntents::onBack, - ), - chooseDerivationState = chooseDerivationState, - tokenData = null, - warnings = persistentSetOf(), - addTokenButton = ButtonState(isEnabled = false, onClick = clickIntents::onAddCustomButtonClick), - ) - } - - private fun getNewChooseWalletState( - suitableUserWallets: List, - allUserWallets: List, - selectedWalletId: UserWalletId?, - ): ChooseWalletState { - val chooseWalletState = if (suitableUserWallets.size == 1) { - ChooseWalletState.NoSelection - } else if (suitableUserWallets.isEmpty() && allUserWallets.all { !it.isMultiCurrency }) { - ChooseWalletState.Warning(ChooseWalletWarning.SINGLE_CURRENCY) - } else { - var selectedWalletState: WalletState? = null - ChooseWalletState.Choose( - wallets = suitableUserWallets.map { wallet -> - val walletState = WalletState( - walletId = wallet.walletId.stringValue, - artworkUrl = wallet.artworkUrl, - onSelected = clickIntents::onWalletSelected, - walletName = wallet.name, - ) - if (wallet.walletId.stringValue == selectedWalletId?.stringValue) { - selectedWalletState = walletState - } - walletState - }.toPersistentList(), - selectedWallet = requireNotNull(selectedWalletState), - onChooseWalletClick = clickIntents::onChooseWalletClick, - onCloseChoosingWalletClick = clickIntents::onCloseChoosingWalletClick, - ) - } - return chooseWalletState - } - - fun removeTokenAddressError(): AddCustomTokenState { - return addTokenAddressFieldError(null) - } - - private fun addTokenAddressFieldError(error: AddCustomTokenWarning?): AddCustomTokenState { - val tokenData = currentStateProvider().tokenData ?: return currentStateProvider() - val contractAddressField = tokenData.contractAddressTextField.copySealed(error = error) - return currentStateProvider().copy(tokenData = tokenData.copy(contractAddressTextField = contractAddressField)) - } - - private fun unlockAndClearNameSymbolAndDecimals(state: AddCustomTokenState): AddCustomTokenState { - val currentTokenData = state.tokenData - return state.copy( - tokenData = currentTokenData?.copy( - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ), - ) - } - - fun getStateAndTriggerEvent( - state: AddCustomTokenState, - event: Event, - setUiState: (AddCustomTokenState) -> Unit, - ): AddCustomTokenState { - return state.copy( - event = triggeredEvent( - data = event, - onConsume = { - val currentState = currentStateProvider() - setUiState(currentState.copy(event = consumedEvent())) - }, - ), - ) - } - - fun updateStateOnNetworkSelected( - networkItemState: NetworkItemState, - supportsTokens: Boolean, - networks: List, - requiresHardenedDerivationOnly: Boolean, - ): AddCustomTokenState { - val uiState = currentStateProvider() - val tokenData = if (supportsTokens) { - uiState.tokenData ?: CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = false, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) - } else { - null - } - - val derivations = getListOfDerivations(networks, requiresHardenedDerivationOnly) - val chooseDerivationState = createChooseDerivationState(derivations) - - return uiState.copy( - chooseNetworkState = uiState.chooseNetworkState.copy( - selectedNetwork = networkItemState, - ), - chooseDerivationState = chooseDerivationState, - tokenData = tokenData, - addTokenButton = uiState.addTokenButton.copy(isEnabled = true), - ) - } - - fun updateOnCustomDerivationSelected(): AddCustomTokenState { - val uiState = currentStateProvider() - return uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = null, - selectedDerivation = Derivation( - networkName = "", - path = uiState.chooseDerivationState.enterCustomDerivationState?.value ?: "", - networkId = null, - standardType = null, - onDerivationSelected = clickIntents::onDerivationSelected, - ), - ), - ) - } - - fun updateStateOnEnterCustomDerivation(): AddCustomTokenState { - val customDerivationState = EnterCustomDerivationState( - value = "", - onValueChange = clickIntents::onCustomDerivationChange, - confirmButtonEnabled = false, - derivationIncorrect = false, - onConfirmButtonClick = clickIntents::onCustomDerivationSelected, - onDismiss = clickIntents::onCustomDerivationDialogDismissed, - ) - val uiState = currentStateProvider() - return uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = customDerivationState, - ), - ) - } - - fun updateOnCustomDerivationEntered(input: String, requiresHardenedDerivationOnly: Boolean): AddCustomTokenState { - val uiState = currentStateProvider() - val path = createDerivationPathOrNull(input) - val isWrongDerivationForWallet2 = isWrongDerivationForWallet2( - requiresHardenedDerivationOnly = requiresHardenedDerivationOnly, - derivationPath = path, - ) - val enterDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( - confirmButtonEnabled = path != null && !isWrongDerivationForWallet2, - derivationIncorrect = input.isNotBlank() && path == null || isWrongDerivationForWallet2, - ) - return uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = enterDerivationState, - ), - ) - } - - private fun isWrongDerivationForWallet2( - requiresHardenedDerivationOnly: Boolean, - derivationPath: DerivationPath?, - ): Boolean { - return if (requiresHardenedDerivationOnly) { - !allNodesHardened(derivationPath) - } else { - false - } - } - - private fun allNodesHardened(derivationPath: DerivationPath?): Boolean { - return derivationPath?.nodes?.all { it.isHardened } ?: false - } - - private fun createDerivationPathOrNull(rawPath: String): DerivationPath? { - return try { - DerivationPath(rawPath) - } catch (error: Throwable) { - null - } - } - - fun updateStateOnLoadingTokenInfo(contractAddress: String): AddCustomTokenState { - return currentStateProvider().copy( - tokenData = CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = contractAddress, - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - - ), - nameTextField = TextFieldState.Loading, - symbolTextField = TextFieldState.Loading, - decimalsTextField = TextFieldState.Loading, - ), - ) - } - - fun handleAddressError(error: AddCustomTokenError): AddCustomTokenState { - val uiState = currentStateProvider() - return when (error) { - AddCustomTokenError.INVALID_CONTRACT_ADDRESS -> { - removeTokenAddressError().also { unlockAndClearNameSymbolAndDecimals(it) } - .copy( - addTokenButton = uiState.addTokenButton.copy(isEnabled = false), - warnings = uiState.warnings - .filterNot { it is AddCustomTokenWarning.PotentialScamToken } - .toPersistentSet(), - ) - } - AddCustomTokenError.FIELD_IS_EMPTY -> - removeTokenAddressError().also { unlockAndClearNameSymbolAndDecimals(it) } - .copy( - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - ) - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt deleted file mode 100644 index e6e1b2f84e..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/AddCustomTokenStateToCryptoCurrencyConverter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.data.tokens.utils.CryptoCurrencyFactory -import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.managetokens.presentation.addcustomtoken.state.AddCustomTokenState -import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState -import com.tangem.utils.converter.Converter - -internal class AddCustomTokenStateToCryptoCurrencyConverter( - private val derivationStyleProvider: DerivationStyleProvider, -) : Converter { - - override fun convert(value: AddCustomTokenState): CryptoCurrency { - val derivationPath = value.chooseDerivationState?.selectedDerivation?.path - val token = parseTokenOrNull(value.tokenData) - - val cryptoCurrency = if (token != null) { - CryptoCurrencyFactory().createToken( - token = token, - networkId = value.chooseNetworkState.selectedNetwork?.id ?: "", - derivationStyleProvider = derivationStyleProvider, - extraDerivationPath = derivationPath, - ) - } else { - CryptoCurrencyFactory().createCoin( - networkId = value.chooseNetworkState.selectedNetwork?.id ?: "", - derivationStyleProvider = derivationStyleProvider, - extraDerivationPath = derivationPath, - ) - } - return requireNotNull(cryptoCurrency) { - "Unless network is not Unknown blockchain, CryptoCurrency cannot be null" - } - } - - @Suppress("ComplexCondition") - private fun parseTokenOrNull(tokenData: CustomTokenData?): CryptoCurrencyFactory.Token? { - val contractAddress = (tokenData?.contractAddressTextField as? TextFieldState.Editable)?.value - val symbol = (tokenData?.symbolTextField as? TextFieldState.Editable)?.value - val name = (tokenData?.nameTextField as? TextFieldState.Editable)?.value - val decimals = (tokenData?.decimalsTextField as? TextFieldState.Editable)?.value?.toIntOrNull() - return if ( - !contractAddress.isNullOrBlank() && !symbol.isNullOrBlank() && !name.isNullOrBlank() && decimals != null - ) { - CryptoCurrencyFactory.Token( - symbol = symbol, - name = name, - contractAddress = contractAddress, - decimals = decimals, - id = null, - ) - } else { - null - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/ContractAddressToCustomTokenDataConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/ContractAddressToCustomTokenDataConverter.kt deleted file mode 100644 index eb0897d370..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/ContractAddressToCustomTokenDataConverter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState -import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents -import com.tangem.utils.converter.Converter - -internal class ContractAddressToCustomTokenDataConverter( - private val clickIntents: AddCustomTokenClickIntents, -) : Converter { - override fun convert(value: String): CustomTokenData { - return CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = value, - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = "", - isEnabled = true, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/FoundTokenToCustomTokenDataConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/FoundTokenToCustomTokenDataConverter.kt deleted file mode 100644 index 65921700dc..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/FoundTokenToCustomTokenDataConverter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.domain.tokens.model.FoundToken -import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState -import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenClickIntents -import com.tangem.utils.converter.Converter - -internal class FoundTokenToCustomTokenDataConverter( - private val clickIntents: AddCustomTokenClickIntents, -) : Converter { - override fun convert(value: FoundToken): CustomTokenData { - return CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = value.contractAddress, - isEnabled = true, - onValueChange = clickIntents::onContractAddressChange, - onFocusExit = clickIntents::onContractAddressFocusExit, - ), - nameTextField = TextFieldState.Editable( - value = value.name, - isEnabled = false, - onValueChange = clickIntents::onTokenNameChange, - onFocusExit = clickIntents::onTokenNameFocusExit, - ), - symbolTextField = TextFieldState.Editable( - value = value.symbol, - isEnabled = false, - onValueChange = clickIntents::onSymbolChange, - onFocusExit = clickIntents::onSymbolFocusExit, - ), - decimalsTextField = TextFieldState.Editable( - value = value.decimals.toString(), - isEnabled = false, - onValueChange = clickIntents::onDecimalsChange, - onFocusExit = clickIntents::onDecimalsFocusExit, - ), - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/NetworkToNetworkItemStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/NetworkToNetworkItemStateConverter.kt deleted file mode 100644 index 74b4eda971..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/factory/NetworkToNetworkItemStateConverter.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.factory - -import com.tangem.core.ui.extensions.getActiveIconResByNetworkId -import com.tangem.domain.tokens.model.Network -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.utils.converter.Converter - -internal class NetworkToNetworkItemStateConverter( - private val onNetworkItemSelected: (NetworkItemState) -> Unit, -) : Converter { - override fun convert(value: Network): NetworkItemState { - return NetworkItemState.Selectable( - name = value.name, - protocolName = value.standardType.name, - iconResId = getActiveIconResByNetworkId(value.backendId), - id = value.backendId, - onNetworkClick = onNetworkItemSelected, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/AddCustomTokenPreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/AddCustomTokenPreviewData.kt deleted file mode 100644 index 96e7bf7379..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/AddCustomTokenPreviewData.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata - -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.WalletState -import com.tangem.managetokens.presentation.addcustomtoken.state.* -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentSetOf - -internal object AddCustomTokenPreviewData { - val state = AddCustomTokenState( - chooseWalletState = ChooseWalletState.Choose( - wallets = persistentListOf(), - selectedWallet = WalletState( - "", - "", - "My Wallet", - {}, - ), - onChooseWalletClick = { }, - onCloseChoosingWalletClick = { }, - ), - chooseNetworkState = ChooseNetworkState( - networks = persistentListOf(), - selectedNetwork = null, - onChooseNetworkClick = { }, - onCloseChoosingNetworkClick = {}, - ), - chooseDerivationState = ChooseDerivationState( - derivations = persistentListOf(), - selectedDerivation = null, - enterCustomDerivationState = null, - onChooseDerivationClick = { }, - onCloseChoosingDerivationClick = {}, - onEnterCustomDerivation = {}, - ), - tokenData = CustomTokenData( - contractAddressTextField = TextFieldState.Editable( - value = "0x4ace7262705b68bcba5b91de96889349394", - isEnabled = false, - onValueChange = {}, - onFocusExit = {}, - ), - nameTextField = TextFieldState.Loading, - symbolTextField = TextFieldState.Loading, - decimalsTextField = TextFieldState.Loading, - ), - warnings = persistentSetOf(), - addTokenButton = ButtonState(isEnabled = true, onClick = {}), - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseDerivationPreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseDerivationPreviewData.kt deleted file mode 100644 index c4cfd15483..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseDerivationPreviewData.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata - -import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseDerivationState -import com.tangem.managetokens.presentation.addcustomtoken.state.Derivation -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -internal object ChooseDerivationPreviewData { - private val derivations: ImmutableList = persistentListOf( - Derivation( - networkName = "Ethereum", - path = "m/44’/9001’/0’/0/0", - networkId = "ethereum", - standardType = "ERC", - onDerivationSelected = {}, - ), - Derivation( - networkName = "Polygon", - path = "m/44’/9001’/0’/0/0", - networkId = "polygon", - standardType = "ERC", - onDerivationSelected = {}, - ), - Derivation( - networkName = "Avalanche", - path = "m/44’/9001’/0’/0/0", - networkId = "avalanche", - standardType = "ERC", - onDerivationSelected = {}, - ), - ) - - val state = ChooseDerivationState( - derivations = derivations, - selectedDerivation = derivations.first(), - enterCustomDerivationState = null, - onEnterCustomDerivation = {}, - onCloseChoosingDerivationClick = {}, - onChooseDerivationClick = {}, - show = true, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseNetworkCustomPreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseNetworkCustomPreviewData.kt deleted file mode 100644 index 08987dbadf..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/state/previewdata/ChooseNetworkCustomPreviewData.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.state.previewdata - -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseNetworkState -import kotlinx.collections.immutable.persistentListOf - -internal object ChooseNetworkCustomPreviewData { - val networks = persistentListOf( - NetworkItemState.Selectable( - name = "Ethereum", - protocolName = "ETH", - iconResId = R.drawable.img_kusama_22, - id = "ethereum", - onNetworkClick = { }, - ), - NetworkItemState.Selectable( - name = "BNB SMART CHAIN", - protocolName = "BEP20", - iconResId = R.drawable.ic_bsc_16, - id = "binance smart chain", - onNetworkClick = { }, - ), - ) - val state = ChooseNetworkState( - networks, - selectedNetwork = networks.first(), - onCloseChoosingNetworkClick = {}, - onChooseNetworkClick = {}, - show = true, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt deleted file mode 100644 index 69fe567327..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenBottomSheet.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import android.annotation.SuppressLint -import androidx.activity.compose.BackHandler -import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.systemBars -import androidx.compose.material3.BottomSheetDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.ModalBottomSheetDefaults -import androidx.compose.material3.ModalBottomSheetProperties -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.KeyEventType -import androidx.compose.ui.input.key.key -import androidx.compose.ui.input.key.onPreviewKeyEvent -import androidx.compose.ui.input.key.type -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader -import com.tangem.core.ui.components.bottomsheets.collapse -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRoute -import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRouter -import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenViewModel -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig) { - val viewModel = hiltViewModel() - - var isVisible by remember { mutableStateOf(value = config.isShow) } - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - - if (isVisible) { - // ViewModel cannot be scoped to ModalBottomSheet's lifecycle, - // so we have to manually initialize and dispose its state when bottom sheet enters and leaves a composition - DisposableEffect(viewModel) { - viewModel.onInitialize() - onDispose { viewModel.onDispose() } - } - - ModalBottomSheetWithBackHandling( - onDismissRequest = config.onDismissRequest, - sheetState = sheetState, - containerColor = TangemTheme.colors.background.tertiary, - shape = TangemTheme.shapes.bottomSheetLarge, - windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top), - dragHandle = { TangemBottomSheetDraggableHeader(color = TangemTheme.colors.background.tertiary) }, - properties = ModalBottomSheetDefaults.properties(shouldDismissOnBackPress = false), - ) { - Content(onDismissRequest = config.onDismissRequest, viewModel = viewModel) - } - } - - LaunchedEffect(key1 = config.isShow) { - if (config.isShow) { - isVisible = true - } else { - sheetState.collapse { isVisible = false } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ModalBottomSheetWithBackHandling( - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - containerColor: Color = BottomSheetDefaults.ContainerColor, - shape: Shape = BottomSheetDefaults.ExpandedShape, - windowInsets: WindowInsets = BottomSheetDefaults.windowInsets, - dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, - sheetState: SheetState = rememberModalBottomSheetState(), - properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties(), - content: @Composable ColumnScope.() -> Unit, -) { - val scope = rememberCoroutineScope() - - BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) { - // Always catch back here, but only let it dismiss if shouldDismissOnBackPress. - // If not, it will have no effect. - if (properties.shouldDismissOnBackPress) { - scope.launch { sheetState.hide() }.invokeOnCompletion { - if (!sheetState.isVisible) { - onDismissRequest() - } - } - } - } - - val requester = remember { FocusRequester() } - val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current - - ModalBottomSheet( - onDismissRequest = onDismissRequest, - containerColor = containerColor, - shape = shape, - windowInsets = windowInsets, - dragHandle = dragHandle, - sheetState = sheetState, - modifier = modifier - .focusRequester(requester) - .focusable() - .onPreviewKeyEvent { - if (it.key == Key.Back && it.type == KeyEventType.KeyUp && !it.nativeKeyEvent.isCanceled) { - backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed() - return@onPreviewKeyEvent true - } - return@onPreviewKeyEvent false - }, - properties = ModalBottomSheetDefaults.properties( - securePolicy = properties.securePolicy, - isFocusable = properties.isFocusable, - // Set false otherwise the onPreviewKeyEvent doesn't work at all. - // The functionality of shouldDismissOnBackPress is achieved by the BackHandler. - shouldDismissOnBackPress = false, - ), - content = content, - ) - - LaunchedEffect(Unit) { - requester.requestFocus() - } -} - -@SuppressLint("RestrictedApi") -@Composable -private fun Content(viewModel: AddCustomTokenViewModel, onDismissRequest: () -> Unit) { - val navController = rememberNavController() - - LaunchedEffect(navController) { - navController.currentBackStack - .collect { - if (it.isEmpty()) { - onDismissRequest() - } - } - } - - BackHandler(true) { - navController.popBackStack() - } - - val router = remember(navController) { AddCustomTokenRouter(navController) } - - viewModel.router = router - - NavHost( - modifier = Modifier.fillMaxSize(), - navController = navController, - startDestination = AddCustomTokenRoute.Main.route, - ) { - composable( - route = AddCustomTokenRoute.Main.route, - ) { - AddCustomTokenScreen(state = viewModel.uiState) - } - composable( - route = AddCustomTokenRoute.ChooseNetwork.route, - ) { - ChooseNetworkCustomScreen(state = viewModel.uiState.chooseNetworkState) - } - composable( - route = AddCustomTokenRoute.ChooseDerivation.route, - ) { - ChooseDerivationScreen(state = requireNotNull(viewModel.uiState.chooseDerivationState)) - } - composable( - route = AddCustomTokenRoute.ChooseWallet.route, - ) { - CustomTokensChooseWalletScreen(state = viewModel.uiState.chooseWalletState as ChooseWalletState.Choose) - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt deleted file mode 100644 index d5691e9017..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/AddCustomTokenScreen.kt +++ /dev/null @@ -1,298 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet -import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig -import com.tangem.managetokens.presentation.common.ui.EventEffect -import com.tangem.managetokens.presentation.common.ui.components.Alert -import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock -import com.tangem.managetokens.presentation.addcustomtoken.state.AddCustomTokenState -import com.tangem.managetokens.presentation.addcustomtoken.state.CustomTokenData -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState -import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.AddCustomTokenPreviewData - -@Composable -internal fun AddCustomTokenScreen(state: AddCustomTokenState, modifier: Modifier = Modifier) { - var alertState by remember { mutableStateOf(value = null) } - - EventEffect( - event = state.event, - onAlertStateSet = { alertState = it }, - ) - alertState?.let { - Alert(state = it, onDismiss = { alertState = null }) - } - Content(state = state, modifier = modifier) -} - -@Composable -private fun Content(state: AddCustomTokenState, modifier: Modifier = Modifier) { - val keyboard by keyboardAsState() - - Column( - modifier = modifier - .background(color = TangemTheme.colors.background.tertiary) - .statusBarsPadding() - .navigationBarsPadding() - .imePadding() - .padding( - top = TangemTheme.dimens.spacing10, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing18, - ) - .fillMaxWidth(), - ) { - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_title), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing10), - ) - Text( - text = stringResource(id = R.string.custom_token_subtitle), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = TangemTheme.dimens.spacing16), - ) - CustomTokenItemsList(state = state) - if (keyboard is Keyboard.Closed) { - PrimaryButton( - text = stringResource(id = R.string.custom_token_add_token), - onClick = state.addTokenButton.onClick, - enabled = state.addTokenButton.isEnabled, - modifier = Modifier.fillMaxWidth(), - ) - } - } - - if (state.chooseWalletState is ChooseWalletState.Choose && state.chooseWalletState.show) { - val config = TangemBottomSheetConfig( - isShow = true, - content = ChooseWalletBottomSheetConfig(state.chooseWalletState), - onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick, - ) - ChooseWalletBottomSheet(config) - } -} - -@Composable -private fun ColumnScope.CustomTokenItemsList(state: AddCustomTokenState, modifier: Modifier = Modifier) { - LazyColumn( - modifier - .fillMaxWidth() - .weight(1f), - ) { - if (state.chooseWalletState is ChooseWalletState.Choose) { - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.manage_tokens_network_selector_wallet), - subtitle = state.chooseWalletState.selectedWallet?.walletName ?: "", - onClick = state.chooseWalletState.onChooseWalletClick, - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } - } - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.custom_token_network_input_title), - subtitle = state.chooseNetworkState.selectedNetwork?.name - ?: stringResource(id = R.string.manage_tokens_network_selector_title), - onClick = state.chooseNetworkState.onChooseNetworkClick, - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } - if (state.tokenData != null) { - item { - TokenFields( - state = state.tokenData, - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } - } - if (state.chooseDerivationState != null) { - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.custom_token_derivation_path), - subtitle = state.chooseDerivationState.selectedDerivation?.path - ?: stringResource(id = R.string.custom_token_derivation_path_default), - onClick = state.chooseDerivationState.onChooseDerivationClick, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) - } - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun TokenFields(state: CustomTokenData, modifier: Modifier = Modifier) { - val focusManager = LocalFocusManager.current - - Column( - modifier = modifier - .fillMaxWidth() - .clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(color = TangemTheme.colors.background.action), - ) { - TokenField( - textFieldState = state.contractAddressTextField, - placeholder = "0x0000000000000000000000000000000", - title = R.string.custom_token_contract_address_input_title, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = if (state.isNameSymbolDecimalsDisabled()) { - ImeAction.Done - } else { - ImeAction.Next - }, - ), - onImeAction = { - if (state.isNameSymbolDecimalsDisabled()) { - focusManager.moveFocus(FocusDirection.Exit) - } else { - focusManager.moveFocus(FocusDirection.Down) - } - }, - ) - TokenField( - textFieldState = state.nameTextField, - placeholder = stringResource(id = R.string.custom_token_name_input_placeholder), - title = R.string.custom_token_name_input_title, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next, - ), - onImeAction = { focusManager.moveFocus(FocusDirection.Down) }, - ) - TokenField( - textFieldState = state.symbolTextField, - placeholder = stringResource(id = R.string.custom_token_token_symbol_input_placeholder), - title = R.string.custom_token_token_symbol_input_title, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next, - ), - onImeAction = { focusManager.moveFocus(FocusDirection.Down) }, - ) - TokenField( - textFieldState = state.decimalsTextField, - placeholder = "0", - title = R.string.custom_token_decimals_input_title, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done, - ), - onImeAction = { focusManager.moveFocus(FocusDirection.Exit) }, - ) - } -} - -@Composable -private fun TokenField( - textFieldState: TextFieldState, - placeholder: String, - title: Int, - onImeAction: () -> Unit, - keyboardOptions: KeyboardOptions, -) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing16), - ) { - TokenTextFieldTitle( - state = textFieldState, - title = stringResource(id = title), - ) - when (textFieldState) { - is TextFieldState.Editable -> TokenTextField( - state = textFieldState, - placeholder = placeholder, - onImeAction = onImeAction, - keyboardOptions = keyboardOptions, - ) - is TextFieldState.Loading -> TokenShimmer() - } - } -} - -@Composable -private fun TokenShimmer() { - RectangleShimmer( - radius = TangemTheme.dimens.radius3, - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing4) - .size( - height = TangemTheme.dimens.size12, - width = TangemTheme.dimens.size90, - ), - ) -} - -@Composable -private fun TokenTextFieldTitle(state: TextFieldState?, title: String) { - val modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing4) - val error = (state as? TextFieldState.Editable)?.error - if (error == null) { - Text( - text = title, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.subtitle1, - modifier = modifier, - ) - } else { - Text( - text = error.title.resolveReference(), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.subtitle1, - modifier = modifier, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseDerivationScreen() { - TangemThemePreview { - AddCustomTokenScreen(state = AddCustomTokenPreviewData.state) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt deleted file mode 100644 index a7805cbb96..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseDerivationScreen.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock -import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseDerivationState -import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.ChooseDerivationPreviewData - -@Composable -internal fun ChooseDerivationScreen(state: ChooseDerivationState, modifier: Modifier = Modifier) { - if (state.enterCustomDerivationState != null) { - CustomDerivationDialog(state.enterCustomDerivationState) - } - - Column( - modifier = modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.tertiary) - .statusBarsPadding() - .navigationBarsPadding() - .padding( - top = TangemTheme.dimens.spacing10, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing18, - ), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = TangemTheme.dimens.size44) - .padding(bottom = TangemTheme.dimens.spacing12), - contentAlignment = Alignment.CenterStart, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - tint = TangemTheme.colors.icon.primary1, - modifier = Modifier - .clickable { state.onCloseChoosingDerivationClick() }, - ) - Text( - text = stringResource(id = R.string.custom_token_derivation_path), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing24) - .align(Alignment.Center), - ) - } - DerivationsList(state) - } -} - -@Composable -private fun DerivationsList(state: ChooseDerivationState) { - LazyColumn( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16), - contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing12), - ) { - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.custom_token_custom_derivation), - subtitle = stringResource(id = R.string.custom_token_custom_derivation), - onClick = state.onEnterCustomDerivation, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) - } - items(state.derivations.count()) { index -> - val derivationItem = state.derivations[index] - SimpleSelectionBlock( - title = derivationItem.networkName, - subtitle = derivationItem.path, - onClick = { derivationItem.onDerivationSelected(derivationItem) }, - modifier = Modifier.roundedShapeItemDecoration( - currentIndex = index, - lastIndex = state.derivations.lastIndex, - addDefaultPadding = false, - ), - roundedCorners = false, - ) - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseDerivationScreen() { - TangemThemePreview { - ChooseDerivationScreen(state = ChooseDerivationPreviewData.state) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt deleted file mode 100644 index 6777fc4fbc..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/ChooseNetworkCustomScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.ui.components.NetworkItem -import com.tangem.managetokens.presentation.addcustomtoken.state.ChooseNetworkState -import com.tangem.managetokens.presentation.addcustomtoken.state.previewdata.ChooseNetworkCustomPreviewData - -@Composable -internal fun ChooseNetworkCustomScreen(state: ChooseNetworkState, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.tertiary) - .statusBarsPadding() - .navigationBarsPadding() - .padding( - top = TangemTheme.dimens.spacing10, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing18, - ), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = TangemTheme.dimens.size44) - .padding(bottom = TangemTheme.dimens.spacing12), - contentAlignment = Alignment.CenterStart, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - tint = TangemTheme.colors.icon.primary1, - modifier = Modifier - .clickable { state.onCloseChoosingNetworkClick() }, - ) - Text( - text = stringResource(id = R.string.custom_token_network_selector_title), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing24) - .align(Alignment.Center), - ) - } - LazyColumn( - contentPadding = PaddingValues(vertical = TangemTheme.dimens.spacing12), - ) { - items(state.networks.count()) { index -> - NetworkItem( - state = state.networks[index], - tokenState = null, - isSelected = state.selectedNetwork == state.networks[index], - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = state.networks.lastIndex, - addDefaultPadding = false, - ), - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseNetworkScreen() { - TangemThemePreview { - ChooseNetworkCustomScreen(ChooseNetworkCustomPreviewData.state) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomDerivationDialog.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomDerivationDialog.kt deleted file mode 100644 index 969d7f61fb..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomDerivationDialog.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.AdditionalTextInputDialogParams -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.components.TextInputDialog -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.addcustomtoken.state.EnterCustomDerivationState - -@Composable -internal fun CustomDerivationDialog(state: EnterCustomDerivationState) { - val confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), - enabled = state.confirmButtonEnabled, - onClick = state.onConfirmButtonClick, - ) - - val dismissButton = DialogButton( - title = stringResource(id = R.string.common_cancel), - onClick = state.onDismiss, - ) - - val params = AdditionalTextInputDialogParams( - placeholder = stringResource(id = R.string.custom_token_custom_derivation_placeholder), - isError = state.derivationIncorrect, - errorText = if (state.derivationIncorrect) { - stringResource(R.string.custom_token_invalid_derivation_path) - } else { - null - }, - ) - - TextInputDialog( - fieldValue = state.value, - confirmButton = confirmButton, - onDismissDialog = state.onDismiss, - onValueChange = state.onValueChange, - textFieldParams = params, - title = stringResource(id = R.string.custom_token_custom_derivation_title), - dismissButton = dismissButton, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomTokenChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomTokenChooseWalletScreen.kt deleted file mode 100644 index 3b8eb7c2af..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/CustomTokenChooseWalletScreen.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.ui.ChooseWalletScreen - -@Composable -internal fun CustomTokensChooseWalletScreen(state: ChooseWalletState.Choose) { - ChooseWalletScreen( - state = state, - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding(), - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/TokenTextField.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/TokenTextField.kt deleted file mode 100644 index 63521c6775..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/ui/TokenTextField.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.ui - -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.input.key.* -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.addcustomtoken.state.TextFieldState - -@Composable -internal fun TokenTextField( - state: TextFieldState.Editable, - placeholder: String, - onImeAction: () -> Unit, - keyboardOptions: KeyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Default, - ), -) { - val isInitiallyComposed = remember { mutableStateOf(false) } - LaunchedEffect(key1 = true) { - isInitiallyComposed.value = true - } - - BasicTextField( - value = state.value, - onValueChange = state.onValueChange, - keyboardOptions = keyboardOptions, - singleLine = true, - maxLines = 1, - textStyle = TangemTheme.typography.subtitle1.copy( - fontWeight = FontWeight.Normal, - color = if (state.isEnabled) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.disabled, - ), - cursorBrush = SolidColor(TangemTheme.colors.icon.primary1), - modifier = Modifier - .fillMaxWidth() - .onKeyEvent { keyEvent -> - if (keyEvent.type == KeyEventType.KeyUp && keyEvent.key == Key.Enter) { - onImeAction() - true - } else { - false - } - } - .onFocusChanged { - if (!it.isFocused && isInitiallyComposed.value) { - state.onFocusExit() - } - }, - decorationBox = { innerTextField -> - Row(modifier = Modifier.fillMaxWidth()) { - if (state.value.isEmpty()) { - Text( - text = placeholder, - color = if (state.isEnabled) { - TangemTheme.colors.text.tertiary - } else { - TangemTheme.colors.text.disabled - }, - style = TangemTheme.typography.body2, - ) - } - } - innerTextField() - }, - enabled = state.isEnabled, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenClickIntents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenClickIntents.kt deleted file mode 100644 index 5a2e8e299b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenClickIntents.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.viewmodels - -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.addcustomtoken.state.Derivation - -@Suppress("TooManyFunctions") -internal interface AddCustomTokenClickIntents { - - fun onNetworkSelected(networkItemState: NetworkItemState) - - fun onChooseNetworkClick() - - fun onCloseChoosingNetworkClick() - - fun onWalletSelected(walletId: String) - - fun onChooseWalletClick() - - fun onCloseChoosingWalletClick() - - fun onContractAddressChange(input: String) - - fun onTokenNameChange(input: String) - - fun onSymbolChange(input: String) - - fun onDecimalsChange(input: String) - - fun onContractAddressFocusExit() - - fun onTokenNameFocusExit() - - fun onSymbolFocusExit() - - fun onDecimalsFocusExit() - - fun onDerivationSelected(derivation: Derivation) - - fun onChooseDerivationClick() - - fun onCloseChoosingDerivationClick() - - fun onEnterCustomDerivation() - - fun onCustomDerivationChange(input: String) - - fun onCustomDerivationSelected() - - fun onCustomDerivationDialogDismissed() - - fun onAddCustomButtonClick() - - fun onBack() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt deleted file mode 100644 index 44e55b947d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/addcustomtoken/viewmodels/AddCustomTokenViewModel.kt +++ /dev/null @@ -1,484 +0,0 @@ -package com.tangem.managetokens.presentation.addcustomtoken.viewmodels - -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import arrow.core.getOrElse -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase -import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRouter -import com.tangem.managetokens.presentation.common.analytics.ManageTokens -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.Event -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.addcustomtoken.state.* -import com.tangem.managetokens.presentation.addcustomtoken.state.factory.AddCustomTokenStateToCryptoCurrencyConverter -import com.tangem.managetokens.presentation.addcustomtoken.state.factory.ContractAddressToCustomTokenDataConverter -import com.tangem.managetokens.presentation.addcustomtoken.state.factory.AddCustomTokenStateFactory -import com.tangem.managetokens.presentation.addcustomtoken.state.factory.FoundTokenToCustomTokenDataConverter -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.Debouncer -import com.tangem.utils.coroutines.Debouncer.Companion.DEFAULT_WAIT_TIME_MS -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.distinctUntilChanged -import javax.inject.Inject -import kotlin.properties.Delegates - -@Suppress("LongParameterList", "TooManyFunctions", "LargeClass") -@Stable -@HiltViewModel -internal class AddCustomTokenViewModel @Inject constructor( - private val dispatchers: CoroutineDispatcherProvider, - private val getWalletsUseCase: GetWalletsUseCase, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, - private val selectWalletUseCase: SelectWalletUseCase, - private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, - private val findTokenByContractAddressUseCase: FindTokenByContractAddressUseCase, - private val validateContractAddressUseCase: ValidateContractAddressUseCase, - private val getNetworksSupportedByWallet: GetNetworksSupportedByWallet, - private val areTokensSupportedByNetworkUseCase: AreTokensSupportedByNetworkUseCase, - private val requiresHardenedDerivationOnlyUseCase: RequiresHardenedDerivationOnlyUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, -) : ViewModel(), AddCustomTokenClickIntents, DefaultLifecycleObserver { - - private val debouncer = Debouncer() - - private val stateFactory = AddCustomTokenStateFactory( - currentStateProvider = Provider { uiState }, - clickIntents = this, - ) - - var router: AddCustomTokenRouter by Delegates.notNull() - - var uiState: AddCustomTokenState by mutableStateOf(stateFactory.getInitialState()) - private set - - /** - * Called when the bottom sheet was opened - */ - fun onInitialize() { - viewModelScope.launch(dispatchers.io) { - getWalletsUseCase() - .distinctUntilChanged() - .collectLatest { userWallets -> - val suitableUserWallets = userWallets.filter { it.isMultiCurrency && !it.isLocked } - val selectedWalletId = selectSuitableWallet(suitableUserWallets) - val networks = selectedWalletId?.let { getSupportedNetworks(selectedWalletId) } ?: emptyList() - withContext(dispatchers.main) { - uiState = stateFactory.getFullState( - allUserWallets = suitableUserWallets, - suitableUserWallets = userWallets, - selectedWalletId = selectedWalletId, - supportedNetworks = networks, - ) - } - } - } - } - - /** - * Called after the bottom sheet is closed - */ - fun onDispose() { - // We have to manually cancel viewModelScope's child jobs when bottom sheet is closed - viewModelScope.coroutineContext.cancelChildren() - // and reset state - uiState = stateFactory.getInitialState() - } - - private suspend fun selectSuitableWallet(suitableUserWallets: List): UserWalletId? { - val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() - val selectedWalletId = if (walletSupportsAddingTokens(selectedWallet) && suitableUserWallets.isNotEmpty()) { - val walletId = suitableUserWallets.first().walletId - selectWalletUseCase(walletId) - walletId - } else { - selectedWallet?.walletId - } - return selectedWalletId - } - - private fun walletSupportsAddingTokens(userWallet: UserWallet?): Boolean { - return userWallet != null && userWallet.isMultiCurrency && !userWallet.isLocked - } - - private suspend fun getSupportedNetworks(userWalletId: UserWalletId): List { - return getNetworksSupportedByWallet(userWalletId).fold( - ifLeft = { emptyList() }, - ifRight = { it }, - ) - } - - override fun onNetworkSelected(networkItemState: NetworkItemState) { - analyticsEventHandler.send(ManageTokens.CustomTokenNetworkSelected(networkItemState.name)) - selectNetwork(networkItemState) - router.popBackStack() - } - - private fun selectNetwork(networkItemState: NetworkItemState) { - viewModelScope.launch(dispatchers.io) { - // TODO [REDACTED_TASK_KEY] - val selectedWalletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId ?: return@launch - val supportsTokens = areTokensSupportedByNetworkUseCase( - networkId = networkItemState.id, - userWalletId = selectedWalletId, - ).getOrNull() ?: false - - val networksForDerivations = getSupportedNetworks(selectedWalletId) - - withContext(dispatchers.main) { - uiState = stateFactory.updateStateOnNetworkSelected( - networkItemState = networkItemState, - supportsTokens = supportsTokens, - networks = networksForDerivations, - requiresHardenedDerivationOnly = requiresHardenedDerivationOnly( - networkId = networkItemState.id, - userWalletId = selectedWalletId, - ), - ) - } - } - } - - override fun onChooseNetworkClick() { - router.openCustomTokenChooseNetwork() - } - - override fun onCloseChoosingNetworkClick() { - router.popBackStack() - } - - override fun onWalletSelected(walletId: String) { - analyticsEventHandler.send(ManageTokens.WalletSelected(ManageTokens.WalletSelected.Source.CustomToken)) - viewModelScope.launch(dispatchers.io) { - val userWalletId = UserWalletId(walletId) - selectWalletUseCase(userWalletId) - val supportedNetworks = getSupportedNetworks(UserWalletId(walletId)) - - withContext(dispatchers.main) { - uiState = stateFactory.updateWithNewWalletSelected( - selectedWalletId = userWalletId, - supportedNetworks = supportedNetworks, - ) - router.popBackStack() - } - } - } - - override fun onChooseWalletClick() { - router.openCustomTokenChooseWallet() - } - - override fun onCloseChoosingWalletClick() { - router.popBackStack() - } - - override fun onContractAddressChange(input: String) { - uiState = uiState.copy( - tokenData = uiState.tokenData?.copy( - contractAddressTextField = TextFieldState.Editable( - value = input, - isEnabled = true, - onValueChange = this::onContractAddressChange, - onFocusExit = this::onContractAddressFocusExit, - ), - ), - ) - debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - uiState.chooseNetworkState.selectedNetwork?.let { networkItemState -> - validateContractAddressUseCase(input, networkItemState.id).fold( - ifRight = { - uiState = stateFactory.removeTokenAddressError() - .copy( - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - warnings = uiState.warnings - .filterNot { it is AddCustomTokenWarning.PotentialScamToken }.toPersistentSet(), - ) - fetchTokenInformation(contractAddress = input, networkId = networkItemState.id) - }, - ifLeft = { error -> - uiState = stateFactory.handleAddressError(error) - }, - ) - } - } - } - - private fun fetchTokenInformation(contractAddress: String, networkId: String) { - viewModelScope.launch(dispatchers.main) { - uiState = stateFactory.updateStateOnLoadingTokenInfo(contractAddress) - withContext(dispatchers.io) { - findTokenByContractAddressUseCase( - contractAddress = contractAddress, - networkId = networkId, - ).fold( - ifLeft = { - val tokenData = ContractAddressToCustomTokenDataConverter(this@AddCustomTokenViewModel) - .convert(contractAddress) - - val isButtonEnabled = tokenData.isRequiredInformationProvided() - uiState = uiState.copy( - tokenData = tokenData, - warnings = (uiState.warnings + AddCustomTokenWarning.PotentialScamToken).toPersistentSet(), - addTokenButton = uiState.addTokenButton.copy( - isEnabled = isButtonEnabled, - ), - ) - }, - ifRight = { token -> - val tokenData = if (token != null) { - FoundTokenToCustomTokenDataConverter(this@AddCustomTokenViewModel).convert(token) - } else { - ContractAddressToCustomTokenDataConverter(this@AddCustomTokenViewModel).convert( - contractAddress, - ) - } - - val isButtonEnabled = tokenData.isRequiredInformationProvided() - uiState = uiState.copy( - tokenData = tokenData, - addTokenButton = uiState.addTokenButton.copy( - isEnabled = isButtonEnabled, - ), - ) - }, - ) - } - } - } - - override fun onTokenNameChange(input: String) { - uiState = uiState.copy( - tokenData = uiState.tokenData?.copy( - nameTextField = TextFieldState.Editable( - value = input, - isEnabled = true, - onValueChange = this::onTokenNameChange, - onFocusExit = this::onTokenNameFocusExit, - ), - ), - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - ) - } - - override fun onSymbolChange(input: String) { - uiState = uiState.copy( - tokenData = uiState.tokenData?.copy( - symbolTextField = TextFieldState.Editable( - value = input, - isEnabled = true, - onValueChange = this::onSymbolChange, - onFocusExit = this::onSymbolFocusExit, - ), - ), - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - ) - } - - override fun onDecimalsChange(input: String) { - val correctInput = input.toIntOrNull() - val error = if (input.isNotBlank() && correctInput == null) { - AddCustomTokenWarning.WrongDecimals - } else { - null - } - uiState = uiState.copy( - tokenData = uiState.tokenData?.copy( - decimalsTextField = TextFieldState.Editable( - value = input, - isEnabled = true, - onValueChange = this::onDecimalsChange, - error = error, - onFocusExit = this::onDecimalsFocusExit, - ), - ), - addTokenButton = uiState.addTokenButton.copy( - isEnabled = uiState.tokenData?.isRequiredInformationProvided() == true, - ), - ) - } - - override fun onContractAddressFocusExit() { - val error = (uiState.tokenData?.contractAddressTextField as? TextFieldState.Editable)?.error - val validated = error !is AddCustomTokenWarning.InvalidContractAddress - analyticsEventHandler.send(ManageTokens.CustomTokenAddress(validated = validated)) - } - - override fun onTokenNameFocusExit() { - analyticsEventHandler.send(ManageTokens.CustomTokenName) - } - - override fun onSymbolFocusExit() { - analyticsEventHandler.send(ManageTokens.CustomTokenSymbol) - } - - override fun onDecimalsFocusExit() { - analyticsEventHandler.send(ManageTokens.CustomTokenDecimals) - } - - override fun onDerivationSelected(derivation: Derivation) { - derivation.standardType?.let { - analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(derivation.networkName)) - } - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy(selectedDerivation = derivation), - ) - router.popBackStack() - } - - override fun onChooseDerivationClick() { - router.openCustomTokenChooseDerivation() - } - - override fun onCloseChoosingDerivationClick() { - router.popBackStack() - } - - override fun onCustomDerivationChange(input: String) { - val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return - - analyticsEventHandler.send(ManageTokens.CustomTokenDerivationSelected(ManageTokens.Derivation.CUSTOM.value)) - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = uiState.chooseDerivationState?.enterCustomDerivationState?.copy( - value = input, - ), - ), - ) - debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - val networkId = uiState.chooseNetworkState.selectedNetwork?.id ?: return@debounce - uiState = stateFactory.updateOnCustomDerivationEntered( - input = input, - requiresHardenedDerivationOnly = requiresHardenedDerivationOnly(networkId, selectedWallet.walletId), - ) - } - } - - private suspend fun requiresHardenedDerivationOnly(networkId: String, userWalletId: UserWalletId): Boolean { - return requiresHardenedDerivationOnlyUseCase.invoke( - networkId = networkId, - userWalletId = userWalletId, - ).getOrElse { false } - } - - override fun onCustomDerivationSelected() { - uiState = stateFactory.updateOnCustomDerivationSelected() - router.popBackStack() - } - - override fun onEnterCustomDerivation() { - uiState = stateFactory.updateStateOnEnterCustomDerivation() - } - - override fun onCustomDerivationDialogDismissed() { - uiState = uiState.copy( - chooseDerivationState = uiState.chooseDerivationState?.copy( - enterCustomDerivationState = null, - ), - ) - } - - override fun onAddCustomButtonClick() { - viewModelScope.launch(dispatchers.io) { - val selectedWallet = getSelectedWalletSyncUseCase().getOrNull() ?: return@launch - val cryptoCurrency = AddCustomTokenStateToCryptoCurrencyConverter( - selectedWallet.scanResponse.derivationStyleProvider, - ).convert(uiState) - val alreadyAdded = isCryptoCurrencyAlreadyAdded(selectedWallet, cryptoCurrency) - if (alreadyAdded) { - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert(AlertState.TokenAlreadyAdded), - setUiState = { uiState = it }, - ) - } else { - sendTokenAddedEvent(cryptoCurrency) - addCryptoCurrenciesUseCase(selectedWallet.walletId, currency = cryptoCurrency) - withContext(dispatchers.main) { router.popBackStack() } - } - } - } - - private suspend fun isCryptoCurrencyAlreadyAdded( - selectedWallet: UserWallet, - cryptoCurrency: CryptoCurrency, - ): Boolean { - val currenciesList = getCurrenciesUseCase.getSync(selectedWallet.walletId).getOrElse { emptyList() } - return when (cryptoCurrency) { - is CryptoCurrency.Coin -> { - currenciesList.any { - it is CryptoCurrency.Coin && - it.id == cryptoCurrency.id && - it.network.derivationPath == cryptoCurrency.network.derivationPath - } - } - is CryptoCurrency.Token -> { - currenciesList.any { - (it as? CryptoCurrency.Token)?.let { - it.id == cryptoCurrency.id && - it.contractAddress == cryptoCurrency.contractAddress && - it.network.id == cryptoCurrency.network.id && - it.network.derivationPath == cryptoCurrency.network.derivationPath - } ?: false - } - } - } - } - - private fun sendTokenAddedEvent(cryptoCurrency: CryptoCurrency) { - val selectedDerivation = uiState.chooseDerivationState?.selectedDerivation - - val derivation = when { - selectedDerivation == null -> ManageTokens.Derivation.DEFAULT.value - selectedDerivation.networkName.isNotEmpty() -> selectedDerivation.networkName - else -> ManageTokens.Derivation.CUSTOM.value - } - when (cryptoCurrency) { - is CryptoCurrency.Token -> { - analyticsEventHandler.send( - ManageTokens.CustomTokenWasAdded( - derivation = derivation, - networkId = cryptoCurrency.network.name, - contractAddress = cryptoCurrency.contractAddress, - token = cryptoCurrency.symbol, - ), - ) - } - is CryptoCurrency.Coin -> { - analyticsEventHandler.send( - ManageTokens.CustomTokenWasAdded( - derivation = derivation, - networkId = cryptoCurrency.network.name, - ), - ) - } - } - } - - override fun onBack() { - router.popBackStack() - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt deleted file mode 100644 index 3a527de7f2..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/analytics/ManageTokens.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.managetokens.presentation.common.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.core.analytics.models.AnalyticsParam - -sealed class ManageTokens( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent("Manage Tokens", event, params) { - - class ScreenOpened : ManageTokens("Manage Tokens Screen Opened") - - class TokenIsNotFound(userInput: String) : ManageTokens( - event = "Token Is Not Found", - params = mapOf("Input" to userInput), - ) - - class TokenSwitcherChanged( - token: String, - state: AnalyticsParam.OnOffState, - ) : ManageTokens( - event = "Token Switcher Changed", - params = mapOf( - "Token" to token, - "State" to state.value, - ), - ) - - class ButtonAdd(token: String) : ManageTokens( - event = "Button - Add", - params = mapOf("Token" to token), - ) - - class ButtonEdit(token: String) : ManageTokens( - event = "Button - Edit", - params = mapOf("Token" to token), - ) - - object ButtonChooseWallet : ManageTokens(event = "Button - Choose Wallet") - - class WalletSelected(source: Source) : ManageTokens( - event = "Wallet Selected", - params = mapOf("Source" to source.name), - ) { - - enum class Source(name: String) { - MainToken("Main Token"), - CustomToken("Custom Token"), - } - } - - object NoticeNonNativeNetworkClicked : ManageTokens(event = "Notice - Non Native Network Clicked") - - class ButtonGenerateAddresses(cardCount: Int) : ManageTokens( - event = "Button - Get Addresses", - params = mapOf("CardCount" to cardCount.toString()), - ) - - object ButtonCustomToken : ManageTokens("Button - Custom Token") - - class CustomTokenWasAdded( - val derivation: String, - val networkId: String, - val token: String? = null, - val contractAddress: String? = null, - ) : ManageTokens( - event = "Custom Token Was Added", - params = mutableMapOf( - "Derivation" to derivation, - "Network Id" to networkId, - ).apply { - token?.let { put("Token", it) } - contractAddress?.let { put("Contract Address", it) } - }, - ) - - class CustomTokenNetworkSelected(blockchain: String) : ManageTokens( - event = "Custom Token Network Selected", - params = mapOf("blockchain" to blockchain), - ) - - class CustomTokenDerivationSelected(derivation: String) : ManageTokens( - event = "Custom Token Derivation Selected", - params = mapOf("Derivation" to derivation), - ) - - class CustomTokenAddress(validated: Boolean) : ManageTokens( - "Custom Token Address", - params = mapOf("Validation" to if (validated) "Ok" else "Error"), - ) - - object CustomTokenName : ManageTokens("Custom Token Name") - - object CustomTokenSymbol : ManageTokens("Custom Token Symbol") - - object CustomTokenDecimals : ManageTokens("Custom Token Decimals") - - enum class Derivation(val value: String) { - DEFAULT("Default"), - CUSTOM("Custom"), - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt deleted file mode 100644 index 82ba359079..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/AlertState.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.managetokens.impl.R - -internal sealed class AlertState { - - abstract val message: TextReference - - class DefaultAlert( - override val message: TextReference, - ) : AlertState() - - class TokenUnavailable( - val onUpvoteClick: () -> Unit, - ) : AlertState() { - override val message: TextReference = resourceReference(R.string.manage_tokens_unavailable_description) - val confirmButtonText: TextReference = resourceReference(R.string.common_close) - val dismissButtonText: TextReference = resourceReference(R.string.manage_tokens_unavailable_vote) - } - - object NonNative : AlertState() { - override val message: TextReference = resourceReference(R.string.manage_tokens_network_selector_non_native_info) - } - - class TokensUnsupported(networkName: String) : AlertState() { - override val message: TextReference = resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(networkName), - ) - } - - object TokensUnsupportedCurve : AlertState() { - override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_curve_message) - } - - class TokensUnsupportedBlockchainByCard(networkName: String) : AlertState() { - override val message: TextReference = resourceReference( - id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, - formatArgs = wrappedList(networkName), - ) - } - - class CannotHideNetworkWithTokens(tokenName: String, currencySymbol: String, networkName: String) : AlertState() { - override val message: TextReference = resourceReference( - id = R.string.token_details_unable_hide_alert_message, - formatArgs = wrappedList(tokenName, currencySymbol, networkName), - ) - } - - object TokenAlreadyAdded : AlertState() { - override val message: TextReference = resourceReference(R.string.custom_token_validation_error_already_added) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt deleted file mode 100644 index 6a585d7148..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/ChooseWalletState.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.managetokens.impl.R -import kotlinx.collections.immutable.ImmutableList - -internal sealed class ChooseWalletState { - data class Choose( - val wallets: ImmutableList, - val selectedWallet: WalletState?, - val onChooseWalletClick: () -> Unit, - val onCloseChoosingWalletClick: () -> Unit, - val show: Boolean = false, - ) : ChooseWalletState() - - object NoSelection : ChooseWalletState() - - class Warning(val type: ChooseWalletWarning) : ChooseWalletState() { - val message: TextReference - get() = when (type) { - ChooseWalletWarning.SINGLE_CURRENCY -> - TextReference.Res(R.string.manage_tokens_wallet_support_only_one_network_title) - } - } -} - -enum class ChooseWalletWarning { - SINGLE_CURRENCY, -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt deleted file mode 100644 index 4024d7a4d8..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/Event.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -import androidx.compose.runtime.Immutable - -@Immutable -internal sealed interface Event { - data class ShowAlert(val state: AlertState) : Event -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt deleted file mode 100644 index 6255c750f9..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/NetworkItemState.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -import androidx.compose.runtime.MutableState -import com.tangem.core.ui.extensions.* -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState - -/** - * Network item state - * - * @property name network name - * @property protocolName network protocol name - * @property id network id - * @property iconRes network icon id from resources - */ -internal sealed interface NetworkItemState { - - val name: String - val protocolName: String - val id: String - - val iconRes: Int - get() = when (this) { - is Selectable -> this.iconResId - is Toggleable -> this.iconResId.value - } - - /** - * Network item state that can be added and deleted - * - * @property name network name - * @property protocolName network protocol name - * @property id network id - * @property iconResId network icon id from resources - * @property isMainNetwork flag that determines if the network is the main network for the token - * @property isAdded flag that determines if the user has saved the network - * @property address contract address - * @property decimals decimal count - * @property onToggleClick lambda be invoked when switch is been toggled - */ - @Suppress("LongParameterList") - data class Toggleable( - override val name: String, - override val protocolName: String, - override val id: String, - val iconResId: MutableState, - val isMainNetwork: Boolean, - val isAdded: MutableState, - val address: String?, - val decimals: Int?, - val onToggleClick: (TokenItemState.Loaded, Toggleable) -> Unit, - ) : NetworkItemState { - - /** - * Change toggle state [isAdded]. - * - * It is a hack that helps us to change element of flow - */ - fun changeToggleState() { - val reverseState = !isAdded.value - isAdded.value = reverseState - iconResId.value = if (reverseState) getActiveIconResByNetworkId(id) else getGreyedOutIconResByNetworkId(id) - } - } - - /** - * Network item state that can be selected - * - * @property name network name - * @property protocolName network protocol name - * @property iconResId network icon id from resources - * @property id network id - * @property onNetworkClick lambda be invoked when network item is been clicked - * - */ - data class Selectable( - override val name: String, - override val protocolName: String, - val iconResId: Int, - override val id: String, - val onNetworkClick: (NetworkItemState) -> Unit, - ) : NetworkItemState -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt deleted file mode 100644 index 6af48736a4..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/WalletState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.managetokens.presentation.common.state - -internal data class WalletState( - val walletId: String, - val artworkUrl: String?, - val walletName: String, - val onSelected: (String) -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt deleted file mode 100644 index f231654b96..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/state/previewdata/ChooseWalletStatePreviewData.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.managetokens.presentation.common.state.previewdata - -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.WalletState -import kotlinx.collections.immutable.persistentListOf - -internal object ChooseWalletStatePreviewData { - - val state: ChooseWalletState.Choose - get() = ChooseWalletState.Choose( - wallets = persistentListOf( - walletState, - walletState.copy(walletId = "2"), - ), - selectedWallet = walletState, - onChooseWalletClick = {}, - onCloseChoosingWalletClick = {}, - ) - - private val walletState: WalletState - get() = WalletState( - walletName = "My wallet", - walletId = "1", - artworkUrl = "", - onSelected = {}, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt deleted file mode 100644 index 9bf85280d4..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletBottomSheet.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui - -import androidx.compose.runtime.Composable -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.res.TangemTheme -import com.tangem.managetokens.presentation.common.state.ChooseWalletState - -@Composable -internal fun ChooseWalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - ) { - ChooseWalletScreen(state = it.chooseWalletState) - } -} - -internal class ChooseWalletBottomSheetConfig( - val chooseWalletState: ChooseWalletState.Choose, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt deleted file mode 100644 index 121faaba26..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/ChooseWalletScreen.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui - -import android.content.res.Configuration -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -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.tooling.preview.Preview -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerW12 -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.WalletState -import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData - -@Composable -internal fun ChooseWalletScreen(state: ChooseWalletState.Choose, modifier: Modifier = Modifier) { - LazyColumn( - modifier = modifier - .background(TangemTheme.colors.background.tertiary) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - item { - Box( - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size44), - ) { - IconButton( - onClick = state.onCloseChoosingWalletClick, - modifier = Modifier - .align(Alignment.CenterStart) - .clickable { state.onCloseChoosingWalletClick() }, - ) { - Icon( - painterResource(id = R.drawable.ic_back_24), - contentDescription = null, - tint = TangemTheme.colors.icon.primary1, - ) - } - Text( - text = stringResource(id = R.string.manage_tokens_wallet_selector_title), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - textAlign = TextAlign.Center, - maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .align(Alignment.Center), - ) - } - } - items( - count = state.wallets.count(), - key = { index -> state.wallets[index].walletId }, - ) { index -> - WalletItem( - wallet = state.wallets[index], - selectedWallet = state.selectedWallet, - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = state.wallets.lastIndex, - addDefaultPadding = false, - ), - ) - } - item { - SpacerH(height = TangemTheme.dimens.spacing16) - } - } -} - -@Composable -private fun WalletItem(wallet: WalletState, selectedWallet: WalletState?, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clickable { wallet.onSelected(wallet.walletId) } - .background(TangemTheme.colors.background.action) - .defaultMinSize(minHeight = TangemTheme.dimens.size72) - .padding(horizontal = TangemTheme.dimens.spacing16), - verticalAlignment = Alignment.CenterVertically, - ) { - SubcomposeAsyncImage( - modifier = Modifier.size(height = TangemTheme.dimens.size30, width = TangemTheme.dimens.size50), - - model = ImageRequest.Builder(context = LocalContext.current) - .data(wallet.artworkUrl) - .crossfade(enable = true) - .build(), - loading = { - Image( - painter = painterResource(R.drawable.card_placeholder_black), - contentDescription = null, - ) - }, - error = { - Image( - painter = painterResource(R.drawable.card_placeholder_black), - contentDescription = null, - ) - }, - contentDescription = null, - ) - SpacerW12() - Text( - text = wallet.walletName, - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - SpacerWMax() - if (selectedWallet == wallet) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseWalletScreen() { - TangemThemePreview { - ChooseWalletScreen( - state = ChooseWalletStatePreviewData.state, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt deleted file mode 100644 index 0bfc404f35..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/EventEffect.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui - -import androidx.compose.runtime.Composable -import com.tangem.core.ui.event.StateEvent -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.Event - -@Composable -internal fun EventEffect(event: StateEvent, onAlertStateSet: (AlertState) -> Unit) { - com.tangem.core.ui.event.EventEffect( - event = event, - onTrigger = { value -> - when (value) { - is Event.ShowAlert -> onAlertStateSet(value.state) - } - }, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt deleted file mode 100644 index 14e49a02b6..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/Alert.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui.components - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButton -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.AlertState - -@Composable -internal fun Alert(state: AlertState, onDismiss: () -> Unit) { - when (state) { - is AlertState.DefaultAlert, - is AlertState.NonNative, - AlertState.TokensUnsupportedCurve, - is AlertState.TokensUnsupported, - is AlertState.TokensUnsupportedBlockchainByCard, - is AlertState.CannotHideNetworkWithTokens, - is AlertState.TokenAlreadyAdded, - -> DefaultAlert(state, onDismiss) - is AlertState.TokenUnavailable -> TokenUnavailableAlert(state, onDismiss) - } -} - -@Composable -private fun DefaultAlert(state: AlertState, onDismiss: () -> Unit) { - BasicDialog( - message = state.message.resolveReference(), - confirmButton = DialogButton( - title = stringResource(id = R.string.common_ok), - onClick = onDismiss, - ), - onDismissDialog = onDismiss, - ) -} - -@Composable -private fun TokenUnavailableAlert(state: AlertState.TokenUnavailable, onDismiss: () -> Unit) { - BasicDialog( - message = state.message.resolveReference(), - confirmButton = DialogButton( - title = state.confirmButtonText.resolveReference(), - onClick = onDismiss, - ), - dismissButton = DialogButton( - title = state.dismissButtonText.resolveReference(), - onClick = { state.onUpvoteClick() }, - ), - onDismissDialog = onDismiss, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt deleted file mode 100644 index 553a6bd999..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/NetworkItem.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -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.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.components.TangemSwitch -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData - -@Composable -internal fun NetworkItem( - state: NetworkItemState, - tokenState: TokenItemState.Loaded?, - modifier: Modifier = Modifier, - isSelected: Boolean = false, -) { - Row( - modifier = modifier - .background(TangemTheme.colors.background.action) - .defaultMinSize(minHeight = TangemTheme.dimens.size68) - .then( - if (state is NetworkItemState.Selectable) { - Modifier.clickable { state.onNetworkClick(state) } - } else { - Modifier - }, - ) - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - NetworkIcon(model = state) - SpacerW(width = TangemTheme.dimens.spacing12) - Text( - text = state.name, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - ) - SpacerW(width = TangemTheme.dimens.spacing6) - Text( - text = state.protocolName, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - modifier = Modifier - .weight(1f), - ) - if (state is NetworkItemState.Toggleable) { - TangemSwitch( - onCheckedChange = { - state.onToggleClick(tokenState!!, state) - }, - checked = state.isAdded.value, - ) - } else if (state is NetworkItemState.Selectable && isSelected) { - Icon( - painter = painterResource(id = R.drawable.ic_check_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - } -} - -@Composable -internal fun NetworkIcon(model: NetworkItemState, modifier: Modifier = Modifier) { - Box(modifier = modifier.size(size = TangemTheme.dimens.size36)) { - val isAdded = when (model) { - is NetworkItemState.Selectable -> true - is NetworkItemState.Toggleable -> model.isAdded.value - } - - if (!isAdded) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size36) - .clip(CircleShape) - .background(TangemTheme.colors.control.unchecked), - ) - } - Icon( - painter = painterResource(id = model.iconRes), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size36), - tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary, - ) - - if (model is NetworkItemState.Toggleable && model.isMainNetwork) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size10) - .clip(CircleShape) - .background(TangemTheme.colors.stroke.transparency), - contentAlignment = Alignment.Center, - ) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size8) - .clip(CircleShape) - .background(TangemTheme.colors.icon.accent), - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_NetworkItem(@PreviewParameter(NetworkItemStateProvider::class) state: NetworkItemState) { - TangemThemePreview { - NetworkItem(state, tokenState = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded) - } -} - -private class NetworkItemStateProvider : CollectionPreviewParameterProvider( - collection = listOf( - NetworkItemState.Toggleable( - name = "Ethereum", - protocolName = "ETH", - iconResId = mutableStateOf(R.drawable.img_polygon_22), - isMainNetwork = true, - isAdded = mutableStateOf(true), - id = "", - address = "", - onToggleClick = { _, _ -> }, - decimals = 0, - ), - NetworkItemState.Toggleable( - name = "BNB SMART CHAIN", - protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_bsc_16), - isMainNetwork = false, - isAdded = mutableStateOf(false), - id = "", - address = "", - onToggleClick = { _, _ -> }, - decimals = 0, - ), - NetworkItemState.Selectable( - name = "Ethereum", - protocolName = "ETH", - iconResId = R.drawable.img_polygon_22, - id = "", - onNetworkClick = { }, - ), - ), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt deleted file mode 100644 index e7ad5cf31b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/ui/components/SimpleSelectionBlock.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.tangem.managetokens.presentation.common.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme - -@Composable -fun SimpleSelectionBlock( - title: String, - subtitle: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - roundedCorners: Boolean = true, -) { - Column( - modifier = modifier - .then( - if (roundedCorners) { - Modifier.clip(shape = RoundedCornerShape(TangemTheme.dimens.radius16)) - } else { - Modifier - }, - ) - .background(color = TangemTheme.colors.background.action) - .clickable { onClick() } - .padding( - horizontal = TangemTheme.dimens.spacing20, - vertical = TangemTheme.dimens.spacing16, - ) - .fillMaxWidth(), - ) { - Text( - text = title, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - ) - SpacerH(height = TangemTheme.dimens.spacing4) - Text( - text = subtitle, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_SimpleSelectionBlock() { - TangemThemePreview { - SimpleSelectionBlock(title = "Wallet", subtitle = "Family Wallet", onClick = { }) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/utils/CurrencyUtils.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/utils/CurrencyUtils.kt deleted file mode 100644 index b005f36dee..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/common/utils/CurrencyUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.managetokens.presentation.common.utils - -import com.tangem.domain.tokens.model.CryptoCurrency - -internal object CurrencyUtils { - fun isAdded(address: String?, networkId: String, currencies: Collection): Boolean { - return if (address != null) { - currencies.any { - !it.isCustom && it is CryptoCurrency.Token && it.contractAddress == address && - it.network.backendId == networkId - } - } else { - currencies.any { - !it.isCustom && it is CryptoCurrency.Coin && it.network.backendId == networkId - } - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt deleted file mode 100644 index df2a7f015f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ChooseNetworkState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import kotlinx.collections.immutable.ImmutableList - -internal data class ChooseNetworkState( - val nativeNetworks: ImmutableList, - val nonNativeNetworks: ImmutableList, - val onNonNativeNetworkHintClick: () -> Unit, - val onCloseChooseNetworkScreen: () -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt deleted file mode 100644 index 0cfdfa16d1..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/DerivationNotificationState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.managetokens.impl.R - -data class DerivationNotificationState( - val totalNeeded: Int, - val totalWallets: Int, - val walletsToDerive: Int, - val onGenerateClick: () -> Unit, -) { - val config = NotificationConfig( - title = resourceReference(id = R.string.warning_missing_derivation_title), - subtitle = pluralReference( - id = R.plurals.warning_missing_derivation_message, - count = totalNeeded, - formatArgs = wrappedList(totalNeeded), - ), - iconResId = R.drawable.ic_alert_circle_24, - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(id = R.string.common_generate_addresses), - iconResId = R.drawable.ic_tangem_24, - onClick = onGenerateClick, - additionalText = pluralReference( - id = R.plurals.manage_tokens_number_of_wallets_android, - count = totalWallets, - formatArgs = wrappedList(walletsToDerive, totalWallets), - ), - ), - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt deleted file mode 100644 index 6738d56d3f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/ManageTokensState.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import androidx.paging.PagingData -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.event.StateEvent -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.Event -import kotlinx.coroutines.flow.Flow - -internal data class ManageTokensState( - val searchBarState: SearchBarState, - val tokens: Flow>, - val isLoading: Boolean, - val addCustomTokenButton: AddCustomTokenButton, - val chooseWalletState: ChooseWalletState, - val derivationNotification: DerivationNotificationState? = null, - val selectedToken: TokenItemState.Loaded? = null, - val showChooseWalletScreen: Boolean = false, - val customTokenBottomSheetConfig: TangemBottomSheetConfig, - val event: StateEvent, - val onEmptySearchResult: (String) -> Unit, -) - -data class AddCustomTokenButton( - val isVisible: Boolean, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt deleted file mode 100644 index 3518626707..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/QuotesState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import com.tangem.core.ui.components.marketprice.PriceChangeType -import kotlinx.collections.immutable.ImmutableList - -internal sealed class QuotesState { - object Unknown : QuotesState() - - data class Content( - val priceChange: String, - val changeType: PriceChangeType, - val chartData: ImmutableList, - ) : QuotesState() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt deleted file mode 100644 index d64808239e..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/SearchBarState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -/** - * SearchBar state. - */ -internal data class SearchBarState( - val query: String, - val onQueryChange: (String) -> Unit, - val active: Boolean, - val onActiveChange: (Boolean) -> Unit, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt deleted file mode 100644 index 6c47a870ec..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenButtonType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -internal enum class TokenButtonType { - ADD, EDIT, NOT_AVAILABLE -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt deleted file mode 100644 index 0bd0c0d25d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenIconState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.extensions.ImageReference - -internal data class TokenIconState( - val iconReference: ImageReference?, - val placeholderTint: Color, - val placeholderBackground: Color, -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt deleted file mode 100644 index 7262bbe337..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/TokenItemState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state - -import androidx.compose.runtime.MutableState - -internal sealed class TokenItemState { - - abstract val id: String - - data class Loading(override val id: String) : TokenItemState() - - data class Loaded( - override val id: String, - val name: String, - val currencySymbol: String, - val tokenId: String, - val tokenIcon: TokenIconState, - val quotes: QuotesState, - val rate: String?, - val availableAction: MutableState, - val chooseNetworkState: ChooseNetworkState, - val onButtonClick: (Loaded) -> Unit, - ) : TokenItemState() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt deleted file mode 100644 index d7531b2e85..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/ManageTokensStateFactory.kt +++ /dev/null @@ -1,194 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import androidx.paging.PagingData -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.domain.tokens.CurrencyCompatibilityError -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.managetokens.presentation.common.state.* -import com.tangem.managetokens.presentation.common.utils.CurrencyUtils -import com.tangem.managetokens.presentation.managetokens.state.* -import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensClickIntents -import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensUiEvents -import com.tangem.utils.Provider -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.Flow - -internal class ManageTokensStateFactory( - private val currentStateProvider: Provider, - private val clickIntents: ManageTokensClickIntents, - private val uiIntents: ManageTokensUiEvents, -) { - - fun getInitialState(tokens: Flow>): ManageTokensState { - return ManageTokensState( - searchBarState = SearchBarState( - query = "", - onQueryChange = clickIntents::onSearchQueryChange, - active = false, - onActiveChange = clickIntents::onSearchActiveChange, - ), - tokens = tokens, - addCustomTokenButton = AddCustomTokenButton( - isVisible = false, - onClick = clickIntents::onAddCustomTokensButtonClick, - ), - derivationNotification = null, - isLoading = false, - event = consumedEvent(), - chooseWalletState = ChooseWalletState.NoSelection, - onEmptySearchResult = uiIntents::onEmptySearchResult, - customTokenBottomSheetConfig = TangemBottomSheetConfig( - isShow = false, - onDismissRequest = uiIntents::onAddCustomTokenSheetDismissed, - content = TangemBottomSheetConfigContent.Empty, - ), - ) - } - - fun updateChooseWalletState( - wallets: List, - userWallets: List, - selectedWallet: UserWallet?, - ): ManageTokensState { - val chooseWalletState = when { - wallets.size == 1 -> { - ChooseWalletState.NoSelection - } - wallets.isEmpty() && userWallets.all { !it.isMultiCurrency } -> { - ChooseWalletState.Warning(ChooseWalletWarning.SINGLE_CURRENCY) - } - else -> { - var selectedWalletState: WalletState? = null - ChooseWalletState.Choose( - wallets = wallets.map { wallet -> - val walletState = WalletState( - walletId = wallet.walletId.stringValue, - artworkUrl = wallet.artworkUrl, - onSelected = clickIntents::onWalletSelected, - walletName = wallet.name, - ) - if (wallet.walletId.stringValue == selectedWallet?.walletId?.stringValue) { - selectedWalletState = walletState - } - walletState - }.toPersistentList(), - selectedWallet = selectedWalletState, - onChooseWalletClick = clickIntents::onChooseWalletClick, - onCloseChoosingWalletClick = clickIntents::onCloseChoosingWalletClick, - ) - } - } - return currentStateProvider().copy(chooseWalletState = chooseWalletState) - } - - fun showAddCustomTokensButton(show: Boolean): ManageTokensState { - return currentStateProvider().copy( - addCustomTokenButton = currentStateProvider().addCustomTokenButton.copy(isVisible = show), - ) - } - - fun updateSelectedWallet(selectedWalletId: String?): ManageTokensState { - val chooseWalletState = currentStateProvider().chooseWalletState - return currentStateProvider().copy( - showChooseWalletScreen = false, - chooseWalletState = if (chooseWalletState is ChooseWalletState.Choose) { - chooseWalletState.copy( - selectedWallet = chooseWalletState.wallets.find { - it.walletId == selectedWalletId - } ?: chooseWalletState.wallets.first(), - ) - } else { - chooseWalletState - }, - ) - } - - fun getStateAndTriggerEvent( - state: ManageTokensState, - event: Event, - setUiState: (ManageTokensState) -> Unit, - ): ManageTokensState { - return state.copy( - event = triggeredEvent( - data = event, - onConsume = { - val currentState = currentStateProvider() - setUiState(currentState.copy(event = consumedEvent())) - }, - ), - ) - } - - fun transformAddTokenErrorToAlert(error: CurrencyCompatibilityError, networkName: String): AlertState { - return when (error) { - CurrencyCompatibilityError.SolanaTokensUnsupported -> AlertState.TokensUnsupported(networkName) - CurrencyCompatibilityError.UnsupportedBlockchain -> AlertState.TokensUnsupportedBlockchainByCard( - networkName, - ) - CurrencyCompatibilityError.UnsupportedCurve -> AlertState.TokensUnsupportedCurve - } - } - - fun toggleNetworkState( - token: TokenItemState.Loaded, - network: NetworkItemState.Toggleable, - allAddedCurrencies: Collection, - ) { - network.changeToggleState() - val anyNetworkAdded = isAnyNetworkAdded( - networks = token.chooseNetworkState.nativeNetworks + token.chooseNetworkState.nonNativeNetworks, - allAddedCurrencies = allAddedCurrencies, - ) - val buttonType = if (anyNetworkAdded) TokenButtonType.EDIT else TokenButtonType.ADD - token.availableAction.value = buttonType - } - - private fun isAnyNetworkAdded( - networks: List, - allAddedCurrencies: Collection, - ): Boolean { - return networks.any { - it is NetworkItemState.Toggleable && CurrencyUtils.isAdded( - address = it.address, - networkId = it.id, - currencies = allAddedCurrencies, - ) - } - } - - fun updateTokenNetworksOnTokenSelection( - token: TokenItemState.Loaded, - addedCurrenciesOnWallet: Collection, - ) { - token.chooseNetworkState.nativeNetworks.forEach { - if (it is NetworkItemState.Toggleable) { - val isAdded = CurrencyUtils.isAdded(it.address, it.id, addedCurrenciesOnWallet) - if (isAdded != it.isAdded.value) (it as? NetworkItemState.Toggleable)?.changeToggleState() - } - } - token.chooseNetworkState.nonNativeNetworks.forEach { - if (it is NetworkItemState.Toggleable) { - val isAdded = CurrencyUtils.isAdded(it.address, it.id, addedCurrenciesOnWallet) - if (isAdded != it.isAdded.value) (it as? NetworkItemState.Toggleable)?.changeToggleState() - } - } - } - - fun updateDerivationNotification(totalNeeded: Int, totalWallets: Int, walletsToDerive: Int): ManageTokensState { - val derivationNotificationState = if (totalNeeded == 0) { - null - } else { - DerivationNotificationState( - totalNeeded = totalNeeded, - totalWallets = totalWallets, - walletsToDerive = walletsToDerive, - onGenerateClick = clickIntents::onGetAddressesClick, - ) - } - return currentStateProvider().copy(derivationNotification = derivationNotificationState) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt deleted file mode 100644 index 01af86d1f2..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworkToNetworkItemStateConverter.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import com.tangem.core.ui.extensions.getActiveIconResByNetworkId -import com.tangem.core.ui.extensions.getGreyedOutIconResByNetworkId -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Token -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.common.utils.CurrencyUtils -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -internal class NetworkToNetworkItemStateConverter( - private val addedCurrenciesByWalletProvider: Provider>>, - private val selectedWalletProvider: Provider, - private val onNetworkToggleClick: (token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) -> Unit, -) : Converter { - - override fun convert(value: Token.Network): NetworkItemState { - return createManageNetworkContent(value) - } - - private fun createManageNetworkContent(network: Token.Network): NetworkItemState { - val addedCurrencies = addedCurrenciesByWalletProvider()[selectedWalletProvider()] ?: emptyList() - val isAdded = CurrencyUtils.isAdded( - address = network.address, - networkId = network.networkId, - currencies = addedCurrencies, - ) - return NetworkItemState.Toggleable( - name = network.name, - iconResId = mutableIntStateOf( - getNetworkIconResId(isAdded, network.networkId), // todo - ), - isMainNetwork = isMainNetwork(network), - isAdded = mutableStateOf(isAdded), - id = network.networkId, - protocolName = network.standardType, - address = network.address, - decimals = network.decimalCount, - onToggleClick = onNetworkToggleClick, - ) - } - - private fun getNetworkIconResId(isAdded: Boolean, networkId: String): Int { - return if (isAdded) { - getActiveIconResByNetworkId(networkId) - } else { - getGreyedOutIconResByNetworkId(networkId) - } - } - - private fun isMainNetwork(network: Token.Network) = network.address == null -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt deleted file mode 100644 index 99509e85ea..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/NetworksToChooseNetworkStateConverter.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import com.tangem.domain.tokens.model.Token -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toPersistentList - -internal class NetworksToChooseNetworkStateConverter( - private val networkToNetworkItemStateConverter: NetworkToNetworkItemStateConverter, - private val onNonNativeNetworkHintClick: () -> Unit, - private val onCloseChooseNetworkScreen: () -> Unit, -) : Converter, ChooseNetworkState> { - - override fun convert(value: List): ChooseNetworkState { - return createChooseNetworksState(value) - } - - private fun createChooseNetworksState(networks: List): ChooseNetworkState { - val nativeNetworks = mutableListOf() - val nonNativeNetworks = mutableListOf() - networks.map { networkToNetworkItemStateConverter.convert(it) }.forEach { - if (it is NetworkItemState.Toggleable && it.isMainNetwork) { - nativeNetworks.add(it) - } else { - nonNativeNetworks.add(it) - } - } - return ChooseNetworkState( - nativeNetworks = nativeNetworks.toPersistentList(), - nonNativeNetworks = nonNativeNetworks.toPersistentList(), - onNonNativeNetworkHintClick = onNonNativeNetworkHintClick, - onCloseChooseNetworkScreen = onCloseChooseNetworkScreen, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt deleted file mode 100644 index 972c004779..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/QuotesToQuotesStateConverter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.tokens.model.Quote -import com.tangem.managetokens.presentation.managetokens.state.QuotesState -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal - -@Suppress("MagicNumber") // TODO: remove when chart data is added (in [REDACTED_TASK_KEY] when endpoint is ready) -internal class QuotesToQuotesStateConverter : Converter { - override fun convert(value: Quote): QuotesState { - val priceChange = value.priceChange - return QuotesState.Content( - priceChange = BigDecimalFormatter.formatPercent( - percent = priceChange.movePointLeft(2), - useAbsoluteValue = true, - ), - changeType = priceChange.getPriceChangeType(), - chartData = // TODO (in [REDACTED_TASK_KEY] when endpoint is ready) - when (priceChange.getPriceChangeType()) { - PriceChangeType.UP -> persistentListOf(0f, 5f, 10f, 30f) - PriceChangeType.DOWN -> persistentListOf(15f, 12f, 13f, 18f, 10f, 3f) - PriceChangeType.NEUTRAL -> persistentListOf(0f, 0f, 0f, 0f) - }, - ) - } - - private fun BigDecimal.getPriceChangeType(): PriceChangeType { - return PriceChangeConverter.fromBigDecimal(value = this) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt deleted file mode 100644 index 156c65d70d..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenConverter.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import androidx.compose.runtime.mutableStateOf -import com.tangem.core.ui.extensions.ImageReference -import com.tangem.core.ui.extensions.getTintForTokenIcon -import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon -import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Token -import com.tangem.managetokens.presentation.common.utils.CurrencyUtils -import com.tangem.managetokens.presentation.managetokens.state.QuotesState -import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType -import com.tangem.managetokens.presentation.managetokens.state.TokenIconState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -internal class TokenConverter( - private val quotesStateConverter: QuotesToQuotesStateConverter, - private val networksToChooseNetworkStateConverter: NetworksToChooseNetworkStateConverter, - private val allAddedCurrencies: Provider>, - private val selectedAppCurrency: Provider, - private val onTokenItemButtonClick: (TokenItemState.Loaded) -> Unit, -) : Converter { - - override fun convert(value: Token): TokenItemState.Loaded { - val isAnyAdded = value.networks.any { network -> - CurrencyUtils.isAdded( - address = network.address, - networkId = network.networkId, - currencies = allAddedCurrencies(), - ) - } - val buttonType = when { - value.isAvailable && isAnyAdded -> TokenButtonType.EDIT - value.isAvailable && !isAnyAdded -> TokenButtonType.ADD - else -> TokenButtonType.NOT_AVAILABLE - } - - val background = tryGetBackgroundForTokenIcon(value.networks.firstOrNull()?.address ?: "") - val tint = getTintForTokenIcon(background) - - return TokenItemState.Loaded( - id = value.id, - name = value.name, - currencySymbol = value.symbol, - tokenId = value.id, - tokenIcon = TokenIconState( - ImageReference.Url(value.iconUrl), - placeholderBackground = background, - placeholderTint = tint, - ), - quotes = value.quote?.let { quotesStateConverter.convert(it) } ?: QuotesState.Unknown, - rate = value.quote?.fiatRate?.let { rate -> - BigDecimalFormatter.formatFiatAmount( - fiatAmount = rate, - fiatCurrencyCode = selectedAppCurrency().code, - fiatCurrencySymbol = selectedAppCurrency().symbol, - ) - }, - availableAction = mutableStateOf(buttonType), - chooseNetworkState = networksToChooseNetworkStateConverter.convert(value.networks), - onButtonClick = onTokenItemButtonClick, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt deleted file mode 100644 index a7a6832cc6..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/factory/TokenToCryptoCurrencyConverter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.factory - -import com.tangem.data.tokens.utils.CryptoCurrencyFactory -import com.tangem.domain.common.DerivationStyleProvider -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.utils.converter.Converter - -internal class TokenToCryptoCurrencyConverter( - private val network: NetworkItemState, - private val derivationStyleProvider: DerivationStyleProvider, -) : Converter { - - override fun convert(value: TokenItemState.Loaded): CryptoCurrency? { - return if (network is NetworkItemState.Toggleable && network.address != null) { - CryptoCurrencyFactory().createToken( - CryptoCurrencyFactory.Token( - symbol = value.currencySymbol, - name = value.name, - id = value.tokenId, - contractAddress = network.address, - decimals = requireNotNull(network.decimals), - ), - networkId = network.id, - derivationStyleProvider = derivationStyleProvider, - extraDerivationPath = null, - ) - } else { - CryptoCurrencyFactory().createCoin( - networkId = network.id, - derivationStyleProvider = derivationStyleProvider, - extraDerivationPath = null, - ) - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt deleted file mode 100644 index f66b65bc1b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ChooseNetworkStatePreviewData.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.previewdata - -import androidx.compose.runtime.mutableStateOf -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState -import kotlinx.collections.immutable.toImmutableList - -internal object ChooseNetworkStatePreviewData { - - val state = ChooseNetworkState( - nativeNetworks = nativeNetworks.toImmutableList(), - nonNativeNetworks = nonNativeNetworks.toImmutableList(), - onNonNativeNetworkHintClick = {}, - onCloseChooseNetworkScreen = {}, - ) -} - -internal val nativeNetworks = listOf( - NetworkItemState.Toggleable( - name = "Ethereum", - protocolName = "ETH", - iconResId = mutableStateOf(R.drawable.img_polygon_22), - isMainNetwork = true, - isAdded = mutableStateOf(true), - id = "", - onToggleClick = { _, _ -> }, - address = "", - decimals = 0, - ), -) - -internal val nonNativeNetworks = listOf( - NetworkItemState.Toggleable( - name = "Ethereum", - protocolName = "ETH", - iconResId = mutableStateOf(R.drawable.img_kusama_22), - isMainNetwork = false, - isAdded = mutableStateOf(true), - id = "1", - onToggleClick = { _, _ -> }, - address = "", - decimals = 0, - ), - NetworkItemState.Toggleable( - name = "BNB SMART CHAIN", - protocolName = "BEP20", - iconResId = mutableStateOf(R.drawable.ic_bsc_16), - isMainNetwork = false, - isAdded = mutableStateOf(false), - id = "2", - onToggleClick = { _, _ -> }, - address = "", - decimals = 0, - ), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt deleted file mode 100644 index b5d88b814a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/DerivationNotificationStatePreviewData.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.previewdata - -import com.tangem.managetokens.presentation.managetokens.state.DerivationNotificationState - -object DerivationNotificationStatePreviewData { - val state = DerivationNotificationState( - totalNeeded = 5, - totalWallets = 3, - walletsToDerive = 2, - onGenerateClick = {}, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt deleted file mode 100644 index eab841b41f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/ManageTokensStatePreviewData.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.previewdata - -import androidx.paging.PagingData -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.event.consumedEvent -import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData -import com.tangem.managetokens.presentation.managetokens.state.* -import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState -import com.tangem.managetokens.presentation.managetokens.state.SearchBarState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import kotlinx.coroutines.flow.flowOf - -internal object ManageTokensStatePreviewData { - val loadedState: ManageTokensState - get() = ManageTokensState( - searchBarState = searchState, - tokens = flowOf(PagingData.from(tokens)), - isLoading = false, - addCustomTokenButton = AddCustomTokenButton(true, {}), - derivationNotification = DerivationNotificationStatePreviewData.state, - event = consumedEvent(), - chooseWalletState = ChooseWalletStatePreviewData.state, - onEmptySearchResult = {}, - customTokenBottomSheetConfig = TangemBottomSheetConfig(false, {}, TangemBottomSheetConfigContent.Empty), - ) - - val loadingState: ManageTokensState - get() = loadedState.copy(isLoading = true) - - private val tokens: List - get() = listOf( - TokenItemStatePreviewData.loadedPriceDown, - TokenItemStatePreviewData.loadedPriceUp, - TokenItemStatePreviewData.loadedPriceNeutral, - ) - - private val searchState: SearchBarState - get() = SearchBarState( - query = "", - onQueryChange = {}, - active = false, - onActiveChange = {}, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt deleted file mode 100644 index 1ba8f760e6..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/state/previewdata/TokenItemStatePreviewData.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.state.previewdata - -import androidx.compose.runtime.mutableStateOf -import androidx.compose.ui.graphics.Color -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.managetokens.presentation.managetokens.state.QuotesState -import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType -import com.tangem.managetokens.presentation.managetokens.state.TokenIconState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import kotlinx.collections.immutable.persistentListOf - -internal object TokenItemStatePreviewData { - - val tokenLoading: TokenItemState - get() = TokenItemState.Loading("id") - - val loadedPriceDown: TokenItemState - get() = TokenItemState.Loaded( - id = "BTC", - name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", - tokenId = "BTC", - currencySymbol = "BTC", - tokenIcon = tokenIconState, - quotes = QuotesState.Content( - priceChange = "0.43%", - changeType = PriceChangeType.DOWN, - chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 4f), - ), - rate = "31 285.72$", - availableAction = mutableStateOf(TokenButtonType.ADD), - onButtonClick = {}, - chooseNetworkState = ChooseNetworkStatePreviewData.state, - ) - - val loadedPriceUp: TokenItemState - get() = TokenItemState.Loaded( - id = "BTC", - name = "Bitcoin", - tokenId = "BTC", - currencySymbol = "BTC", - tokenIcon = tokenIconState, - quotes = QuotesState.Content( - priceChange = "0.43%", - changeType = PriceChangeType.UP, - chartData = persistentListOf(1f, 3f, 4f, 8f, 12f, 10f, 8f, 3f, 5f, 7f), - ), - rate = "31 285.72$", - availableAction = mutableStateOf(TokenButtonType.NOT_AVAILABLE), - onButtonClick = {}, - chooseNetworkState = ChooseNetworkStatePreviewData.state, - ) - - val loadedPriceNeutral: TokenItemState - get() = TokenItemState.Loaded( - id = "BTC", - name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", - tokenId = "BTC", - currencySymbol = "BTC", - tokenIcon = tokenIconState, - quotes = QuotesState.Content( - priceChange = "0.00%", - changeType = PriceChangeType.NEUTRAL, - chartData = persistentListOf(10f, 2f, 5f, 3f, 4f, 8f, 9f, 7f, 10f), - ), - rate = "31 285.72$", - availableAction = mutableStateOf(TokenButtonType.ADD), - onButtonClick = {}, - chooseNetworkState = ChooseNetworkStatePreviewData.state, - ) - - private val tokenIconState: TokenIconState - get() = TokenIconState( - iconReference = null, - placeholderTint = Color.White, - placeholderBackground = Color.Black, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt deleted file mode 100644 index 5cbdc20a93..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkBottomSheet.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui - -import androidx.compose.runtime.Composable -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.res.TangemTheme -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState - -@Composable -internal fun ChooseNetworkBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - ) { - ChooseNetworkScreen(state = it.selectedToken, walletState = it.chooseWalletState) - } -} - -internal class ChooseNetworkBottomSheetConfig( - val selectedToken: TokenItemState.Loaded, - val chooseWalletState: ChooseWalletState, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt deleted file mode 100644 index 2f5d51df8a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ChooseNetworkScreen.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.components.WarningCardTitleOnly -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.state.previewdata.ChooseWalletStatePreviewData -import com.tangem.managetokens.presentation.common.ui.components.NetworkItem -import com.tangem.managetokens.presentation.common.ui.components.SimpleSelectionBlock -import com.tangem.managetokens.presentation.managetokens.state.ChooseNetworkState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData - -@Composable -internal fun ChooseNetworkScreen( - state: TokenItemState.Loaded, - walletState: ChooseWalletState, - modifier: Modifier = Modifier, -) { - val networkState = state.chooseNetworkState - LazyColumn( - contentPadding = PaddingValues( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - modifier = modifier - .background(TangemTheme.colors.background.tertiary), - ) { - item { - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_title), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth(), - ) - } - - when (walletState) { - is ChooseWalletState.Choose -> { - item { - SpacerH(height = TangemTheme.dimens.spacing10) - } - item { - SimpleSelectionBlock( - title = stringResource(id = R.string.manage_tokens_network_selector_wallet), - subtitle = walletState.selectedWallet?.walletName ?: "", - onClick = walletState.onChooseWalletClick, - ) - } - } - ChooseWalletState.NoSelection -> Unit - is ChooseWalletState.Warning -> { - item { - SpacerH(height = TangemTheme.dimens.spacing10) - } - item { - WarningCardTitleOnly( - title = stringResource(id = R.string.manage_tokens_wallet_support_only_one_network_title), - ) - } - } - } - - item { - SpacerH(height = TangemTheme.dimens.spacing16) - } - - if (networkState.nativeNetworks.isNotEmpty()) { - this@LazyColumn.nativeNetworks(networkState = networkState, tokenState = state) - } - - if (networkState.nonNativeNetworks.isNotEmpty()) { - this@LazyColumn.nonNativeNetworks(networkState = networkState, tokenState = state) - } - } -} - -private fun LazyListScope.nativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { - item { - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_native_title), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption1, - ) - SpacerH(height = TangemTheme.dimens.spacing2) - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_native_subtitle), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - ) - SpacerH(height = TangemTheme.dimens.spacing8) - } - - items( - count = networkState.nativeNetworks.count(), - key = { index -> networkState.nativeNetworks[index].id }, - ) { index -> - NetworkItem( - state = networkState.nativeNetworks[index], - tokenState = tokenState, - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = networkState.nativeNetworks.lastIndex, - addDefaultPadding = false, - ), - ) - } - - item { - SpacerH(height = TangemTheme.dimens.spacing16) - } -} - -private fun LazyListScope.nonNativeNetworks(networkState: ChooseNetworkState, tokenState: TokenItemState.Loaded) { - item { - NonNativeNetworksHeader(networkState.onNonNativeNetworkHintClick) - SpacerH(height = TangemTheme.dimens.spacing8) - } - - items( - count = networkState.nonNativeNetworks.count(), - key = { index -> networkState.nonNativeNetworks[index].id }, - ) { index -> - NetworkItem( - state = networkState.nonNativeNetworks[index], - tokenState = tokenState, - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = networkState.nonNativeNetworks.lastIndex, - addDefaultPadding = false, - ), - ) - } - - item { - SpacerH(height = TangemTheme.dimens.spacing16) - } -} - -@Composable -private fun NonNativeNetworksHeader(onNonNativeNetworkHintClick: () -> Unit) { - Column { - Row { - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_non_native_title), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption1, - ) - SpacerW(width = TangemTheme.dimens.spacing2) - Icon( - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.inactive, - contentDescription = null, - modifier = Modifier - .size(TangemTheme.dimens.size16) - .clickable { onNonNativeNetworkHintClick() }, - ) - } - SpacerH(height = TangemTheme.dimens.spacing2) - Text( - text = stringResource(id = R.string.manage_tokens_network_selector_non_native_subtitle), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ChooseNetworkScreen() { - TangemThemePreview { - ChooseNetworkScreen( - state = TokenItemStatePreviewData.loadedPriceDown as TokenItemState.Loaded, - walletState = ChooseWalletStatePreviewData.state, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt deleted file mode 100644 index 15197a8e4a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/ManageTokensScreen.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui - -import android.content.res.Configuration -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.Surface -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.paging.LoadState -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.SpacerH18 -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.keyboardAsState -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.addcustomtoken.ui.AddCustomTokenBottomSheet -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.ChooseWalletState -import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheet -import com.tangem.managetokens.presentation.common.ui.ChooseWalletBottomSheetConfig -import com.tangem.managetokens.presentation.common.ui.EventEffect -import com.tangem.managetokens.presentation.common.ui.components.Alert -import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.previewdata.ManageTokensStatePreviewData -import com.tangem.managetokens.presentation.managetokens.ui.components.DerivationNotification -import com.tangem.managetokens.presentation.managetokens.ui.components.TokensList -import com.tangem.managetokens.presentation.managetokens.ui.components.TokensSearchBar - -@Composable -internal fun ManageTokensScreen(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) { - var alertState by remember { mutableStateOf(value = null) } - - EventEffect( - event = state.event, - onAlertStateSet = { alertState = it }, - ) - alertState?.let { - Alert(state = it, onDismiss = { alertState = null }) - } - - Content(state = state, onHeaderSizeChange = onHeaderSizeChange) - - AddCustomTokenBottomSheet(state.customTokenBottomSheetConfig) -} - -@Composable -private fun Content(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit) { - val keyboard by keyboardAsState() - val density = LocalDensity.current - var tokenListAlertBottomPadding by remember(keyboard is Keyboard.Opened) { mutableStateOf(0.dp) } - - Box( - modifier = Modifier - .fillMaxSize() - .navigationBarsPadding() - .imePadding() - .background(color = TangemTheme.colors.background.primary), - ) { - Column { - val listState = rememberLazyListState() - val raiseSearchBar by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } - val elevation by animateDpAsState( - targetValue = if (raiseSearchBar) TangemTheme.dimens.elevation8 else TangemTheme.dimens.elevation0, - label = "top_bar_elevation", - ) - - Surface( - elevation = elevation, - modifier = Modifier.onGloballyPositioned { - with(density) { onHeaderSizeChange(it.size.height.toDp()) } - }, - ) { - TokensSearchBar( - state = state.searchBarState, - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing4, - ), - ) - } - - SpacerH18() - - val tokens = state.tokens.collectAsLazyPagingItems() - val query = state.searchBarState.query - - TrackPossibleEmptySearchResult( - tokens = tokens, - query = query, - onEmptySearchResult = state.onEmptySearchResult, - ) - - TokensList( - modifier = Modifier.padding(bottom = tokenListAlertBottomPadding), - tokens = tokens, - addCustomTokenButton = state.addCustomTokenButton, - ) - } - - state.selectedToken?.let { selectedToken -> - ManageTokensBottomSheet(selectedToken = selectedToken, state = state) - } - - state.derivationNotification?.let { - if (keyboard is Keyboard.Closed) { - DerivationNotification( - config = it.config, - modifier = Modifier - .align(Alignment.BottomCenter) - .onGloballyPositioned { - with(density) { tokenListAlertBottomPadding = it.size.height.toDp() } - }, - ) - DisposableEffect(Unit) { - onDispose { tokenListAlertBottomPadding = 0.dp } - } - } - } - } -} - -@Composable -private fun TrackPossibleEmptySearchResult( - tokens: LazyPagingItems, - query: String, - onEmptySearchResult: (String) -> Unit, -) { - val wasLoading = remember { mutableStateOf(false) } - - LaunchedEffect(tokens.loadState) { - val isLoading = tokens.loadState.refresh == LoadState.Loading - val stoppedLoading = wasLoading.value && !isLoading - val queryAndTokensCondition = query.isNotEmpty() && tokens.itemSnapshotList.isEmpty() - - if (stoppedLoading && queryAndTokensCondition) { - onEmptySearchResult(query) - } - - wasLoading.value = isLoading - } -} - -@Composable -private fun ManageTokensBottomSheet(selectedToken: TokenItemState.Loaded, state: ManageTokensState) { - if (state.showChooseWalletScreen && state.chooseWalletState is ChooseWalletState.Choose) { - val config = TangemBottomSheetConfig( - isShow = true, - content = ChooseWalletBottomSheetConfig(state.chooseWalletState), - onDismissRequest = state.chooseWalletState.onCloseChoosingWalletClick, - ) - ChooseWalletBottomSheet(config) - } else { - val config = TangemBottomSheetConfig( - isShow = true, - content = ChooseNetworkBottomSheetConfig( - selectedToken = selectedToken, - chooseWalletState = state.chooseWalletState, - ), - onDismissRequest = selectedToken.chooseNetworkState.onCloseChooseNetworkScreen, - ) - ChooseNetworkBottomSheet(config) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ManageTokensScreen( - @PreviewParameter(ManageTokensConfigProvider::class) - state: ManageTokensState, -) { - TangemThemePreview { - ManageTokensScreen(state) {} - } -} - -private class ManageTokensConfigProvider : CollectionPreviewParameterProvider( - collection = listOf( - ManageTokensStatePreviewData.loadingState, - ManageTokensStatePreviewData.loadedState, - ), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt deleted file mode 100644 index 32962c70db..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/AddCustomTokenButton.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.managetokens.impl.R - -@Composable -internal fun AddCustomTokenButton(onButtonClick: () -> Unit, modifier: Modifier = Modifier) { - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size68) - .fillMaxWidth() - .clickable { onButtonClick() } - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size36) - .background(color = TangemTheme.colors.button.secondary, shape = CircleShape), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_plus_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - SpacerW(width = TangemTheme.dimens.spacing12) - Text( - text = stringResource(id = R.string.add_custom_token_title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun AddCustomTokenButton_Preview() { - TangemThemePreview { - AddCustomTokenButton(onButtonClick = { }) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt deleted file mode 100644 index d1f3822c9b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/DerivationNotification.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.PrimaryButtonIconEndTwoLines -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.previewdata.DerivationNotificationStatePreviewData - -@Composable -internal fun DerivationNotification(config: NotificationConfig, modifier: Modifier = Modifier) { - BaseContainer( - modifier = modifier, - ) { - Column( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), - ) { - MainContent( - iconResId = config.iconResId, - iconTint = TangemTheme.colors.icon.accent, - title = config.title, - subtitle = config.subtitle, - ) - val buttonConfig = config.buttonsState - if (buttonConfig is NotificationConfig.ButtonsState.PrimaryButtonConfig) { - PrimaryButtonIconEndTwoLines( - text = buttonConfig.text.resolveReference(), - iconResId = buttonConfig.iconResId ?: R.drawable.ic_tangem_24, - onClick = buttonConfig.onClick, - modifier = Modifier - .fillMaxWidth(), - additionalText = buttonConfig.additionalText?.resolveReference(), - ) - } - } - } -} - -@Composable -private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit) { - Card( - modifier = modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size62) - .fillMaxWidth(), - shape = RoundedCornerShape( - topStart = TangemTheme.dimens.radius16, - topEnd = TangemTheme.dimens.radius16, - ), - elevation = TangemTheme.dimens.elevation12, - backgroundColor = TangemTheme.colors.background.action, - ) { - Box(content = content) - } -} - -@Composable -private fun MainContent(iconResId: Int, iconTint: Color, title: TextReference, subtitle: TextReference) { - Row { - NotificationIcon(iconResId = iconResId, iconTint = iconTint) - SpacerW(width = TangemTheme.dimens.spacing10) - TextsBlock(title = title, subtitle = subtitle) - } -} - -@Composable -private fun RowScope.NotificationIcon(iconResId: Int, iconTint: Color) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .align(alignment = Alignment.CenterVertically), - ) { - Box( - modifier = Modifier - .size(TangemTheme.dimens.size36) - .background( - color = iconTint.copy(alpha = 0.12f), - shape = CircleShape, - ), - ) - Box( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .background( - color = TangemTheme.colors.background.action, - shape = CircleShape, - ), - ) - Icon( - painter = painterResource(id = iconResId), - contentDescription = null, - tint = iconTint, - ) - } -} - -@Composable -private fun TextsBlock(title: TextReference, subtitle: TextReference) { - Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing2)) { - Text( - text = title.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.button, - ) - - Text( - text = subtitle.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ManageTokensScreen() { - TangemThemePreview { - DerivationNotification(DerivationNotificationStatePreviewData.state.config) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt deleted file mode 100644 index 1a18c5ee93..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/PriceChangesChart.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.Stroke -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -/** - * A chart with a solid line and gradient underneath. It can accept values of any range and number. - * If the last value is bigger or equal to the first, the chart is of accent color, otherwise it's warning color. - * - * @param values a list of float values for a chart. - **/ -@Composable -fun PriceChangesChart(values: ImmutableList, modifier: Modifier = Modifier) { - Row(modifier = modifier) { - if (values.size < 2) return // escape without drawing when there are not enough points - - val lineColor = if (values.last() >= values.first()) { - TangemTheme.colors.icon.accent - } else { - TangemTheme.colors.icon.warning - } - val gradient = Brush.verticalGradient( - colors = listOf(lineColor.copy(alpha = 0.21f), lineColor.copy(alpha = 0.0f)), - ) - Chart(list = values, lineColor = lineColor, gradient = gradient, modifier = Modifier.weight(1f)) - } -} - -@Composable -private fun Chart(list: ImmutableList, lineColor: Color, gradient: Brush, modifier: Modifier = Modifier) { - val max = list.max() - val min = list.min() - val zipList: List> = list.zipWithNext() - - for (pair in zipList) { - val fromValuePercentage = getValuePercentageForRange(pair.first, max, min) - val toValuePercentage = getValuePercentageForRange(pair.second, max, min) - - Canvas( - modifier = modifier.fillMaxHeight(), - onDraw = { - val fromPoint = Offset(x = 0f, y = size.height.times(1 - fromValuePercentage)) - val toPoint = Offset(x = size.width, y = size.height.times(1 - toValuePercentage)) - - val path = drawChartLineAndCreatePath(fromPoint = fromPoint, toPoint = toPoint, lineColor = lineColor) - - fillChart( - path = path, - fromPoint = fromPoint, - toPoint = toPoint, - size = size, - gradient = gradient, - ) - }, - ) - } -} - -private fun DrawScope.drawChartLineAndCreatePath(fromPoint: Offset, toPoint: Offset, lineColor: Color): Path { - val path = Path() - path.moveTo(fromPoint.x, fromPoint.y) - path.lineTo(toPoint.x, toPoint.y) - drawPath( - path = path, - color = lineColor, - style = Stroke(width = 1f), - ) - return path -} - -private fun DrawScope.fillChart(path: Path, fromPoint: Offset, toPoint: Offset, size: Size, gradient: Brush) { - path.lineTo(toPoint.x, size.height) - path.lineTo(fromPoint.x, size.height) - path.lineTo(0f, fromPoint.y) - drawPath( - path = path, - brush = gradient, - ) -} - -private fun getValuePercentageForRange(value: Float, max: Float, min: Float): Float { - return if (max == min) { // to draw a straight line when all values are the same - val modifiedMax = max + 1 - val modifiedMin = min - 1 - (value - modifiedMin) / (modifiedMax - modifiedMin) - } else { - (value - min) / (max - min) - } -} - -@Preview(widthDp = 150, heightDp = 150, showBackground = true) -@Composable -private fun Chart_Positive_Preview() { - TangemThemePreview(isDark = true) { - PriceChangesChart( - persistentListOf(1f, 2f, 4f, 1f, 5f), - ) - } -} - -@Preview(widthDp = 150, heightDp = 150, showBackground = true) -@Composable -private fun Chart_Negative_Preview() { - TangemThemePreview(isDark = true) { - PriceChangesChart( - persistentListOf(10f, 2f, 4f, 1f, 5f), - ) - } -} - -@Preview(widthDp = 150, heightDp = 150, showBackground = true) -@Composable -private fun Chart_Neutral_Preview() { - TangemThemePreview(isDark = true) { - PriceChangesChart( - persistentListOf(5f, 2f, 4f, 1f, 5f), - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt deleted file mode 100644 index 8faf911ebb..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/SearchBar.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.SearchBarState - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -internal fun TokensSearchBar(state: SearchBarState, modifier: Modifier = Modifier) { - val keyboardController = LocalSoftwareKeyboardController.current - val focusManager = LocalFocusManager.current - - TextField( - value = state.query, - onValueChange = state.onQueryChange, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { - keyboardController?.hide() - focusManager.clearFocus() - }, - ), - singleLine = true, - maxLines = 1, - textStyle = TangemTheme.typography.body2.copy( - color = TangemTheme.colors.text.primary1, - ), - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_search_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - modifier = Modifier.clickable { state.onActiveChange(true) }, - ) - }, - trailingIcon = { - if (state.query.isNotEmpty() || state.active) { - IconButton( - onClick = { - if (state.query.isNotEmpty()) { - state.onQueryChange("") - } - focusManager.clearFocus() - keyboardController?.hide() - state.onActiveChange(false) - }, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_close), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - } - }, - placeholder = { - Text( - text = stringResource(R.string.manage_tokens_search_placeholder), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) - }, - shape = RoundedCornerShape(TangemTheme.dimens.radius36), - colors = searchbarTextFieldColors(), - modifier = modifier - .fillMaxWidth() - .onFocusChanged { - if (it.isFocused) { - state.onActiveChange(true) - } else { - state.onActiveChange(false) - } - }, - ) -} - -@Composable -private fun searchbarTextFieldColors(): TextFieldColors { - return TextFieldDefaults.textFieldColors( - backgroundColor = TangemTheme.colors.field.primary, - textColor = TangemTheme.colors.text.primary1, - cursorColor = TangemTheme.colors.icon.primary1, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - disabledIndicatorColor = Color.Transparent, - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_TokensSearchBar( - @PreviewParameter(SearchBarkConfigProvider::class) - state: SearchBarState, -) { - TangemThemePreview { - TokensSearchBar(state) - } -} - -private class SearchBarkConfigProvider : CollectionPreviewParameterProvider( - collection = listOf( - SearchBarState( - query = "BTC", - onQueryChange = {}, - active = true, - onActiveChange = {}, - ), - SearchBarState( - query = "", - onQueryChange = {}, - active = false, - onActiveChange = {}, - ), - ), -) \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt deleted file mode 100644 index 3829ec91db..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenButton.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.material.Icon -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -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 com.tangem.core.ui.components.buttons.PrimarySmallButton -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType - -@Composable -internal fun TokenButton(type: TokenButtonType, onClick: () -> Unit, modifier: Modifier = Modifier) { - when (type) { - TokenButtonType.ADD -> PrimarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.manage_tokens_add), - onClick = onClick, - ), - modifier = modifier, - ) - TokenButtonType.EDIT -> SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.manage_tokens_edit), - onClick = onClick, - ), - modifier = modifier, - ) - TokenButtonType.NOT_AVAILABLE -> { - Box( - modifier = modifier - .size(height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size46) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( - bounded = false, - radius = TangemTheme.dimens.size20, - ), - onClick = onClick, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size20), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } - } - } -} - -@Preview(backgroundColor = 0xffffff, showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun TokenButton_Preview(@PreviewParameter(TokenButtonTypeProvider::class) type: TokenButtonType) { - TangemThemePreview { - TokenButton(type = type, {}) - } -} - -private class TokenButtonTypeProvider : PreviewParameterProvider { - override val values = sequenceOf( - TokenButtonType.ADD, - TokenButtonType.EDIT, - TokenButtonType.NOT_AVAILABLE, - ) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt deleted file mode 100644 index 262ebcf808..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenIcon.kt +++ /dev/null @@ -1,121 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.CircleShimmer -import com.tangem.core.ui.extensions.ImageReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.TokenIconState -import kotlinx.coroutines.launch - -@Composable -internal fun TokenIcon(state: TokenIconState, modifier: Modifier = Modifier) { - val iconModifier = modifier.size(TangemTheme.dimens.size36) - if (state.iconReference != null) { - DefaultCurrencyIcon( - modifier = iconModifier, - iconReference = state.iconReference, - errorIcon = { - PlaceholderIcon( - modifier = iconModifier, - tint = state.placeholderTint, - background = state.placeholderBackground, - ) - }, - ) - } else { - PlaceholderIcon( - modifier = iconModifier, - tint = state.placeholderTint, - background = state.placeholderBackground, - ) - } -} - -@Composable -private fun PlaceholderIcon(tint: Color, background: Color, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .background( - color = background, - shape = CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier.matchParentSize(), - painter = painterResource(id = R.drawable.ic_custom_token_44), - tint = tint, - contentDescription = null, - ) - } -} - -@Composable -private inline fun DefaultCurrencyIcon( - iconReference: ImageReference, - crossinline errorIcon: @Composable () -> Unit, - modifier: Modifier = Modifier, -) { - var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) } - var isBackgroundColorDefined by remember { mutableStateOf(false) } - val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb() - val isDarkTheme = isSystemInDarkTheme() - val coroutineScope = rememberCoroutineScope() - - val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() } - - SubcomposeAsyncImage( - modifier = modifier - .background( - color = iconBackgroundColor, - shape = TangemTheme.shapes.roundedCorners8, - ), - model = ImageRequest.Builder(context = LocalContext.current) - .data(iconReference.getReference()) - .size(size = pixelsSize) - .memoryCacheKey(key = iconReference.getReference().toString() + pixelsSize) - .crossfade(enable = true) - .allowHardware(false) - .listener( - onSuccess = { _, result -> - if (!isBackgroundColorDefined && isDarkTheme) { - coroutineScope.launch { - val color = ImageBackgroundContrastChecker( - drawable = result.drawable, - backgroundColor = itemBackgroundColor, - size = pixelsSize, - ).getContrastColor(isDarkTheme = true) - iconBackgroundColor = color - isBackgroundColorDefined = true - } - } - }, - ) - .build(), - loading = { LoadingIcon() }, - error = { errorIcon() }, - contentDescription = null, - ) -} - -@Composable -internal fun LoadingIcon(modifier: Modifier = Modifier) { - CircleShimmer(modifier = modifier) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt deleted file mode 100644 index 28b7691794..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenPriceChange.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.QuotesState - -@Composable -internal fun TokenPriceChange(state: QuotesState, modifier: Modifier = Modifier) { - when (state) { - is QuotesState.Content -> - PriceChangeBlock(modifier = modifier, type = state.changeType, text = state.priceChange) - QuotesState.Unknown -> PriceChangeBlock(modifier = modifier) - } -} - -@Composable -private fun PriceChangeBlock(modifier: Modifier = Modifier, type: PriceChangeType? = null, text: String? = null) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - PriceChangeIcon(type = type) - SpacerW4() - PriceChangeText(type = type, text = text) - } -} - -@Composable -private fun PriceChangeIcon(type: PriceChangeType?) { - AnimatedContent(targetState = type, label = "Update the price change's arrow") { animatedType -> - animatedType ?: return@AnimatedContent - - Icon( - painter = painterResource( - id = when (animatedType) { - PriceChangeType.UP -> R.drawable.ic_arrow_up_8 - PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 - PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 - }, - ), - tint = when (animatedType) { - PriceChangeType.UP -> TangemTheme.colors.icon.accent - PriceChangeType.DOWN -> TangemTheme.colors.icon.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors.icon.inactive - }, - contentDescription = null, - ) - } -} - -@Composable -private fun PriceChangeText(type: PriceChangeType?, text: String?) { - AnimatedContent(targetState = text, label = "Update the price change's text") { animatedText -> - animatedText ?: return@AnimatedContent - - Text( - text = animatedText, - color = when (type) { - PriceChangeType.UP -> TangemTheme.colors.text.accent - PriceChangeType.DOWN -> TangemTheme.colors.text.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors.text.disabled - null -> TangemTheme.colors.text.primary1 - }, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt deleted file mode 100644 index 50cf504643..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokenRowItem.kt +++ /dev/null @@ -1,203 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -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.components.* -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.managetokens.presentation.managetokens.state.QuotesState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.previewdata.TokenItemStatePreviewData - -private val TOKEN_ITEM_HEIGHT: Dp - @Composable - @ReadOnlyComposable - get() = TangemTheme.dimens.size68 - -@Composable -internal fun TokenRowItem(state: TokenItemState, modifier: Modifier = Modifier) { - when (state) { - is TokenItemState.Loading -> LoadingTokenItem(modifier) - is TokenItemState.Loaded -> LoadedTokenItem(state, modifier) - } -} - -@Composable -private fun LoadedTokenItem(state: TokenItemState.Loaded, modifier: Modifier = Modifier) { - BoxWithConstraints( - modifier = modifier - .fillMaxWidth() - .defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT) - .background(TangemTheme.colors.background.primary), - contentAlignment = Alignment.CenterStart, - ) { - val width = maxWidth - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - ) { - TokenIcon(state = state.tokenIcon) - SpacerW12() - - Column( - modifier = Modifier - .weight(weight = 1f), - ) { - TokenName(name = state.name, currencyId = state.currencySymbol) - TokenPriceData(price = state.rate, quotesState = state.quotes) - } - SpacerW24() - - if (width > TangemTheme.dimens.size350 && // hide chart for small screens - state.quotes is QuotesState.Content - ) { - Chart(quotes = state.quotes) - SpacerW24() - } - - TokenButton( - type = state.availableAction.value, - onClick = { state.onButtonClick(state) }, - ) - } - } -} - -@Composable -private fun TokenName(name: String, currencyId: String) { - Row { - Text( - text = name, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(weight = 1f, fill = false), - ) - SpacerW(width = TangemTheme.dimens.spacing6) - Text( - text = currencyId, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } -} - -@Composable -private fun TokenPriceData(price: String?, quotesState: QuotesState) { - if (price != null) { - Row { - Text( - text = price, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(weight = 1f, fill = false), - ) - SpacerW(width = TangemTheme.dimens.spacing6) - TokenPriceChange(state = quotesState) - } - } -} - -@Composable -private fun Chart(quotes: QuotesState.Content) { - Box( - modifier = Modifier - .size(width = TangemTheme.dimens.size50, height = TangemTheme.dimens.size28), - ) { - PriceChangesChart(values = quotes.chartData) - } -} - -@Composable -private fun LoadingTokenItem(modifier: Modifier = Modifier) { - BaseSurface(modifier) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing4, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - CircleShimmer(modifier = Modifier.size(size = TangemTheme.dimens.size36)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10)) { - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size70, - height = TangemTheme.dimens.size12, - ), - ) - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size52, - height = TangemTheme.dimens.size12, - ), - ) - } - RectangleShimmer( - modifier = Modifier.size( - width = TangemTheme.dimens.size46, - height = TangemTheme.dimens.size12, - ), - ) - } - } - } -} - -@Composable -private fun BaseSurface(modifier: Modifier = Modifier, content: @Composable () -> Unit) { - Surface( - modifier = modifier.defaultMinSize(minHeight = TOKEN_ITEM_HEIGHT), - color = TangemTheme.colors.background.primary, - ) { - content() - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_Tokens(@PreviewParameter(TokenConfigProvider::class) state: TokenItemState) { - TangemThemePreview { - TokenRowItem(state) - } -} - -private class TokenConfigProvider : CollectionPreviewParameterProvider( - collection = listOf( - TokenItemStatePreviewData.tokenLoading, - TokenItemStatePreviewData.loadedPriceDown, - TokenItemStatePreviewData.loadedPriceUp, - TokenItemStatePreviewData.loadedPriceNeutral, - ), -) -// endregion Preview \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt deleted file mode 100644 index ca26ef0621..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/ui/components/TokensList.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.ui.components - -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.paging.LoadState -import androidx.paging.compose.LazyPagingItems -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.managetokens.impl.R -import com.tangem.managetokens.presentation.managetokens.state.AddCustomTokenButton -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState - -private const val PLACEHOLDER_ITEMS_COUNT = 50 - -@Composable -internal fun TokensList( - tokens: LazyPagingItems, - addCustomTokenButton: AddCustomTokenButton, - modifier: Modifier = Modifier, -) { - LazyColumn(modifier = modifier) { - item { - Text( - text = stringResource(id = R.string.manage_tokens_list_header_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - ) - } - - if (tokens.loadState.refresh is LoadState.Loading) { - items(PLACEHOLDER_ITEMS_COUNT) { - TokenRowItem(state = TokenItemState.Loading(it.toString())) - } - } else { - val tokensList = tokens.itemSnapshotList - if (tokensList.isEmpty()) { - item { - Text( - text = stringResource(id = R.string.manage_tokens_nothing_found), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding( - top = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - ), - ) - } - } - - items(items = tokensList.items, key = TokenItemState::id) { token -> - TokenRowItem(state = token) - } - - if (addCustomTokenButton.isVisible) { - item { - AddCustomTokenButton(onButtonClick = addCustomTokenButton.onClick) - } - } - } - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt deleted file mode 100644 index 2b6ba5b3bd..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensClickIntents.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.viewmodels - -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState - -internal interface ManageTokensClickIntents { - - fun onAddCustomTokensButtonClick() - - fun onSearchQueryChange(query: String) - - fun onSearchActiveChange(active: Boolean) - - fun onTokenItemButtonClick(token: TokenItemState.Loaded) - - fun onGetAddressesClick() - - fun onBackClick() - - fun onCloseChooseNetworkScreen() - - fun onNetworkToggleClick(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) - - fun onNonNativeNetworkHintClick() - - fun onChooseWalletClick() - - fun onCloseChoosingWalletClick() - - fun onWalletSelected(walletId: String) -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt deleted file mode 100644 index 663871cf2a..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensUiEvents.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.viewmodels - -internal interface ManageTokensUiEvents { - - fun onEmptySearchResult(query: String) - - fun onAddCustomTokenSheetDismissed() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt deleted file mode 100644 index caa5ca105b..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/managetokens/viewmodels/ManageTokensViewModel.kt +++ /dev/null @@ -1,445 +0,0 @@ -package com.tangem.managetokens.presentation.managetokens.viewmodels - -import androidx.compose.runtime.* -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import androidx.paging.PagingData -import androidx.paging.map -import arrow.core.getOrElse -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.DerivePublicKeysUseCase -import com.tangem.domain.common.util.derivationStyleProvider -import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.domain.wallets.usecase.SelectWalletUseCase -import com.tangem.features.managetokens.navigation.ExpandableState -import com.tangem.managetokens.presentation.common.analytics.ManageTokens -import com.tangem.managetokens.presentation.common.state.AlertState -import com.tangem.managetokens.presentation.common.state.Event -import com.tangem.managetokens.presentation.common.state.NetworkItemState -import com.tangem.managetokens.presentation.managetokens.state.ManageTokensState -import com.tangem.managetokens.presentation.managetokens.state.TokenButtonType -import com.tangem.managetokens.presentation.managetokens.state.TokenItemState -import com.tangem.managetokens.presentation.managetokens.state.factory.* -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.Debouncer -import com.tangem.utils.coroutines.Debouncer.Companion.DEFAULT_WAIT_TIME_MS -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import timber.log.Timber -import java.util.Collections -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList -import javax.inject.Inject -import kotlin.collections.set -import kotlin.properties.Delegates - -@Suppress("LongParameterList", "LargeClass") -@HiltViewModel -internal class ManageTokensViewModel @Inject constructor( - private val dispatchers: CoroutineDispatcherProvider, - private val getGlobalTokenListUseCase: GetGlobalTokenListUseCase, - private val getWalletsUseCase: GetWalletsUseCase, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, - private val selectWalletUseCase: SelectWalletUseCase, - private val getCurrenciesUseCase: GetCryptoCurrenciesUseCase, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, - private val fetchTokenListUseCase: FetchTokenListUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val checkCurrencyCompatibilityUseCase: CheckCurrencyCompatibilityUseCase, - private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, -) : ViewModel(), ManageTokensClickIntents, ManageTokensUiEvents { - - private val stateFactory = ManageTokensStateFactory( - currentStateProvider = Provider { uiState }, - clickIntents = this, - uiIntents = this, - ) - - var uiState: ManageTokensState by mutableStateOf(stateFactory.getInitialState(flowOf(PagingData.from(emptyList())))) - private set - - private var expandableState: ExpandableState = ExpandableState.COLLAPSED - - private val currenciesListJobHolder: JobHolder = JobHolder() - - private val debouncer = Debouncer() - - private var allAddedCurrencies: MutableList = Collections.synchronizedList( - mutableListOf(), - ) - - private var wallets: CopyOnWriteArrayList by Delegates.notNull() - - private var addedCurrenciesByWallet: MutableMap> = ConcurrentHashMap() - - private var selectedWallet: UserWallet? = null - - private var currenciesToGenerateAddresses: Map> = emptyMap() - - private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - - private val quotesStateConverter = QuotesToQuotesStateConverter() - - private val networkToNetworkItemStateConverter = NetworkToNetworkItemStateConverter( - addedCurrenciesByWalletProvider = Provider { addedCurrenciesByWallet }, - selectedWalletProvider = Provider { selectedWallet }, - onNetworkToggleClick = this::onNetworkToggleClick, - ) - - private val networksToChooseNetworkStateConverter = NetworksToChooseNetworkStateConverter( - networkToNetworkItemStateConverter = networkToNetworkItemStateConverter, - onNonNativeNetworkHintClick = this::onNonNativeNetworkHintClick, - onCloseChooseNetworkScreen = this::onCloseChooseNetworkScreen, - ) - - private val tokenConverter = TokenConverter( - quotesStateConverter = quotesStateConverter, - networksToChooseNetworkStateConverter = networksToChooseNetworkStateConverter, - allAddedCurrencies = Provider { allAddedCurrencies }, - selectedAppCurrency = Provider { selectedAppCurrencyFlow.value }, - onTokenItemButtonClick = this::onTokenItemButtonClick, - ) - - init { - analyticsEventHandler.send(ManageTokens.ScreenOpened()) - - viewModelScope.launch(dispatchers.io) { - getWalletsUseCase() - .distinctUntilChanged() - .collectLatest { userWallets -> - launch { - subscribeToCurrencies(userWallets) - }.saveIn(currenciesListJobHolder) - } - } - } - - fun setExpandableState(state: State) { - expandableState = state.value - } - - private suspend fun subscribeToCurrencies(userWallets: List) { - wallets = CopyOnWriteArrayList(userWallets.filter { it.isMultiCurrency && !it.isLocked }) - - combine(wallets.map { getCurrenciesUseCase.invoke(it.walletId).distinctUntilChanged() }) { - if (expandableState == ExpandableState.EXPANDED) return@combine - - allAddedCurrencies.clear() - addedCurrenciesByWallet.clear() - - val walletsWithCurrencies = wallets.zip( - it.map { currencyList -> - currencyList.getOrElse { - Timber.e("Couldn't retrieve currency list") - emptyList() - } - }, - ) - - allAddedCurrencies = walletsWithCurrencies.flatMap { it.second }.toMutableList() - - walletsWithCurrencies.forEach { (wallet, currencies) -> - addedCurrenciesByWallet[wallet] = currencies.toMutableList() - } - - withContext(dispatchers.main) { - uiState = uiState.copy(tokens = getInitialTokensList()) - } - selectedWallet = getSelectedWalletSyncUseCase().fold( - ifLeft = { null }, - ifRight = { if (!it.isMultiCurrency || it.isLocked) null else it }, - ) - if (selectedWallet == null && wallets.isNotEmpty()) { - selectWalletUseCase(wallets.first().walletId) - selectedWallet = wallets.first() - } - updateDerivationNotificationState() - withContext(dispatchers.main) { - uiState = stateFactory.updateChooseWalletState(wallets, userWallets, selectedWallet) - } - }.collect() - } - - private fun getInitialTokensList(searchText: String = ""): Flow> { - return getGlobalTokenListUseCase(searchText = searchText).map { - it.map { token -> tokenConverter.convert(token) } - } - } - - private fun updateDerivationNotificationState() { - viewModelScope.launch(dispatchers.io) { - getMissedAddressesCryptoCurrenciesUseCase(wallets.map { it.walletId }) - .distinctUntilChanged() - .collectLatest { - it.onRight { mapOfMissingDerivations -> - currenciesToGenerateAddresses = mapOfMissingDerivations - withContext(dispatchers.main) { updateDerivation() } - } - } - } - } - - private fun updateDerivation() { - val totalNeeded = currenciesToGenerateAddresses.values.sumOf { derivations -> derivations.size } - val walletsToDerive = currenciesToGenerateAddresses.values - .filter { derivations -> derivations.isNotEmpty() }.size - uiState = stateFactory.updateDerivationNotification( - totalNeeded = totalNeeded, - totalWallets = wallets.size, - walletsToDerive = walletsToDerive, - ) - } - - private fun createSelectedAppCurrencyFlow(): StateFlow { - return getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - } - - override fun onAddCustomTokensButtonClick() { - analyticsEventHandler.send(ManageTokens.ButtonCustomToken) - uiState = uiState.copy(customTokenBottomSheetConfig = uiState.customTokenBottomSheetConfig.copy(isShow = true)) - } - - override fun onSearchQueryChange(query: String) { - uiState = uiState.copy(searchBarState = uiState.searchBarState.copy(query = query)) - - debouncer.debounce(waitMs = DEFAULT_WAIT_TIME_MS, coroutineScope = viewModelScope + dispatchers.io) { - val state = stateFactory.showAddCustomTokensButton(query.isNotBlank()) - uiState = state.copy(tokens = getInitialTokensList(query)) - } - } - - override fun onSearchActiveChange(active: Boolean) { - uiState = uiState.copy(searchBarState = uiState.searchBarState.copy(active = active)) - } - - override fun onTokenItemButtonClick(token: TokenItemState.Loaded) { - when (token.availableAction.value) { - TokenButtonType.ADD, TokenButtonType.EDIT -> { - if (token.availableAction.value == TokenButtonType.ADD) { - analyticsEventHandler.send(ManageTokens.ButtonAdd(token.currencySymbol)) - } - if (token.availableAction.value == TokenButtonType.EDIT) { - analyticsEventHandler.send(ManageTokens.ButtonEdit(token.currencySymbol)) - } - - uiState = uiState.copy(selectedToken = token) - val addedCurrenciesOnWallet = addedCurrenciesByWallet[selectedWallet] ?: listOf() - stateFactory.updateTokenNetworksOnTokenSelection(token, addedCurrenciesOnWallet) - } - TokenButtonType.NOT_AVAILABLE -> { - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert( - AlertState.TokenUnavailable( - onUpvoteClick = {}, // TODO later, when endpoint is available - ), - ), - setUiState = { uiState = it }, - ) - } - } - } - - override fun onGetAddressesClick() { - if (currenciesToGenerateAddresses.isNotEmpty()) { - viewModelScope.launch(dispatchers.io) { - val cardCount = currenciesToGenerateAddresses.count { it.value.isNotEmpty() } - analyticsEventHandler.send(ManageTokens.ButtonGenerateAddresses(cardCount)) - - currenciesToGenerateAddresses.forEach { (walletId, currenciesToDerive) -> - if (currenciesToDerive.isNotEmpty()) { - derivePublicKeysUseCase(walletId, currenciesToDerive) - .onRight { - updateDerivationNotificationState() - fetchTokenListUseCase(userWalletId = walletId) - } - } - } - } - } - } - - override fun onBackClick() { - TODO("Not yet implemented") // TODO: implement if needed when custom tokens and bottom sheet is complete - } - - override fun onCloseChooseNetworkScreen() { - uiState = uiState.copy(selectedToken = null) - } - - override fun onNetworkToggleClick(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) { - val selectedWallet = selectedWallet ?: return - if (!selectedWallet.isMultiCurrency || selectedWallet.isLocked) return - - if (network.isAdded.value) { - analyticsEventHandler.send( - ManageTokens.TokenSwitcherChanged(token = token.currencySymbol, AnalyticsParam.OnOffState.Off), - ) - toggleToken(token, network, selectedWallet) - } else { - analyticsEventHandler.send( - ManageTokens.TokenSwitcherChanged(token = token.currencySymbol, AnalyticsParam.OnOffState.On), - ) - viewModelScope.launch(dispatchers.io) { - checkCompatibilityAndToggleToken(token, network, selectedWallet) - } - } - } - - private suspend fun checkCompatibilityAndToggleToken( - token: TokenItemState.Loaded, - network: NetworkItemState.Toggleable, - selectedWallet: UserWallet, - ) { - checkCurrencyCompatibilityUseCase( - networkId = network.id, - isMainNetwork = network.address == null, - userWalletId = selectedWallet.walletId, - ) - .fold( - ifLeft = { error -> - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert( - stateFactory.transformAddTokenErrorToAlert( - error, - network.name, - ), - ), - setUiState = { uiState = it }, - ) - }, - ifRight = { - withContext(dispatchers.main) { toggleToken(token, network, selectedWallet) } - }, - ) - } - - private fun toggleToken( - token: TokenItemState.Loaded, - network: NetworkItemState.Toggleable, - selectedWallet: UserWallet, - ) { - val cryptoCurrency = requireNotNull( - TokenToCryptoCurrencyConverter( - network = network, - derivationStyleProvider = selectedWallet.scanResponse.derivationStyleProvider, - ).convert(token), - ) { - "It is only null if Blockchain is Unknown, which mustn't happen here" - } - if (!network.isAdded.value) { - addedCurrenciesByWallet[selectedWallet]?.add(cryptoCurrency) - allAddedCurrencies.add(cryptoCurrency) - viewModelScope.launch(dispatchers.io) { - addCryptoCurrenciesUseCase( - userWalletId = selectedWallet.walletId, - currency = cryptoCurrency, - ) - } - updateUi(token, network) - } else { - viewModelScope.launch(dispatchers.io) { - if (canBeRemovedAndShowAlertIfNot(selectedWallet.walletId, cryptoCurrency)) { - addedCurrenciesByWallet[selectedWallet]?.remove(cryptoCurrency) - allAddedCurrencies.remove(cryptoCurrency) - removeCurrencyUseCase(selectedWallet.walletId, cryptoCurrency) - withContext(dispatchers.main) { updateUi(token, network) } - } - } - } - } - - private fun updateUi(token: TokenItemState.Loaded, network: NetworkItemState.Toggleable) { - stateFactory.toggleNetworkState(token, network, allAddedCurrencies) - updateDerivationNotificationState() - } - - private suspend fun canBeRemovedAndShowAlertIfNot( - walletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): Boolean { - return if (cryptoCurrency is CryptoCurrency.Coin && - !isCryptoCurrencyCoinCouldHide(walletId, cryptoCurrency) - ) { - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert( - AlertState.CannotHideNetworkWithTokens( - tokenName = cryptoCurrency.name, - currencySymbol = cryptoCurrency.symbol, - networkName = cryptoCurrency.network.name, - ), - ), - setUiState = { uiState = it }, - ) - false - } else { - true - } - } - - override fun onNonNativeNetworkHintClick() { - analyticsEventHandler.send(ManageTokens.NoticeNonNativeNetworkClicked) - uiState = stateFactory.getStateAndTriggerEvent( - state = uiState, - event = Event.ShowAlert(AlertState.NonNative), - setUiState = { uiState = it }, - ) - } - - override fun onChooseWalletClick() { - analyticsEventHandler.send(ManageTokens.ButtonChooseWallet) - uiState = uiState.copy( - showChooseWalletScreen = true, - ) - } - - override fun onCloseChoosingWalletClick() { - uiState = uiState.copy( - showChooseWalletScreen = false, - ) - } - - override fun onWalletSelected(walletId: String) { - analyticsEventHandler.send(ManageTokens.WalletSelected(ManageTokens.WalletSelected.Source.MainToken)) - viewModelScope.launch(dispatchers.io) { - selectWalletUseCase(UserWalletId(walletId)) - } - selectedWallet = wallets.find { it.walletId.stringValue == walletId } - uiState.selectedToken?.let { onTokenItemButtonClick(it) } - uiState = stateFactory.updateSelectedWallet(selectedWalletId = selectedWallet?.walletId?.stringValue) - } - - override fun onEmptySearchResult(query: String) { - analyticsEventHandler.send(ManageTokens.TokenIsNotFound(query)) - } - - override fun onAddCustomTokenSheetDismissed() { - uiState = uiState.copy(customTokenBottomSheetConfig = uiState.customTokenBottomSheetConfig.copy(isShow = false)) - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensUiImpl.kt b/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensUiImpl.kt deleted file mode 100644 index 9bcf70872f..0000000000 --- a/features/manage-tokens/impl/src/main/java/com/tangem/managetokens/presentation/router/ManageTokensUiImpl.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.managetokens.presentation.router - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.ui.unit.Dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.tangem.features.managetokens.navigation.ExpandableState -import com.tangem.features.managetokens.navigation.ManageTokensUi -import com.tangem.managetokens.presentation.managetokens.ui.ManageTokensScreen -import com.tangem.managetokens.presentation.managetokens.viewmodels.ManageTokensViewModel -import javax.inject.Inject - -internal class ManageTokensUiImpl @Inject constructor() : ManageTokensUi { - - @Composable - override fun Content(onHeaderSizeChange: (Dp) -> Unit, state: State) { - val viewModel = hiltViewModel() - viewModel.setExpandableState(state) - - ManageTokensScreen( - state = viewModel.uiState, - onHeaderSizeChange = onHeaderSizeChange, - ) - } -} \ No newline at end of file diff --git a/features/markets/api/.gitignore b/features/markets/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/markets/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/markets/api/build.gradle.kts b/features/markets/api/build.gradle.kts new file mode 100644 index 0000000000..0248feb86d --- /dev/null +++ b/features/markets/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.markets.api" +} + +dependencies { + implementation(deps.compose.foundation) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt new file mode 100644 index 0000000000..0084b73528 --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/BottomSheetState.kt @@ -0,0 +1,6 @@ +package com.tangem.features.markets + +enum class BottomSheetState { + EXPANDED, + COLLAPSED, +} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt new file mode 100644 index 0000000000..e987e0d66e --- /dev/null +++ b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsListComponent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets + +import androidx.compose.runtime.Stable + +@Stable +interface MarketsListComponent { + + // TODO + + // @Composable + // fun BottomSheetContent(state: State, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier) +} \ No newline at end of file diff --git a/features/markets/impl/.gitignore b/features/markets/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/markets/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts new file mode 100644 index 0000000000..80288a27be --- /dev/null +++ b/features/markets/impl/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.markets.impl" +} + +dependencies { + implementation(deps.compose.coil) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.ui.utils) + + implementation(deps.kotlin.immutable.collections) + + implementation(projects.core.ui) + implementation(projects.common.ui) + implementation(projects.common.uiCharts) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt new file mode 100644 index 0000000000..7ceb15ca05 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/MarketsListItem.kt @@ -0,0 +1,456 @@ +package com.tangem.features.markets.ui + +import android.content.res.Configuration +import androidx.compose.animation.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.layout.* +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +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.unit.IntOffset +import com.tangem.common.ui.charts.MarketChartMini +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.currency.icon.CoinIcon +import com.tangem.core.ui.components.marketprice.PriceChangeInPercent +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.LocalWindowSize +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.windowsize.WindowSizeType +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.ui.models.MarketsListItemModel +import com.tangem.features.markets.ui.preview.MarketChartListItemPreviewDataProvider +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.launch +import kotlin.math.roundToInt +import kotlin.random.Random + +internal enum class DragValue { Start, End } + +const val SWIPE_THRESHOLD_PERCENT = 0.8f +const val SWIPE_VELOCITY_THRESHOLD = 20f + +@Suppress("LongMethod") +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MarketsListItem( + model: MarketsListItemModel, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, + onSwipeToAction: () -> Unit = {}, +) { + val actionWidth = TangemTheme.dimens.size68 + val actionWidthPx = with(LocalDensity.current) { actionWidth.toPx() } + + val hapticManager = LocalHapticManager.current + + val anchors = DraggableAnchors { + DragValue.Start at 0f + DragValue.End at -actionWidthPx + } + val state = remember { + AnchoredDraggableState( + initialValue = DragValue.Start, + anchors = anchors, + positionalThreshold = { it * (1 - SWIPE_THRESHOLD_PERCENT) }, + velocityThreshold = { SWIPE_VELOCITY_THRESHOLD }, + animationSpec = tween(easing = FastOutSlowInEasing), + confirmValueChange = { it == DragValue.Start }, + ) + } + val dragInteractionSource = remember { MutableInteractionSource() } + val clickInteractionSource = remember { MutableInteractionSource() } + val isInDraggedState by dragInteractionSource.collectIsDraggedAsState() + + LaunchedEffect(Unit) { + var actionPerformed = false + var releasePerformed = true + launch { + snapshotFlow { state.offset } + .collect { + val border = -actionWidthPx * SWIPE_THRESHOLD_PERCENT + if (it < border && actionPerformed.not()) { + hapticManager.vibrateLong() + actionPerformed = true + releasePerformed = false + } + + if (it > border) { + if (releasePerformed.not()) { + hapticManager.vibrateShort() + releasePerformed = true + } + actionPerformed = false + } + } + } + launch { + snapshotFlow { isInDraggedState } + .collect { + if (it.not() && actionPerformed) { + releasePerformed = true + onSwipeToAction() + } + } + } + } + + Box( + modifier = Modifier + .height(intrinsicSize = IntrinsicSize.Min) + .fillMaxWidth(), + ) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .fillMaxHeight() + .offset { + IntOffset( + x = actionWidthPx.roundToInt() + + state + .requireOffset() + .toInt(), + y = 0, + ) + } + .width(actionWidth) + .background(TangemTheme.colors.control.checked), + contentAlignment = Alignment.Center, + ) { + Image( + modifier = Modifier.size(TangemTheme.dimens.size28), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_plus_mini_28), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.primary2), + contentDescription = null, + ) + } + + Box( + modifier = modifier + .align(Alignment.CenterStart) + .clip(RectangleShape) + .offset { + IntOffset( + x = state + .requireOffset() + .toInt(), + y = 0, + ) + } + .anchoredDraggable( + state = state, + orientation = Orientation.Horizontal, + interactionSource = dragInteractionSource, + ) + .clickable( + enabled = true, + interactionSource = clickInteractionSource, + indication = rememberRipple(), + onClick = onClick, + ), + ) { + MarketsListItemContent(model = model) + } + } +} + +@Composable +private fun MarketsListItemContent(model: MarketsListItemModel, modifier: Modifier = Modifier) { + val windowSize = LocalWindowSize.current + + Row( + modifier = modifier.padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing15, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + CoinIcon( + modifier = Modifier.size(TangemTheme.dimens.size36), + url = model.iconUrl, + alpha = 1f, + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + + SpacerW12() + + Column(modifier = Modifier.weight(1f)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + TokenTitle( + modifier = Modifier.weight(1f, fill = false), + name = model.name, + currencySymbol = model.currencySymbol, + ) + SpacerW8() + TokenPriceText( + modifier = Modifier.alignByBaseline(), + price = model.price.text, + priceChangeType = model.price.changeType, + ) + } + + SpacerH(height = TangemTheme.dimens.spacing2) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Bottom, + ) { + TokenSubtitle( + modifier = Modifier + .weight(1f, fill = false) + .alignByBaseline(), + ratingPosition = model.ratingPosition, + marketCap = model.marketCap, + ) + PriceChangeInPercent( + modifier = Modifier.alignByBaseline(), + textStyle = TangemTheme.typography.caption2, + type = model.trendType, + valueInPercent = model.trendPercentText, + ) + } + } + + if (windowSize.widthAtLeast(WindowSizeType.Small)) { + Spacer(Modifier.width(TangemTheme.dimens.spacing10)) + + Chart( + chartType = model.chartType, + chartRawData = model.chardData, + ) + } + } +} + +@Composable +private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier = Modifier) { + Row(modifier = modifier) { + Text( + modifier = Modifier + .weight(1f, fill = false) + .alignByBaseline(), + text = name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + SpacerW4() + Text( + modifier = Modifier.alignByBaseline(), + text = currencySymbol, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + maxLines = 1, + overflow = TextOverflow.Visible, + ) + } +} + +@Composable +private fun TokenSubtitle(ratingPosition: String?, marketCap: String?, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + TokenRatingPlace(ratingPosition = ratingPosition) + SpacerW4() + TokenMarketCapText(text = marketCap ?: "") + } +} + +@Composable +private fun RowScope.TokenRatingPlace(ratingPosition: String?) { + Box( + modifier = Modifier + .alignByBaseline() + .heightIn(min = TangemTheme.dimens.size16) + .background( + color = TangemTheme.colors.field.primary, + shape = TangemTheme.shapes.roundedCornersSmall2, + ) + .padding(horizontal = TangemTheme.dimens.size5), // TODO check + ) { + Text( + text = ratingPosition ?: "-", + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + +@Composable +private fun RowScope.TokenMarketCapText(text: String) { + Text( + modifier = Modifier.alignByBaseline(), + text = text, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { + val growColor = TangemTheme.colors.text.accent + val fallColor = TangemTheme.colors.text.warning + val generalColor = TangemTheme.colors.text.primary1 + + val color = remember { Animatable(generalColor) } + + LaunchedEffect(price, growColor, fallColor, generalColor) { + snapshotFlow { price } + .drop(1) + .distinctUntilChanged() + .collectLatest { + val nextColor = when (priceChangeType) { + PriceChangeType.NEUTRAL, + PriceChangeType.UP, + -> growColor + PriceChangeType.DOWN -> fallColor + null -> generalColor + } + + color.animateTo(nextColor, snap()) + color.animateTo(generalColor, tween(durationMillis = 500)) + } + } + + Text( + modifier = modifier, + text = price, + color = color.value, + maxLines = 1, + style = TangemTheme.typography.body2, + overflow = TextOverflow.Visible, + ) +} + +@Composable +private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) { + val chartWidth = TangemTheme.dimens.size56 + Box( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .size(height = TangemTheme.dimens.size32, width = chartWidth), + ) { + if (chartRawData != null) { + MarketChartMini( + rawData = chartRawData, + type = chartType, + ) + } else { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size15) // TODO check + .align(Alignment.Center), + ) + } + } +} + +// region preview +@Preview(showBackground = true, widthDp = 360, name = "normal") +@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, widthDp = 320, name = "small width") +@Composable +private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemModel) { + TangemThemePreview { + var state1 by remember { mutableStateOf(state) } + var state2 by remember { mutableStateOf(state) } + var prices by remember { + mutableStateOf( + listOf( + 100 to PriceChangeType.NEUTRAL, + 200 to PriceChangeType.NEUTRAL, + ), + ) + } + + Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { + MarketsListItem( + modifier = Modifier, + model = state1, + ) + MarketsListItem( + modifier = Modifier, + model = state2, + ) + Row { + Button( + onClick = { + state1 = state1.copy( + trendType = PriceChangeType.entries.random(), + ) + state2 = state2.copy( + trendType = PriceChangeType.entries.random(), + ) + }, + ) { Text(text = "trend") } + + Button( + onClick = { + prices = prices.map { + if (Random.nextBoolean()) { + it.first.inc() to PriceChangeType.UP + } else { + it.first.dec() to PriceChangeType.DOWN + } + } + state1 = state1.copy( + price = MarketsListItemModel.Price( + text = "0.${prices[0].first}023 $", + changeType = prices[0].second, + ), + ) + state2 = state2.copy( + price = MarketsListItemModel.Price( + text = "0.${prices[1].first}023 $", + changeType = prices[1].second, + ), + ) + }, + ) { Text(text = "price") } + } + } + } +} + +// endregion preview \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt new file mode 100644 index 0000000000..4c8d21f229 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/models/MarketsListItemModel.kt @@ -0,0 +1,33 @@ +package com.tangem.features.markets.ui.models + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.marketprice.PriceChangeType + +@Immutable +data class MarketsListItemModel( + val id: String, + val name: String, + val currencySymbol: String, + val iconUrl: String?, + val ratingPosition: String?, + val marketCap: String?, + val price: Price, + val trendPercentText: String, + val trendType: PriceChangeType, + val chardData: MarketChartRawData?, +) { + val chartType: MarketChartLook.Type = when (trendType) { + PriceChangeType.UP, + PriceChangeType.NEUTRAL, + -> MarketChartLook.Type.Growing + PriceChangeType.DOWN -> MarketChartLook.Type.Falling + } + + @Immutable + data class Price( + val text: String, + val changeType: PriceChangeType = PriceChangeType.NEUTRAL, + ) +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt new file mode 100644 index 0000000000..6121a35222 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/ui/preview/MarketChartListItemPreviewDataProvider.kt @@ -0,0 +1,94 @@ +@file:Suppress("MagicNumber") +package com.tangem.features.markets.ui.preview + +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.features.markets.ui.models.MarketsListItemModel + +internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( + collection = listOf( + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = "", + ratingPosition = "10", + marketCap = "$6.233 B", + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.UP, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = "10", + marketCap = "$6.233 B", + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.NEUTRAL, + chardData = null, + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = "10", + marketCap = "$6.23348172384781234 B", + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.DOWN, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = "10", + marketCap = null, + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.UP, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = null, + marketCap = "$6.233 B", + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.UP, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + MarketsListItemModel( + id = "1", + name = "Bitcoin", + currencySymbol = "BTC", + iconUrl = null, + ratingPosition = null, + marketCap = null, + price = MarketsListItemModel.Price(text = "31 285.72$"), + trendPercentText = "12.43%", + trendType = PriceChangeType.UP, + chardData = MarketChartRawData( + y = listOf(0.4f, 0.2f, 0.4f, 0.1f, 0.4f, 2f, 5f, 0.1f, 2f, 2f, 3f), + ), + ), + ), +) \ No newline at end of file diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index 6629611e63..9f081a463f 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { /** Compose libraries */ implementation(deps.compose.material) + implementation(deps.compose.material3) implementation(deps.compose.animation) implementation(deps.compose.foundation) implementation(deps.compose.ui) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt index 978f1ea01e..6db1217109 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt @@ -1,7 +1,6 @@ package com.tangem.feature.onboarding.api import androidx.activity.compose.BackHandler -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.material.LinearProgressIndicator import androidx.compose.runtime.Composable @@ -19,11 +18,9 @@ class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi { @Composable override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) { BackHandler(onBack = uiState.onBackClick) - TangemTheme(isDark = isSystemInDarkTheme()) { - Column { - ProgressIndicator(progress) - Content(subScreen, uiState) - } + Column { + ProgressIndicator(progress) + Content(subScreen, uiState) } } } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt index 5704da9b7f..b6bf1b0f60 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt @@ -4,9 +4,4 @@ package com.tangem.feature.onboarding.navigation * Onboarding router */ // TODO: Move to onboarding api module [REDACTED_JIRA] -interface OnboardingRouter { - - companion object { - const val CAN_SKIP_BACKUP = "onboarding_wallet_can_skip_backup" - } -} \ No newline at end of file +interface OnboardingRouter \ No newline at end of file diff --git a/features/push-notifications/api/.gitignore b/features/push-notifications/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/push-notifications/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/push-notifications/api/build.gradle.kts b/features/push-notifications/api/build.gradle.kts new file mode 100644 index 0000000000..4335917f36 --- /dev/null +++ b/features/push-notifications/api/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.pushnotifications.api" +} + +dependencies { + + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) +} \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt new file mode 100644 index 0000000000..18024f464f --- /dev/null +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/featuretoggles/PushNotificationsFeatureToggles.kt @@ -0,0 +1,9 @@ +package com.tangem.features.pushnotifications.api.featuretoggles + +/** + * Push notifications feature toggles + */ +interface PushNotificationsFeatureToggles { + /** Availability of push notifications */ + val isPushNotificationsEnabled: Boolean +} \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/navigation/PushNotificationsRouter.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/navigation/PushNotificationsRouter.kt new file mode 100644 index 0000000000..6881437671 --- /dev/null +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/navigation/PushNotificationsRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.features.pushnotifications.api.navigation + +import androidx.fragment.app.Fragment + +interface PushNotificationsRouter { + + fun entryFragment(): Fragment +} \ No newline at end of file diff --git a/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt new file mode 100644 index 0000000000..c3d27e3ef5 --- /dev/null +++ b/features/push-notifications/api/src/main/java/com/tangem/features/pushnotifications/api/utils/PermissionUtils.kt @@ -0,0 +1,20 @@ +package com.tangem.features.pushnotifications.api.utils + +import android.Manifest +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast + +@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU) +private val isRequirePushPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + +val PUSH_PERMISSION = if (isRequirePushPermission) { + Manifest.permission.POST_NOTIFICATIONS +} else { + "android.permission.POST_NOTIFICATIONS" +} + +fun getPushPermissionOrNull() = if (isRequirePushPermission) { + Manifest.permission.POST_NOTIFICATIONS +} else { + null +} \ No newline at end of file diff --git a/features/push-notifications/impl/.gitignore b/features/push-notifications/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/push-notifications/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts new file mode 100644 index 0000000000..7cf8d0915d --- /dev/null +++ b/features/push-notifications/impl/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.feature.pushnotifications.impl" +} + +dependencies { + /** AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.activity.compose) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.accompanist.permission) + + /** Other dependencies */ + implementation(deps.timber) + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + + /** Core modules */ + implementation(projects.core.ui) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + + /** Common modules */ + implementation(projects.common.routing) + + /** Common modules */ + implementation(projects.common.routing) + + /** Domain module */ + implementation(projects.domain.settings) + + /** Feature modules */ + implementation(projects.features.pushNotifications.api) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt new file mode 100644 index 0000000000..0b8fedc562 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/PushNotificationsFragment.kt @@ -0,0 +1,39 @@ +package com.tangem.features.pushnotifications.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsScreen +import com.tangem.features.pushnotifications.impl.presentation.viewmodel.PushNotificationViewModel +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +internal class PushNotificationsFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + private val viewModel by viewModels() + + @Composable + override fun ScreenContent(modifier: Modifier) { + BackHandler(onBack = requireActivity()::finish) + NavigationBar3ButtonsScrim() + PushNotificationsScreen( + onShowAllow = viewModel::onAllowPermission, + onAllow = viewModel::onAllowedPermission, + onLater = viewModel::onAskLater, + onOpenSettings = viewModel::openSettings, + ) + } + + companion object { + /** Create push notifications fragment instance */ + fun create(): PushNotificationsFragment = PushNotificationsFragment() + } +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsFeatureTogglesModule.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsFeatureTogglesModule.kt new file mode 100644 index 0000000000..0df7b0a222 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsFeatureTogglesModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.pushnotifications.impl.di + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles +import com.tangem.features.pushnotifications.impl.featuretoggles.DefaultPushNotificationsFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * DI module provides implementation of [PushNotificationsFeatureToggles] + */ +@Module +@InstallIn(SingletonComponent::class) +internal object PushNotificationsFeatureTogglesModule { + + @Provides + @Singleton + fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): PushNotificationsFeatureToggles { + return DefaultPushNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsModule.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsModule.kt new file mode 100644 index 0000000000..b568880483 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/di/PushNotificationsModule.kt @@ -0,0 +1,24 @@ +package com.tangem.features.pushnotifications.impl.di + +import com.tangem.common.routing.AppRouter +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter +import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +/** + * DI module provides implementation of [PushNotificationsRouter] + */ +@Module +@InstallIn(ActivityComponent::class) +object PushNotificationsModule { + + @Provides + @ActivityScoped + fun provideDisclaimerRouter(appRouter: AppRouter): PushNotificationsRouter { + return DefaultPushNotificationsRouter(appRouter) + } +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt new file mode 100644 index 0000000000..296ae7ca81 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/featuretoggles/DefaultPushNotificationsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.pushnotifications.impl.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles + +internal class DefaultPushNotificationsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : PushNotificationsFeatureToggles { + override val isPushNotificationsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED") +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/DefaultPushNotificationsRouter.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/DefaultPushNotificationsRouter.kt new file mode 100644 index 0000000000..b2536cfe47 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/DefaultPushNotificationsRouter.kt @@ -0,0 +1,18 @@ +package com.tangem.features.pushnotifications.impl.navigation + +import androidx.fragment.app.Fragment +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.features.pushnotifications.impl.PushNotificationsFragment +import javax.inject.Inject + +internal class DefaultPushNotificationsRouter @Inject constructor( + private val appRouter: AppRouter, +) : InnerPushNotificationsRouter { + + override fun entryFragment(): Fragment = PushNotificationsFragment.create() + + override fun openHome() { + appRouter.push(AppRoute.Home) + } +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/InnerPushNotificationsRouter.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/InnerPushNotificationsRouter.kt new file mode 100644 index 0000000000..20050eca1e --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/navigation/InnerPushNotificationsRouter.kt @@ -0,0 +1,8 @@ +package com.tangem.features.pushnotifications.impl.navigation + +import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter + +interface InnerPushNotificationsRouter : PushNotificationsRouter { + + fun openHome() +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt new file mode 100644 index 0000000000..9fe0f81030 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/ui/PushNotificationsScreen.kt @@ -0,0 +1,61 @@ +package com.tangem.features.pushnotifications.impl.presentation.ui + +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.showcase.Showcase +import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel +import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.requestPushPermission +import com.tangem.feature.pushnotifications.impl.R +import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun PushNotificationsScreen( + onShowAllow: () -> Unit, + onAllow: () -> Unit, + onLater: () -> Unit, + onOpenSettings: () -> Unit, +) { + val isClicked = remember { mutableStateOf(false) } + val requestPushPermission = requestPushPermission( + isFirstTimeAsking = true, + isClicked = isClicked, + onAllow = onAllow, + onDeny = onLater, + onOpenSettings = onOpenSettings, + pushPermission = getPushPermissionOrNull(), + ) + + Showcase( + headerIconRes = R.drawable.ic_notifications_unread_24, + headerText = resourceReference(R.string.user_push_notification_agreement_header), + showcaseItems = persistentListOf( + ShowcaseItemModel( + R.drawable.ic_rocket_launch_24, + resourceReference(R.string.user_push_notification_agreement_argument_one), + ), + ShowcaseItemModel( + R.drawable.ic_storefront_24, + resourceReference(R.string.user_push_notification_agreement_argument_two), + ), + ), + primaryButton = ShowcaseButtonModel( + buttonText = resourceReference(R.string.common_allow), + onClick = { + isClicked.value = true + onShowAllow() + requestPushPermission() + }, + ), + secondaryButton = ShowcaseButtonModel( + buttonText = resourceReference(R.string.common_later), + onClick = onLater, + ), + modifier = Modifier.systemBarsPadding(), + ) +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt new file mode 100644 index 0000000000..6653043296 --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationViewModel.kt @@ -0,0 +1,46 @@ +package com.tangem.features.pushnotifications.impl.presentation.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.tangem.core.navigation.settings.SettingsManager +import com.tangem.domain.settings.DelayPermissionRequestUseCase +import com.tangem.domain.settings.NeverRequestPermissionUseCase +import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +internal class PushNotificationViewModel @Inject constructor( + private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, + private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, + private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val router: DefaultPushNotificationsRouter, + private val settingsManager: SettingsManager, +) : ViewModel(), PushNotificationsClickIntents { + + override fun onAskLater() { + viewModelScope.launch { + delayPermissionRequestUseCase(PUSH_PERMISSION) + setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + } + router.openHome() + } + + override fun onAllowPermission() { + viewModelScope.launch { + setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + } + } + + override fun onAllowedPermission() { + viewModelScope.launch { + neverRequestPermissionUseCase(PUSH_PERMISSION) + } + router.openHome() + } + + override fun openSettings() = settingsManager.openSettings() +} \ No newline at end of file diff --git a/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt new file mode 100644 index 0000000000..f101e15e7a --- /dev/null +++ b/features/push-notifications/impl/src/main/java/com/tangem/features/pushnotifications/impl/presentation/viewmodel/PushNotificationsClickIntents.kt @@ -0,0 +1,11 @@ +package com.tangem.features.pushnotifications.impl.presentation.viewmodel + +internal interface PushNotificationsClickIntents { + fun onAskLater() + + fun onAllowPermission() + + fun onAllowedPermission() + + fun openSettings() +} \ No newline at end of file diff --git a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt index 7f5f0352b7..049e039209 100644 --- a/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt +++ b/features/qr-scanning/api/src/main/java/com/tangem/feature/qrscanning/QrScanningRouter.kt @@ -5,9 +5,4 @@ import androidx.fragment.app.Fragment interface QrScanningRouter { fun getEntryFragment(): Fragment - - companion object { - const val SOURCE_KEY = "source" - const val NETWORK_KEY = "network" - } } \ No newline at end of file diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index 5ad7d8346d..f2dfb02aa4 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.utils) implementation(projects.core.navigation) + implementation(projects.common.routing) implementation(deps.androidx.fragment.ktx) implementation(deps.androidx.activity.compose) diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt index 72d725c460..5118630639 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/QrScanningFragment.kt @@ -7,21 +7,16 @@ import android.os.Bundle import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.mlkit.vision.common.InputImage import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter @@ -105,7 +100,8 @@ internal class QrScanningFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - StatusBarTransparencyDisposable() + SystemBarsIconsDisposable(darkIcons = false) + QrScanningContent( executor = { cameraExecutor }, analyzer = { cameraAnalyzer }, @@ -145,29 +141,4 @@ internal class QrScanningFragment : ComposeFragment() { fun create() = QrScanningFragment() } -} - -@Composable -private fun StatusBarTransparencyDisposable() { - val systemUiController = rememberSystemUiController() - val systemBarsColor = TangemTheme.colors.background.secondary - val lifecycleOwner = LocalLifecycleOwner.current - DisposableEffect(Unit) { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START) { - systemUiController.setSystemBarsColor( - color = Color.Transparent, - darkIcons = false, - ) - } - if (event == Lifecycle.Event.ON_STOP) { - systemUiController.setSystemBarsColor(systemBarsColor) - } - } - lifecycleOwner.lifecycle.addObserver(observer) - - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt index 5bf6beee3f..b22a6c713f 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/di/QrScanningRouterModule.kt @@ -1,6 +1,6 @@ package com.tangem.feature.qrscanning.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.feature.qrscanning.navigation.DefaultQrScanningRouter import dagger.Module @@ -15,7 +15,7 @@ internal object QrScanningRouterModule { @Provides @ActivityScoped - fun provideQrScanRouter(reduxNavController: ReduxNavController): QrScanningRouter { - return DefaultQrScanningRouter(reduxNavController) + fun provideQrScanRouter(appRouter: AppRouter): QrScanningRouter { + return DefaultQrScanningRouter(appRouter) } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt index 17de053b79..3813274b68 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/navigation/DefaultQrScanningRouter.kt @@ -1,15 +1,16 @@ package com.tangem.feature.qrscanning.navigation import androidx.fragment.app.Fragment -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter + import com.tangem.feature.qrscanning.QrScanningFragment class DefaultQrScanningRouter( - private val reduxNavController: ReduxNavController, + private val router: AppRouter, ) : QrScanningInnerRouter { override fun getEntryFragment(): Fragment = QrScanningFragment.create() override fun popBackStack() { - reduxNavController.popBackStack() + router.pop() } } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt index cca4825f93..49b23ff8cd 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/viewmodel/QrScanningViewModel.kt @@ -3,8 +3,7 @@ package com.tangem.feature.qrscanning.viewmodel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY -import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY +import com.tangem.common.routing.AppRoute import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter import com.tangem.feature.qrscanning.presentation.QrScanningState @@ -24,8 +23,10 @@ internal class QrScanningViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val source: SourceType = savedStateHandle[SOURCE_KEY] ?: error("Source is mandatory") - private val network: String? = savedStateHandle[NETWORK_KEY] + private val source: SourceType = savedStateHandle.get(AppRoute.QrScanning.SOURCE_KEY) + ?.let { SourceType.entries[it] } + ?: error("Source is mandatory") + private val network: String? = savedStateHandle[AppRoute.QrScanning.NETWORK_KEY] val uiState: StateFlow = stateHolder.uiState val launchGalleryEvent: SharedFlow = clickIntents.launchGallery diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index 0a90fdc329..8a99c77aa6 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -12,12 +12,13 @@ android { dependencies { /** Core modules */ - implementation(project(":core:analytics")) + implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(project(":core:res")) - implementation(project(":core:utils")) - implementation(project(":core:ui")) - implementation(project(":libs:crypto")) + implementation(projects.core.res) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.libs.crypto) + implementation(projects.common.routing) /** AndroidX */ implementation(deps.androidx.appCompat) @@ -30,7 +31,7 @@ dependencies { implementation(deps.compose.ui.tooling) /** Domain */ - implementation(project(":features:referral:domain")) + implementation(projects.features.referral.domain) /** Other libraries */ implementation(deps.compose.shimmer) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt index 2ccd4b74fc..cec296cc80 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ReferralFragment.kt @@ -1,19 +1,16 @@ package com.tangem.feature.referral import android.os.Bundle -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.referral.router.ReferralRouter import com.tangem.feature.referral.ui.ReferralScreen import com.tangem.feature.referral.viewmodels.ReferralViewModel import dagger.hilt.android.AndroidEntryPoint -import java.lang.ref.WeakReference import javax.inject.Inject @AndroidEntryPoint @@ -22,22 +19,19 @@ class ReferralFragment : ComposeFragment() { @Inject override lateinit var uiDependencies: UiDependencies + @Inject + internal lateinit var appRouter: AppRouter + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.onScreenOpened() - viewModel.setRouter(ReferralRouter(fragmentManager = WeakReference(parentFragmentManager))) + viewModel.setRouter(ReferralRouter(appRouter)) } @Composable override fun ScreenContent(modifier: Modifier) { - val backgroundColor = TangemTheme.colors.background.secondary - SystemBarsEffect { setSystemBarsColor(backgroundColor) } - - ReferralScreen( - modifier = Modifier.systemBarsPadding(), - stateHolder = viewModel.uiState, - ) + ReferralScreen(stateHolder = viewModel.uiState) } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/router/ReferralRouter.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/router/ReferralRouter.kt index ecd73795bf..a8ac11e54b 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/router/ReferralRouter.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/router/ReferralRouter.kt @@ -1,11 +1,12 @@ package com.tangem.feature.referral.router -import androidx.fragment.app.FragmentManager -import java.lang.ref.WeakReference +import com.tangem.common.routing.AppRouter -internal class ReferralRouter(private val fragmentManager: WeakReference) { +internal class ReferralRouter( + private val appRouter: AppRouter, +) { fun back() { - fragmentManager.get()?.popBackStack() + appRouter.pop() } } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index e44e192d0d..8d8b5aadfa 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -2,23 +2,20 @@ package com.tangem.feature.referral.ui import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.google.accompanist.web.WebView import com.google.accompanist.web.rememberWebViewState import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons -import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.presentation.R /** @@ -28,15 +25,16 @@ import com.tangem.feature.referral.presentation.R */ @Composable internal fun AgreementBottomSheetContent(url: String) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + Column( modifier = Modifier - .fillMaxWidth() - .height(LocalConfiguration.current.screenHeightDp.dp - TangemTheme.dimens.spacing16), + .fillMaxSize() + .verticalScroll(rememberScrollState()), ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title)) - AgreementHtmlView(url = url) - } + AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title)) + AgreementHtmlView(url = url) + Spacer(modifier = Modifier.height(bottomBarHeight)) } } @@ -46,7 +44,7 @@ private fun AgreementHtmlView(url: String) { val isInPreviewMode = LocalInspectionMode.current WebView( state = state, - modifier = Modifier.background(TangemTheme.colors.background.secondary), + modifier = Modifier.background(TangemTheme.colors.background.primary), captureBackPresses = false, onCreated = { if (!isInPreviewMode) { diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt deleted file mode 100644 index 5f11c074e1..0000000000 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.tangem.feature.referral.ui - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview - -@Suppress("LongParameterList") -@Composable -internal fun AwardText( - startText: String, - startTextColor: Color, - startTextStyle: TextStyle, - endText: String, - endTextColor: Color, - endTextStyle: TextStyle, - cornersToRound: CornersToRound, -) { - Surface( - shape = cornersToRound.getShape(), - color = TangemTheme.colors.background.primary, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .heightIn(TangemTheme.dimens.size48) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = startText, - color = startTextColor, - maxLines = 1, - style = startTextStyle, - ) - - Text( - text = endText, - color = endTextColor, - maxLines = 1, - style = endTextStyle, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Composable -private fun Preview_AwardItem() { - TangemThemePreview { - AwardText( - startText = "startText", - startTextColor = TangemTheme.colors.text.tertiary, - startTextStyle = TangemTheme.typography.subtitle2, - endText = "endText", - endTextColor = TangemTheme.colors.text.primary1, - endTextStyle = TangemTheme.typography.subtitle2, - cornersToRound = CornersToRound.TOP_2, - ) - } -} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt deleted file mode 100644 index b5070cdb28..0000000000 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.referral.ui - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme - -internal enum class CornersToRound { - - ALL_4, - TOP_2, - BOTTOM_2, - ZERO, - ; - - @Suppress("TopLevelComposableFunctions") - @Composable - fun getShape(): RoundedCornerShape { - val radius = TangemTheme.dimens.radius12 - return when (this) { - ALL_4 -> RoundedCornerShape(radius) - TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) - BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) - ZERO -> RoundedCornerShape(0.dp) - } - } -} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index cd403043df..01e2c4b3d1 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.core.content.ContextCompat.startActivity import com.tangem.core.ui.components.PrimaryButtonIconStart +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.components.rows.RoundableCornersRow import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.referral.domain.models.ExpectedAward @@ -82,7 +84,7 @@ private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: Expected private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { val isExpectedAwardsPresent = expectedAwards != null - AwardText( + RoundableCornersRow( startText = stringResource(id = R.string.referral_friends_bought_title), startTextColor = TangemTheme.colors.text.tertiary, startTextStyle = TangemTheme.typography.subtitle2, @@ -111,7 +113,7 @@ private fun Awards(expectedAwards: ExpectedAwards) { thickness = TangemTheme.dimens.size0_5, color = TangemTheme.colors.stroke.primary, ) - AwardText( + RoundableCornersRow( startText = if (expectedAwards.expectedAwards.isNotEmpty()) { stringResource(id = R.string.referral_expected_awards) } else { @@ -137,7 +139,7 @@ private fun Awards(expectedAwards: ExpectedAwards) { val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode) initialItems.forEachIndexed { index, expectedAward -> - AwardText( + RoundableCornersRow( startText = expectedAward.paymentDate, startTextColor = TangemTheme.colors.text.primary1, startTextStyle = TangemTheme.typography.subtitle2, @@ -218,7 +220,7 @@ private fun LessMoreButton(isExpanded: MutableState) { private fun ExtraItems(extraItems: List) { Column { extraItems.forEach { expectedAward -> - AwardText( + RoundableCornersRow( startText = expectedAward.paymentDate, startTextColor = TangemTheme.colors.text.primary1, startTextStyle = TangemTheme.typography.subtitle2, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt index 2d70513c8a..d91a122117 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralBottomSheet.kt @@ -1,11 +1,18 @@ package com.tangem.feature.referral.ui +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.SheetState import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.referral.models.ReferralStateHolder @OptIn(ExperimentalMaterial3Api::class) @@ -16,14 +23,18 @@ internal fun ReferralBottomSheet( onDismissRequest: () -> Unit, config: ReferralStateHolder.ReferralInfoState, ) { + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } + if (isVisible) { ModalBottomSheet( + modifier = Modifier.height(LocalWindowSize.current.height - statusBarHeight), onDismissRequest = onDismissRequest, sheetState = sheetState, shape = RoundedCornerShape( topStart = TangemTheme.dimens.radius16, topEnd = TangemTheme.dimens.radius16, ), + windowInsets = WindowInsetsZero, containerColor = TangemTheme.colors.background.primary, ) { AgreementBottomSheetContent( diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 39b216fb16..7d6614a7aa 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -46,16 +46,16 @@ import kotlinx.coroutines.launch */ @OptIn(ExperimentalMaterial3Api::class) @Composable -internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier = Modifier) { +internal fun ReferralScreen(stateHolder: ReferralStateHolder) { var isBottomSheetVisible by remember { mutableStateOf(value = false) } val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val snackbarHostState = remember(::SnackbarHostState) Scaffold( - modifier = modifier, topBar = { AppBarWithBackButton( + modifier = Modifier.statusBarsPadding(), text = stringResource(R.string.details_referral_title), onBackClick = stateHolder.headerState.onBackClicked, ) diff --git a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt index 4a2940f972..286f908ad8 100644 --- a/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt +++ b/features/send/api/src/main/kotlin/com/tangem/features/send/api/navigation/SendRouter.kt @@ -5,13 +5,4 @@ import androidx.fragment.app.Fragment interface SendRouter { fun getEntryFragment(): Fragment - - companion object { - const val CRYPTO_CURRENCY_KEY = "send_crypto_currency" - const val USER_WALLET_ID_KEY = "send_user_wallet_id" - const val TRANSACTION_ID_KEY = "send_transaction_id" - const val AMOUNT_KEY = "send_amount" - const val TAG_KEY = "send_tag" - const val DESTINATION_ADDRESS_KEY = "send_destination_address" - } } \ No newline at end of file diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 66e103efad..9a3de6d8cb 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(deps.jodatime) implementation(deps.timber) implementation(deps.reKotlin) + implementation(deps.kotlin.serialization) /** Compose */ implementation(deps.compose.accompanist.systemUiController) @@ -51,7 +52,8 @@ dependencies { implementation(projects.core.datasource) /** Common */ - implementation(projects.common) + implementation(projects.common.ui) + implementation(projects.common.routing) /** Libs */ implementation(projects.libs.crypto) @@ -72,6 +74,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.feedback) implementation(projects.domain.qrScanning) implementation(projects.domain.qrScanning.models) implementation(projects.domain.settings) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt index 7f38cacd35..b99742c742 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/di/SendRouterModule.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter +import com.tangem.core.navigation.url.UrlOpener import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.DefaultSendRouter import dagger.Module @@ -18,7 +19,7 @@ internal object SendRouterModule { @Provides @ActivityScoped - fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter { - return DefaultSendRouter(reduxNavController) + fun provideSendRouter(appRouter: AppRouter, urlOpener: UrlOpener): SendRouter { + return DefaultSendRouter(appRouter, urlOpener) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt index 0629979fd1..ed9f1f7b14 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/navigation/DefaultSendRouter.kt @@ -1,48 +1,43 @@ package com.tangem.features.send.impl.navigation -import androidx.core.os.bundleOf import androidx.fragment.app.Fragment -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.send.impl.presentation.SendFragment -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultSendRouter( - private val reduxNavController: ReduxNavController, + private val router: AppRouter, + private val urlOpener: UrlOpener, ) : InnerSendRouter { override fun getEntryFragment(): Fragment = SendFragment.create() override fun openUrl(url: String) { - reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + urlOpener.openUrl(url) } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { - reduxNavController.popBackStack() - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, - ), - ), - ) + router.pop { isSuccess -> + if (isSuccess) { + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, + ), + ) + } + } } override fun openQrCodeScanner(network: String) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.QrScanning, - bundle = bundleOf( - QrScanningRouter.SOURCE_KEY to SourceType.SEND, - QrScanningRouter.NETWORK_KEY to network, - ), + router.push( + AppRoute.QrScanning( + source = SourceType.SEND, + networkName = network, ), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt index 4eb34fce88..48899c5eb3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/SendFragment.kt @@ -6,10 +6,10 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter @@ -17,7 +17,6 @@ import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.ui.SendScreen import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel import dagger.hilt.android.AndroidEntryPoint -import java.lang.ref.WeakReference import javax.inject.Inject /** @@ -32,6 +31,9 @@ internal class SendFragment : ComposeFragment() { @Inject lateinit var router: SendRouter + @Inject + lateinit var appRouter: AppRouter + @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler @@ -45,11 +47,11 @@ internal class SendFragment : ComposeFragment() { super.onCreate(savedInstanceState) lifecycle.addObserver(viewModel) - val isEditingDisabled = arguments?.getString(SendRouter.TRANSACTION_ID_KEY) != null + val isEditingDisabled = arguments?.getString(AppRoute.Send.TRANSACTION_ID_KEY) != null viewModel.setRouter( innerSendRouter, StateRouter( - fragmentManager = WeakReference(parentFragmentManager), + appRouter = appRouter, isEditingDisabled = isEditingDisabled, analyticsEventsHandler = analyticsEventsHandler, ), @@ -58,11 +60,6 @@ internal class SendFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.tertiary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle() val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt index ac6485fc8a..7b8bbc3be3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/analytics/utils/SendScreenAnalyticSender.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.analytics.utils import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -36,7 +37,7 @@ internal class SendScreenAnalyticSender( } } SendUiStateType.Amount -> { - val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return + val amountState = state.getAmountState(stateRouterProvider().isEditState) as? AmountState.Data ?: return val isFiatSelected = amountState.amountTextField.isFiatValue val selectedCurrency = if (!isFiatSelected) { SelectedCurrencyType.Token diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index 8e552379b5..e71731954d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -1,18 +1,18 @@ package com.tangem.features.send.impl.presentation.state import com.tangem.blockchain.common.TransactionData -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.common.ui.amountScreen.converters.AmountStateConverter +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage -import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider @@ -33,20 +33,12 @@ internal class SendStateFactory( ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { - SendAmountFieldConverter( - clickIntents = clickIntents, - stateRouterProvider = stateRouterProvider, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, - ) - } private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { - SendAmountStateConverter( + AmountStateConverter( + clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, iconStateConverter = iconStateConverter, userWalletProvider = userWalletProvider, - sendAmountFieldConverter = amountFieldConverter, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } @@ -79,12 +71,19 @@ internal class SendStateFactory( isBalanceHidden = false, cryptoCurrencyName = "", isSubtracted = false, + amountState = AmountState.Empty(false), + editAmountState = AmountState.Empty(false), ) fun getReadyState(): SendUiState { val state = currentStateProvider() + val amountState = if (state.amountState is AmountState.Empty) { + amountStateConverter.convert("") + } else { + state.amountState + } return state.copy( - amountState = state.amountState ?: amountStateConverter.convert(""), + amountState = amountState, recipientState = state.recipientState ?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)), feeState = state.feeState ?: feeStateConverter.convert(Unit), @@ -95,8 +94,13 @@ internal class SendStateFactory( fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState { val state = currentStateProvider() + val amountState = if (state.amountState is AmountState.Empty) { + amountStateConverter.convert(amount) + } else { + state.amountState + } return state.copy( - amountState = state.amountState ?: amountStateConverter.convert(amount), + amountState = amountState, recipientState = state.recipientState ?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)), feeState = state.feeState ?: feeStateConverter.convert(Unit), @@ -117,7 +121,7 @@ internal class SendStateFactory( fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState { val state = currentStateProvider() val balance = cryptoCurrencyStatusProvider().value.amount ?: return state - val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return state + val amountState = state.getAmountState(stateRouterProvider().isEditState) as? AmountState.Data ?: return state val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return state val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO @@ -146,7 +150,7 @@ internal class SendStateFactory( ) } - fun getTransactionSendState(txData: TransactionData, txUrl: String): SendUiState { + fun getTransactionSendState(txData: TransactionData.Uncompiled, txUrl: String): SendUiState { val state = currentStateProvider() val sendState = state.sendState ?: return state return state.copy( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt index c718256d95..e9badc64d0 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendUiState.kt @@ -3,17 +3,14 @@ package com.tangem.features.send.impl.presentation.state import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import com.tangem.blockchain.common.transaction.Fee -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent -import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.PersistentList import java.math.BigDecimal /** @@ -24,11 +21,11 @@ internal data class SendUiState( val clickIntents: SendClickIntents, val isEditingDisabled: Boolean, val cryptoCurrencyName: String, - val amountState: SendStates.AmountState? = null, + val amountState: AmountState, val recipientState: SendStates.RecipientState? = null, val feeState: SendStates.FeeState? = null, val sendState: SendStates.SendState? = null, - val editAmountState: SendStates.AmountState? = null, + val editAmountState: AmountState, val editRecipientState: SendStates.RecipientState? = null, val editFeeState: SendStates.FeeState? = null, val isBalanceHidden: Boolean, @@ -36,7 +33,7 @@ internal data class SendUiState( val event: StateEvent, ) { - fun getAmountState(isEditState: Boolean): SendStates.AmountState? { + fun getAmountState(isEditState: Boolean): AmountState { return if (isEditState) { editAmountState } else { @@ -62,7 +59,7 @@ internal data class SendUiState( fun copyWrapped( isEditState: Boolean, - amountState: SendStates.AmountState? = this.amountState, + amountState: AmountState = this.amountState, feeState: SendStates.FeeState? = this.feeState, recipientState: SendStates.RecipientState? = this.recipientState, sendState: SendStates.SendState? = this.sendState, @@ -90,21 +87,6 @@ internal sealed class SendStates { abstract val isPrimaryButtonEnabled: Boolean - /** Amount state */ - @Stable - data class AmountState( - override val type: SendUiStateType = SendUiStateType.Amount, - override val isPrimaryButtonEnabled: Boolean, - val walletName: String, - val walletBalance: TextReference, - val tokenIconState: TokenIconState, - val segmentedButtonConfig: PersistentList, - val selectedButton: Int, - val isSegmentedButtonsEnabled: Boolean, - val amountTextField: SendTextField.AmountField, - val appCurrencyCode: String, - ) : SendStates() - /** Recipient state */ @Stable data class RecipientState( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt index e09467fa9c..6335c3b154 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/StateRouter.kt @@ -1,15 +1,14 @@ package com.tangem.features.send.impl.presentation.state -import androidx.fragment.app.FragmentManager +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import java.lang.ref.WeakReference internal class StateRouter( - private val fragmentManager: WeakReference, + private val appRouter: AppRouter, private val analyticsEventsHandler: AnalyticsEventHandler, private val isEditingDisabled: Boolean, ) { @@ -26,7 +25,7 @@ internal class StateRouter( } fun popBackStack() { - fragmentManager.get()?.popBackStack() + appRouter.pop() } fun onBackClick(isSuccess: Boolean = false) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt index d933d3276f..204918b99b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.amount +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -64,7 +65,7 @@ internal class AmountStateFactory( fun getOnAmountReduceByState(reduceAmountBy: BigDecimal, reduceAmountByDiff: BigDecimal) = amountReduceByConverter.convert( - SendAmountReduceByConverter.ReduceByData( + AmountReduceByTransformer.ReduceByData( reduceAmountBy = reduceAmountBy, reduceAmountByDiff = reduceAmountByDiff, ), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt index fa31b63ea0..c59bf9780c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountCurrencyConverter.kt @@ -1,46 +1,27 @@ package com.tangem.features.send.impl.presentation.state.amount -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero internal class SendAmountCurrencyConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) : Converter { + override fun convert(value: Boolean): SendUiState { val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState - val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return state - val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() - val isDoneActionEnabled = amountState.isPrimaryButtonEnabled - return if (amountTextField.isFiatValue == value && !isValidFiatRate) { - state - } else { - return state.copyWrapped( - isEditState = isEditState, - amountState = amountState.copy( - amountTextField = amountTextField.copy( - isFiatValue = value, - isValuePasted = true, - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - ), - selectedButton = amountState.segmentedButtonConfig.indexOfFirst { it.isFiat == value }, - ), - ) - } + return state.copyWrapped( + isEditState = isEditState, + amountState = AmountCurrencyTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), + ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt index 093b1b6a8e..3766d7c9c9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountPastedTriggerDismissConverter.kt @@ -1,5 +1,7 @@ package com.tangem.features.send.impl.presentation.state.amount +import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider @@ -9,17 +11,15 @@ internal class SendAmountPastedTriggerDismissConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, ) : Converter { + override fun convert(value: Boolean): SendUiState { val state = currentStateProvider() val isEditState = stateRouterProvider().isEditState - val amountState = state.getAmountState(isEditState) ?: return state + val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return state + return state.copyWrapped( isEditState = isEditState, - amountState = amountState.copy( - amountTextField = amountState.amountTextField.copy( - isValuePasted = false, - ), - ), + amountState = AmountPastedTriggerDismissTransformer().transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt index e937600d51..0468cd8c82 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt @@ -1,67 +1,29 @@ package com.tangem.features.send.impl.presentation.state.amount -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import java.math.BigDecimal internal class SendAmountReduceByConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, -) : Converter { - override fun convert(value: ReduceByData): SendUiState { +) : Converter { + + override fun convert(value: AmountReduceByTransformer.ReduceByData): SendUiState { val state = currentStateProvider() - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals - val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state - val decimalCryptoValue = amountValue.minus(value.reduceAmountByDiff) - val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) - val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, - isFiatValue = false, - decimals = fiatDecimals, - ) - - val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) - val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy( reduceAmountBy = value.reduceAmountBy, ), - amountState = amountState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, - amountTextField = amountTextField.copy( - value = cryptoValue, - fiatValue = fiatValue, - isError = isExceedBalance, - cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), - keyboardType = KeyboardType.Number, - ), - ), - ), + amountState = AmountReduceByTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), ) } - - internal data class ReduceByData( - val reduceAmountBy: BigDecimal, - val reduceAmountByDiff: BigDecimal, - ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt index 76fd7e2654..1bb5518063 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt @@ -1,15 +1,11 @@ package com.tangem.features.send.impl.presentation.state.amount -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero import java.math.BigDecimal internal class SendAmountReduceToConverter( @@ -17,41 +13,15 @@ internal class SendAmountReduceToConverter( private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) : Converter { + override fun convert(value: BigDecimal): SendUiState { val state = currentStateProvider() - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals - val cryptoValue = value.parseBigDecimal(cryptoDecimals) - val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, - isFiatValue = false, - decimals = fiatDecimals, - ) - - val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) - val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isZero() return state.copyWrapped( isEditState = isEditState, - amountState = amountState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, - amountTextField = amountTextField.copy( - value = cryptoValue, - fiatValue = fiatValue, - isError = isExceedBalance, - cryptoAmount = amountTextField.cryptoAmount.copy(value = value), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, value), - keyboardType = KeyboardType.Number, - ), - ), - ), + amountState = AmountReduceToTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 16c1315e2d..98001756d9 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -6,6 +6,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.minimalAmount +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.utils.BigDecimalFormatter @@ -24,8 +27,6 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents import com.tangem.features.send.impl.presentation.state.* import com.tangem.features.send.impl.presentation.state.fee.* -import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.utils.getFiatString import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.isTezos @@ -41,7 +42,7 @@ import java.math.BigDecimal @Suppress("LongParameterList", "LargeClass") internal class SendNotificationFactory( private val cryptoCurrencyStatusProvider: Provider, - private val coinCryptoCurrencyStatusProvider: Provider, + private val feeCryptoCurrencyStatusProvider: Provider, private val currentStateProvider: Provider, private val userWalletProvider: Provider, private val currencyChecksRepository: CurrencyChecksRepository, @@ -62,7 +63,7 @@ internal class SendNotificationFactory( val balance = cryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO val sendState = state.sendState ?: return@map persistentListOf() val feeState = state.getFeeState(isEditState) ?: return@map persistentListOf() - val amountState = state.getAmountState(isEditState) ?: return@map persistentListOf() + val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return@map persistentListOf() val amountValue = amountState.amountTextField.cryptoAmount.value ?: BigDecimal.ZERO val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO @@ -239,7 +240,7 @@ internal class SendNotificationFactory( private fun MutableList.addFeeCoverageNotification( isFeeCoverage: Boolean, - amountField: SendTextField.AmountField, + amountField: AmountFieldModel, sendingValue: BigDecimal, ) { if (isFeeCoverage) { @@ -307,6 +308,7 @@ internal class SendNotificationFactory( private fun checkDustLimits(feeAmount: BigDecimal, receivedAmount: BigDecimal, dustValue: BigDecimal): Boolean { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val feeCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return false val change = when (cryptoCurrencyStatus.currency) { is CryptoCurrency.Coin -> { @@ -314,7 +316,7 @@ internal class SendNotificationFactory( balance - (feeAmount + receivedAmount) } is CryptoCurrency.Token -> { - val balance = coinCryptoCurrencyStatusProvider().value.amount ?: BigDecimal.ZERO + val balance = feeCurrencyStatus.value.amount ?: BigDecimal.ZERO balance - feeAmount } } @@ -351,16 +353,14 @@ internal class SendNotificationFactory( val feeValue = fee?.amount?.value ?: BigDecimal.ZERO val userWalletId = userWalletProvider().walletId val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val feeCurrencyStatus = feeCryptoCurrencyStatusProvider() ?: return val warning = getBalanceNotEnoughForFeeWarningUseCase( fee = feeValue, userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, - coinStatus = coinCryptoCurrencyStatusProvider(), - ).fold( - ifLeft = { null }, - ifRight = { it }, - ) ?: return + coinStatus = feeCurrencyStatus, + ).getOrNull() ?: return val mergeFeeNetworkName = cryptoCurrencyStatus.shouldMergeFeeNetworkName() when (warning) { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt index 62bf1460ad..2d5e5999e3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/BitcoinCustomFeeConverter.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal @@ -14,7 +15,6 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.utils.getFiatReference import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.Provider diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt index d64f1c3d47..0c9185cf91 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/EthereumCustomFeeConverter.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal @@ -14,7 +15,6 @@ import com.tangem.features.send.impl.R import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.features.send.impl.presentation.state.fee.checkExceedBalance import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.utils.getFiatReference import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 112b0f1cb7..4b5dda431d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -1,89 +1,27 @@ package com.tangem.features.send.impl.presentation.state.fields -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseToBigDecimal +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter -import com.tangem.features.send.impl.presentation.state.amount.checkExceedBalance -import com.tangem.features.send.impl.presentation.state.amount.getCryptoValue -import com.tangem.features.send.impl.presentation.state.amount.getFiatValue -import com.tangem.features.send.impl.presentation.state.amount.getKeyboardAction import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import java.math.BigDecimal internal class SendAmountFieldChangeConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, ) : Converter { + override fun convert(value: String): SendUiState { val state = currentStateProvider() - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - if (value.isEmpty()) return state.emptyState() - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals - - val trimmedValue = value.trim() - val cryptoValue = trimmedValue.getCryptoValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, - isFiatValue = amountTextField.isFiatValue, - decimals = cryptoDecimals, - ) - val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) - val (fiatValue, decimalFiatValue) = trimmedValue.getFiatValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, - isFiatValue = amountTextField.isFiatValue, - decimals = fiatDecimals, - ) - - val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) - val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy(reduceAmountBy = null), - amountState = amountState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, - amountTextField = amountTextField.copy( - value = cryptoValue, - fiatValue = fiatValue, - isError = isExceedBalance, - cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), - keyboardType = KeyboardType.Number, - ), - ), - ), - ) - } - - private fun SendUiState.emptyState(): SendUiState { - val isEditState = stateRouterProvider().isEditState - val amountState = getAmountState(isEditState) ?: return this - val amountTextField = amountState.amountTextField - return copyWrapped( - isEditState = isEditState, - amountState = amountState.copy( - isPrimaryButtonEnabled = false, - amountTextField = amountTextField.copy( - value = "", - fiatValue = "", - cryptoAmount = amountTextField.cryptoAmount.copy(value = BigDecimal.ZERO), - fiatAmount = amountTextField.fiatAmount.copy(value = BigDecimal.ZERO), - isError = false, - ), - ), + amountState = AmountFieldChangeTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index 11cfa98faa..5577388040 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -1,16 +1,12 @@ package com.tangem.features.send.impl.presentation.state.fields -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero -import java.math.RoundingMode internal class SendAmountFieldMaxAmountConverter( private val stateRouterProvider: Provider, @@ -23,36 +19,14 @@ internal class SendAmountFieldMaxAmountConverter( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState val amountState = state.getAmountState(isEditState) ?: return state - val amountTextField = amountState.amountTextField - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals val decimalCryptoValue = cryptoCurrencyStatus.value.amount - val decimalFiatValue = cryptoCurrencyStatus.value.fiatAmount - if (decimalCryptoValue.isNullOrZero()) return state - val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() - val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() - val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy(reduceAmountBy = null), - amountState = amountState.copy( - isPrimaryButtonEnabled = true, - amountTextField = amountTextField.copy( - isValuePasted = true, - value = cryptoValue, - fiatValue = fiatValue, - isError = false, - cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - ), - ), + amountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatusProvider()).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt index 4f5ce27038..e289c7968a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendTextField.kt @@ -4,7 +4,6 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.tokens.model.Amount @Immutable internal sealed class SendTextField { @@ -18,22 +17,6 @@ internal sealed class SendTextField { /** Keyboard options */ abstract val keyboardOptions: KeyboardOptions - data class AmountField( - override val value: String, - override val onValueChange: (String) -> Unit, - override val keyboardOptions: KeyboardOptions, - val keyboardActions: KeyboardActions, - val cryptoAmount: Amount, - val fiatAmount: Amount, - val isFiatValue: Boolean, - val fiatValue: String, - val isFiatUnavailable: Boolean, - val isValuePasted: Boolean, - val onValuePastedTriggerDismiss: () -> Unit, - val isError: Boolean, - val error: TextReference, - ) : SendTextField() - data class RecipientAddress( override val value: String, override val onValueChange: (String) -> Unit, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt index 2eeda94fd5..8eab2cbac3 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendClickIntentsStub.kt @@ -29,6 +29,7 @@ internal object SendClickIntentsStub : SendClickIntents { override fun onAmountValueChange(value: String) {} override fun onCurrencyChangeClick(isFiat: Boolean) {} + override fun onAmountNext() {} override fun onMaxValueClick() {} diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendStatesPreviewData.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendStatesPreviewData.kt index 2366b09686..dd64a24aed 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendStatesPreviewData.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/previewdata/SendStatesPreviewData.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.state.previewdata +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.core.ui.event.consumedEvent import com.tangem.features.send.impl.presentation.state.SendUiState diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index f5247e516a..e47d869fb6 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -21,6 +21,9 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.utils.getCryptoReference +import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.R import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SecondaryButtonIconStart @@ -32,12 +35,9 @@ import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.domain.tokens.model.Amount import com.tangem.features.send.impl.presentation.state.SendUiCurrentScreen import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType -import com.tangem.features.send.impl.presentation.utils.getFiatString -import com.tangem.features.send.impl.presentation.utils.getCryptoReference @Composable internal fun SendNavigationButtons( @@ -166,7 +166,7 @@ private fun SendingText( exit = fadeOut(tween(durationMillis = 300)), label = "Animate show sending state text", ) { - val amountState = uiState.getAmountState(isEditState) + val amountState = uiState.getAmountState(isEditState) as? AmountState.Data val feeState = uiState.getFeeState(isEditState) val fiatRate = feeState?.rate val fiatAmount = amountState?.amountTextField?.fiatAmount diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index cdef0b59c6..992da60b9f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -16,6 +16,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.AmountScreenContent +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference @@ -28,7 +30,6 @@ import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.SendUiStateType import com.tangem.features.send.impl.presentation.state.previewdata.ConfirmStatePreviewData import com.tangem.features.send.impl.presentation.state.previewdata.SendStatesPreviewData -import com.tangem.features.send.impl.presentation.ui.amount.SendAmountContent import com.tangem.features.send.impl.presentation.ui.fee.SendSpeedAndFeeContent import com.tangem.features.send.impl.presentation.ui.recipient.SendRecipientContent import com.tangem.features.send.impl.presentation.ui.send.SendContent @@ -46,10 +47,10 @@ internal fun SendScreen(uiState: SendUiState, currentState: SendUiCurrentScreen) BackHandler(onBack = onBackClick) Column( modifier = Modifier + .background(color = TangemTheme.colors.background.tertiary) .fillMaxSize() .imePadding() - .systemBarsPadding() - .background(color = TangemTheme.colors.background.tertiary), + .systemBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { SendAppBar( @@ -87,7 +88,7 @@ private fun SendAppBar(uiState: SendUiState, currentState: SendUiCurrentScreen) -> resourceReference(R.string.common_fee_selector_title) to null SendUiStateType.Send -> if (uiState.sendState?.isSuccess == false) { resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName)) to - uiState.amountState?.walletName + (uiState.amountState as? AmountState.Data)?.walletName } else { null to null } @@ -160,13 +161,13 @@ private fun SendScreenContent(uiState: SendUiState, currentState: SendUiCurrentS isTransitionAnimationRunning = transition.targetState != transition.currentState when (state.type) { - SendUiStateType.Amount -> SendAmountContent( + SendUiStateType.Amount -> AmountScreenContent( amountState = uiState.amountState, isBalanceHiding = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) - SendUiStateType.EditAmount -> SendAmountContent( - amountState = uiState.editAmountState, + SendUiStateType.EditAmount -> AmountScreenContent( + amountState = uiState.editAmountState!!, isBalanceHiding = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, ) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt index 1c7e332844..1af5a5154a 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendCustomFee.kt @@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.fields.SendTextField -import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import com.tangem.core.ui.components.containers.FooterContainer import kotlinx.collections.immutable.ImmutableList @Composable diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt index f403cf423b..096fbe1d8b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelector.kt @@ -63,7 +63,7 @@ internal fun SendSpeedSelector( onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) }, ) SendSpeedSelectorItem( - titleRes = R.string.common_fee_selector_option_custom, + titleRes = R.string.common_custom, iconRes = R.drawable.ic_edit_24, feeType = FeeType.Custom, state = state, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index ed1ad443a4..b1cf3f315d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.amountScreen.utils.getCryptoReference +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem @@ -20,8 +22,6 @@ import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType -import com.tangem.features.send.impl.presentation.utils.getCryptoReference -import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable internal fun SendSpeedSelectorItem( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt index b3402a2bbe..d1421da969 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/SendRecipientContent.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.inputrow.InputRowRecipient import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -33,8 +32,9 @@ import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fields.SendTextField import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub -import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents +import com.tangem.utils.Strings.STARS import kotlinx.collections.immutable.ImmutableList private const val ADDRESS_FIELD_KEY = "ADDRESS_FIELD_KEY" diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt index e11b62e872..762dc7f7eb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/recipient/TextFieldWithPaste.kt @@ -17,7 +17,7 @@ import com.tangem.core.ui.components.inputrow.inner.PasteButton import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.send.impl.presentation.ui.common.FooterContainer +import com.tangem.core.ui.components.containers.FooterContainer @Composable internal fun TextFieldWithPaste( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index f2dcd33987..11c4f4ebab 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.utils.getCryptoReference +import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.res.TangemThemePreview @@ -24,8 +26,6 @@ import com.tangem.features.send.impl.presentation.state.SendStates import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState import com.tangem.features.send.impl.presentation.state.fee.FeeType import com.tangem.features.send.impl.presentation.state.previewdata.FeeStatePreviewData -import com.tangem.features.send.impl.presentation.utils.getCryptoReference -import com.tangem.features.send.impl.presentation.utils.getFiatReference @Composable internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, onClick: () -> Unit) { @@ -53,7 +53,7 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24 FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24 - FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24 + FeeType.Custom -> R.string.common_custom to R.drawable.ic_edit_24 } } else { R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt index 0f6304ae81..2576d7b37f 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/SendContent.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.common.ui.amountScreen.ui.AmountBlock import com.tangem.core.ui.components.transactions.TransactionDoneTitle import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -53,7 +54,7 @@ internal fun SendContent(uiState: SendUiState) { } private fun LazyListScope.blocks(uiState: SendUiState) { - val amountState = uiState.amountState ?: return + val amountState = uiState.amountState val recipientState = uiState.recipientState ?: return val feeState = uiState.feeState ?: return val sendState = uiState.sendState ?: return diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt index 457de01af1..b7bc64d493 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.viewmodel +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource @@ -8,7 +9,7 @@ import com.tangem.features.send.impl.presentation.state.fee.FeeType import java.math.BigDecimal @Suppress("TooManyFunctions") -internal interface SendClickIntents { +internal interface SendClickIntents : AmountScreenClickIntents { fun popBackStack() @@ -26,16 +27,6 @@ internal interface SendClickIntents { fun onTokenDetailsClick(userWalletId: UserWalletId, currency: CryptoCurrency) - // region Amount - fun onAmountValueChange(value: String) - - fun onCurrencyChangeClick(isFiat: Boolean) - - fun onMaxValueClick() - - fun onAmountPasteTriggerDismiss() - // endregion - // region Recipient fun onRecipientAddressValueChange(value: String, type: EnterAddressSource? = null) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 622ca294c5..446343bfeb 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.send.impl.presentation.viewmodel +import android.os.Bundle import android.os.SystemClock import androidx.lifecycle.* import arrow.core.Either @@ -7,6 +8,9 @@ import arrow.core.getOrElse import arrow.core.left import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -35,7 +39,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.send.impl.navigation.InnerSendRouter import com.tangem.features.send.impl.presentation.analytics.EnterAddressSource import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents @@ -66,7 +69,6 @@ internal class SendViewModel @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, - private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, @@ -99,17 +101,18 @@ internal class SendViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, SendClickIntents { - private val userWalletId: UserWalletId = savedStateHandle.get(SendRouter.USER_WALLET_ID_KEY) - ?.let { stringValue -> UserWalletId(stringValue) } + private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Send.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) ?: error("This screen can't open without `UserWalletId`") - private val cryptoCurrency: CryptoCurrency = savedStateHandle[SendRouter.CRYPTO_CURRENCY_KEY] + private val cryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Send.CRYPTO_CURRENCY_KEY) + ?.unbundle(CryptoCurrency.serializer()) ?: error("This screen can't open without `CryptoCurrency`") - private val transactionId: String? = savedStateHandle[SendRouter.TRANSACTION_ID_KEY] - private val amount: String? = savedStateHandle[SendRouter.AMOUNT_KEY] - private val destinationAddress: String? = savedStateHandle[SendRouter.DESTINATION_ADDRESS_KEY] - private val memo: String? = savedStateHandle[SendRouter.TAG_KEY] + private val transactionId: String? = savedStateHandle[AppRoute.Send.TRANSACTION_ID_KEY] + private val amount: String? = savedStateHandle[AppRoute.Send.AMOUNT_KEY] + private val destinationAddress: String? = savedStateHandle[AppRoute.Send.DESTINATION_ADDRESS_KEY] + private val memo: String? = savedStateHandle[AppRoute.Send.TAG_KEY] private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() @@ -167,7 +170,7 @@ internal class SendViewModel @Inject constructor( private val sendNotificationFactory = SendNotificationFactory( cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - coinCryptoCurrencyStatusProvider = Provider { coinCryptoCurrencyStatus }, + feeCryptoCurrencyStatusProvider = Provider { feeCryptoCurrencyStatus }, currentStateProvider = Provider { uiState.value }, userWalletProvider = Provider { userWallet }, stateRouterProvider = Provider { stateRouter }, @@ -199,7 +202,6 @@ internal class SendViewModel @Inject constructor( private var isAmountSubtractAvailable: Boolean = false private var isUtxoConsolidationAvailable: Boolean = false private var isTapHelpPreviewEnabled: Boolean = false - private var coinCryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null @@ -277,33 +279,18 @@ internal class SendViewModel @Inject constructor( } private suspend fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { - val maybeCurrencyStatus = getCurrencyStatus( + getCurrencyStatus( isSingleWalletWithToken = isSingleWalletWithToken, isMultiCurrency = isMultiCurrency, + ).fold( + ifRight = { cryptoCurrencyStatus -> + onDataLoaded( + currencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = getFeeCurrencyStatusSync(cryptoCurrencyStatus, isMultiCurrency), + ) + }, + ifLeft = { showErrorAlert() }, ) - val maybeCoinStatus = if (cryptoCurrency is CryptoCurrency.Coin) { - maybeCurrencyStatus - } else { - getCoinCurrencyStatusUpdates(isSingleWalletWithToken) - } - - if (maybeCoinStatus.isRight() && maybeCurrencyStatus.isRight()) { - val currencyStatus = maybeCurrencyStatus.getOrElse { - showErrorAlert() - return Timber.e("Currency status is unreachable") - } - val coinStatus = maybeCoinStatus.getOrElse { - showErrorAlert() - return Timber.e("Coin status is unreachable") - } - onDataLoaded( - currencyStatus = currencyStatus, - coinCurrencyStatus = coinStatus, - feeCurrencyStatus = getFeeCurrencyStatusSync(currencyStatus, isMultiCurrency), - ) - } else { - showErrorAlert() - } } private fun getTapHelpPreviewAvailability() { @@ -312,14 +299,6 @@ internal class SendViewModel @Inject constructor( } } - private suspend fun getCoinCurrencyStatusUpdates(isSingleWalletWithToken: Boolean) = getNetworkCoinStatusUseCase - .invokeSync( - userWalletId = userWalletId, - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = isSingleWalletWithToken, - ) - private suspend fun getCurrencyStatus( isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean, @@ -361,13 +340,8 @@ internal class SendViewModel @Inject constructor( ) } - private fun onDataLoaded( - currencyStatus: CryptoCurrencyStatus, - coinCurrencyStatus: CryptoCurrencyStatus, - feeCurrencyStatus: CryptoCurrencyStatus?, - ) { + private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus?) { cryptoCurrencyStatus = currencyStatus - coinCryptoCurrencyStatus = coinCurrencyStatus feeCryptoCurrencyStatus = feeCurrencyStatus subscribeOnQRScannerResult() when { @@ -518,6 +492,8 @@ internal class SendViewModel @Inject constructor( stateRouter.onNextClick() } + override fun onAmountNext() = onNextClick(stateRouter.isEditState) + override fun onPrevClick() { cancelFeeRequest() stateRouter.onPrevClick() @@ -531,7 +507,7 @@ internal class SendViewModel @Inject constructor( override fun onFailedTxEmailClick(errorMessage: String) { val recipient = uiState.value.recipientState?.addressTextField?.value val feeValue = uiState.value.feeState?.fee?.amount?.value - val amountValue = uiState.value.amountState?.amountTextField?.cryptoAmount?.value + val amountValue = (uiState.value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value val receivingAmount = if (amountValue != null && feeValue != null) { checkAndCalculateSubtractedAmount( @@ -552,6 +528,7 @@ internal class SendViewModel @Inject constructor( fee = feeValue, destinationAddress = recipient, errorMessage = errorMessage, + scanResponse = userWallet.scanResponse, ), ) } @@ -766,7 +743,7 @@ internal class SendViewModel @Inject constructor( private suspend fun callFeeUseCase(): Either? { val isFromConfirmation = stateRouter.currentState.value.isFromConfirmation - val amountState = uiState.value.getAmountState(isFromConfirmation) ?: return null + val amountState = uiState.value.getAmountState(isFromConfirmation) as? AmountState.Data ?: return null val recipientState = uiState.value.getRecipientState(isFromConfirmation) ?: return null val amount = amountState.amountTextField.cryptoAmount.value ?: return null @@ -857,7 +834,9 @@ internal class SendViewModel @Inject constructor( val feeState = uiState.value.feeState ?: return val fee = feeState.fee ?: return val memo = uiState.value.recipientState?.memoTextField?.value - val amountValue = uiState.value.amountState?.amountTextField?.cryptoAmount?.value ?: return + val amountValue = (uiState.value.amountState as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value + ?: return val feeValue = fee.amount.value ?: return val receivingAmount = checkAndCalculateSubtractedAmount( @@ -892,7 +871,7 @@ internal class SendViewModel @Inject constructor( } } - private suspend fun sendTransaction(txData: TransactionData) { + private suspend fun sendTransaction(txData: TransactionData.Uncompiled) { val result = sendTransactionUseCase( txData = txData, userWallet = userWallet, @@ -935,7 +914,7 @@ internal class SendViewModel @Inject constructor( } } - private suspend fun updateTransactionStatus(txData: TransactionData) { + private suspend fun updateTransactionStatus(txData: TransactionData.Uncompiled) { val txUrl = getExplorerTransactionUrlUseCase( userWalletId = userWalletId, network = cryptoCurrency.network, diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index ac2ce74301..8325def273 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -47,8 +47,24 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) + + /** Domain */ + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + implementation(projects.domain.staking) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.transaction) + /** Common */ - implementation(projects.common) + implementation(projects.common.ui) + implementation(projects.common.routing) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt index 83cd830e4d..351a4a7a28 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -1,8 +1,9 @@ package com.tangem.features.staking.impl.navigation import androidx.fragment.app.Fragment +import com.tangem.features.staking.impl.presentation.StakingFragment internal class DefaultStakingRouter : InnerStakingRouter { - override fun getEntryFragment(): Fragment = TODO() + override fun getEntryFragment(): Fragment = StakingFragment.create() } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt index 7d7aa226d4..dd7187d902 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -2,4 +2,7 @@ package com.tangem.features.staking.impl.navigation import com.tangem.features.staking.api.navigation.StakingRouter -interface InnerStakingRouter : StakingRouter \ No newline at end of file +interface InnerStakingRouter : StakingRouter { + + // TODO staking +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt new file mode 100644 index 0000000000..64b167ead5 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/StakingFragment.kt @@ -0,0 +1,73 @@ +package com.tangem.features.staking.impl.presentation + +import android.os.Bundle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.UiDependencies +import com.tangem.core.ui.screen.ComposeFragment +import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.state.StakingStateController +import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.ui.StakingScreen +import com.tangem.features.staking.impl.presentation.viewmodel.StakingViewModel +import dagger.hilt.android.AndroidEntryPoint +import java.lang.ref.WeakReference +import javax.inject.Inject + +/** + * Staking fragment + */ +@AndroidEntryPoint +internal class StakingFragment : ComposeFragment() { + + @Inject + override lateinit var uiDependencies: UiDependencies + + @Inject + lateinit var router: StakingRouter + + @Inject + lateinit var stateController: StakingStateController + + @Inject + lateinit var analyticsEventsHandler: AnalyticsEventHandler + + private val viewModel by viewModels() + private val innerStakingRouter: InnerStakingRouter + get() = requireNotNull(router as? InnerStakingRouter) { + "innerStakingRouter should be instance of InnerStakingRouter" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) + + viewModel.setRouter( + innerStakingRouter, + StakingStateRouter( + fragmentManager = WeakReference(parentFragmentManager), + stateController = stateController, + ), + ) + } + + @Composable + override fun ScreenContent(modifier: Modifier) { + val currentState = viewModel.uiState.collectAsStateWithLifecycle() + StakingScreen(currentState.value) + } + + override fun onDestroy() { + lifecycle.removeObserver(viewModel) + super.onDestroy() + } + + companion object { + /** Create staking fragment instance */ + fun create(): StakingFragment = StakingFragment() + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt new file mode 100644 index 0000000000..c58332a870 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/FeeState.kt @@ -0,0 +1,20 @@ +package com.tangem.features.staking.impl.presentation.state + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.appcurrency.model.AppCurrency +import java.math.BigDecimal + +sealed class FeeState { + + data class Content( + val fee: Fee?, + val rate: BigDecimal?, + val isFeeConvertibleToFiat: Boolean, + val appCurrency: AppCurrency, + val isFeeApproximate: Boolean, + ) : FeeState() + + data object Loading : FeeState() + + data object Error : FeeState() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt new file mode 100644 index 0000000000..7a0acf09f9 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerFeeState.kt @@ -0,0 +1,14 @@ +package com.tangem.features.staking.impl.presentation.state + +import com.tangem.blockchain.common.transaction.TransactionFee + +internal sealed class InnerFeeState { + + data class Content( + val fees: TransactionFee, + ) : InnerFeeState() + + data object Loading : InnerFeeState() + + data object Error : InnerFeeState() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt new file mode 100644 index 0000000000..61015e2357 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -0,0 +1,36 @@ +package com.tangem.features.staking.impl.presentation.state + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.staking.model.Yield +import kotlinx.collections.immutable.ImmutableList + +sealed class InnerYieldBalanceState { + data class Data( + val rewardsCrypto: String, + val rewardsFiat: String, + val isRewardsToClaim: Boolean, + val balance: List, + ) : InnerYieldBalanceState() + + data object Empty : InnerYieldBalanceState() +} + +data class BalanceGroupedState( + val items: ImmutableList, + val footer: TextReference?, + val title: TextReference, +) + +data class BalanceState( + val validator: Yield.Validator, + val cryptoValue: String, + val cryptoAmount: TextReference, + val fiatAmount: TextReference, + val rawCurrencyId: String?, +) + +enum class BalanceGroupType { + ACTIVE, + UNSTAKED, + UNKNOWN, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt new file mode 100644 index 0000000000..c133fe00ca --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingAlertState.kt @@ -0,0 +1,24 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.staking.impl.R + +@Immutable +internal sealed class StakingAlertState { + + abstract val title: TextReference? + abstract val message: TextReference + open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) + open val onConfirmClick: (() -> Unit)? = null + + data class GenericError( + override val title: TextReference? = TODO(), + override val onConfirmClick: () -> Unit, + ) : StakingAlertState() { + override val message: TextReference = resourceReference(R.string.common_unknown_error) + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_support) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt new file mode 100644 index 0000000000..dbc800ea01 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingEvent.kt @@ -0,0 +1,12 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class StakingEvent { + + data class ShowSnackBar(val text: TextReference) : StakingEvent() + + data class ShowAlert(val alert: StakingAlertState) : StakingEvent() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt new file mode 100644 index 0000000000..967f681edb --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -0,0 +1,54 @@ +package com.tangem.features.staking.impl.presentation.state + +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.features.staking.impl.R + +internal sealed class StakingNotification(val config: NotificationConfig) { + + sealed class Error( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, + buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : StakingNotification( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonState, + onCloseClick = onCloseClick, + ), + ) { + // TODO staking + } + + sealed class Warning( + title: TextReference, + subtitle: TextReference, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : StakingNotification( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ), + ) { + data class EarnRewards( + val currencyName: String, + val days: Int, + ) : Warning( + title = resourceReference(R.string.staking_notification_earn_rewards_title), + subtitle = resourceReference( + R.string.staking_notification_earn_rewards_text, + wrappedList(currencyName, days), + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt new file mode 100644 index 0000000000..d62526b9a8 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -0,0 +1,49 @@ +package com.tangem.features.staking.impl.presentation.state + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.event.consumedEvent +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub +import com.tangem.utils.transformer.Transformer +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class StakingStateController @Inject constructor() { + + val value: StakingUiState get() = uiState.value + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + val uiState: StateFlow get() = mutableUiState.asStateFlow() + + fun update(function: (StakingUiState) -> StakingUiState) { + mutableUiState.update(function = function) + } + + fun update(transformer: Transformer) { + mutableUiState.update(function = transformer::transform) + } + + fun clear() { + mutableUiState.update { getInitialState() } + } + + private fun getInitialState(): StakingUiState { + return StakingUiState( + clickIntents = StakingClickIntentsStub, + cryptoCurrencyName = "", + currentStep = StakingStep.InitialInfo, + initialInfoState = StakingStates.InitialInfoState.Empty(), + amountState = AmountState.Empty(), + rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), + confirmStakingState = StakingStates.ConfirmStakingState.Empty(), + isBalanceHidden = false, + event = consumedEvent(), + bottomSheetConfig = null, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt new file mode 100644 index 0000000000..5f277ce4a4 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -0,0 +1,74 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.fragment.app.FragmentManager +import java.lang.ref.WeakReference + +internal class StakingStateRouter( + private val fragmentManager: WeakReference, + private val stateController: StakingStateController, +) { + + private fun closeStaking() { + fragmentManager.get()?.popBackStack() + stateController.clear() + } + + fun onBackClick(isSuccess: Boolean = false) { + val type = stateController.uiState.value.currentStep + when { + isSuccess -> closeStaking() + else -> when (type) { + StakingStep.Amount -> showInitial() + StakingStep.Confirm -> showAmount() + else -> closeStaking() + } + } + } + + fun onNextClick() { + when (stateController.uiState.value.currentStep) { + StakingStep.InitialInfo -> showAmount() + StakingStep.RewardsValidators, + StakingStep.Validators, + StakingStep.Amount, + -> showConfirm() + StakingStep.Confirm -> showSuccess() + StakingStep.Success -> closeStaking() + } + } + + fun onPrevClick() { + when (stateController.uiState.value.currentStep) { + StakingStep.Amount -> showInitial() + StakingStep.Confirm -> showAmount() + StakingStep.Success -> closeStaking() + StakingStep.Validators -> showConfirm() + else -> closeStaking() + } + } + + private fun showInitial() { + stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } + } + + fun showRewardsValidators() { + stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) } + } + + fun showAmount() { + stateController.update { it.copy(currentStep = StakingStep.Amount) } + } + + fun showValidators() { + stateController.update { it.copy(currentStep = StakingStep.Validators) } + } + + fun showConfirm() { + stateController.update { it.copy(currentStep = StakingStep.Confirm) } + } + + fun showSuccess() { + // stateController.update { it.copy(currentStep = StakingStep.Success) } + // TODO staking [REDACTED_TASK_KEY] + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt new file mode 100644 index 0000000000..65fe669de8 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -0,0 +1,108 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import kotlinx.collections.immutable.ImmutableList + +/** + * Ui states of the staking screen + */ +@Immutable +internal data class StakingUiState( + val clickIntents: StakingClickIntents, + val cryptoCurrencyName: String, + val currentStep: StakingStep, + val initialInfoState: StakingStates.InitialInfoState, + val amountState: AmountState, + val rewardsValidatorsState: StakingStates.RewardsValidatorsState, + val confirmStakingState: StakingStates.ConfirmStakingState, + val isBalanceHidden: Boolean, + val bottomSheetConfig: TangemBottomSheetConfig?, + val event: StateEvent, +) { + + fun copyWrapped( + initialInfoState: StakingStates.InitialInfoState = this.initialInfoState, + amountState: AmountState = this.amountState, + confirmStakingState: StakingStates.ConfirmStakingState = this.confirmStakingState, + ): StakingUiState = copy( + initialInfoState = initialInfoState, + amountState = amountState, + confirmStakingState = confirmStakingState, + ) +} + +internal sealed class StakingStates { + + abstract val isPrimaryButtonEnabled: Boolean + + /** Initial info state */ + sealed class InitialInfoState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + val available: String, + val onStake: String, + val aprRange: TextReference, + val unbondingPeriod: String, + val minimumRequirement: String, + val rewardClaiming: String, + val warmupPeriod: String, + val rewardSchedule: String, + val onInfoClick: (InfoType) -> Unit, + val yieldBalance: InnerYieldBalanceState, + ) : InitialInfoState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : InitialInfoState() + } + + /** Select validator to claim rewards state */ + sealed class RewardsValidatorsState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + val rewards: ImmutableList, + ) : RewardsValidatorsState() + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : RewardsValidatorsState() + } + + /** Confirm state */ + sealed class ConfirmStakingState : StakingStates() { + data class Data( + override val isPrimaryButtonEnabled: Boolean, + val feeState: FeeState, + val validatorState: ValidatorState, + val notifications: ImmutableList, + val footerText: String, + val innerState: InnerConfirmStakingState, + ) : ConfirmStakingState() { + + enum class InnerConfirmStakingState { + CONFIRM, + IN_PROGRESS, + SUCCESS, + } + } + + data class Empty( + override val isPrimaryButtonEnabled: Boolean = false, + ) : ConfirmStakingState() + } +} + +enum class StakingStep { + InitialInfo, + RewardsValidators, + Amount, + Confirm, + Validators, + Success, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt new file mode 100644 index 0000000000..d99ed81d8e --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.presentation.state + +import androidx.compose.runtime.Immutable +import com.tangem.domain.staking.model.Yield + +@Immutable +internal sealed class ValidatorState { + + data class Content( + val chosenValidator: Yield.Validator, + val availableValidators: List, + ) : ValidatorState() + + data object Loading : ValidatorState() + + data object Error : ValidatorState() +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt new file mode 100644 index 0000000000..f8cc06e495 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/bottomsheet/StakingInfoBottomSheetConfig.kt @@ -0,0 +1,9 @@ +package com.tangem.features.staking.impl.presentation.state.bottomsheet + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.TextReference + +data class StakingInfoBottomSheetConfig( + val title: TextReference, + val text: TextReference, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt new file mode 100644 index 0000000000..7ebb9058e7 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -0,0 +1,100 @@ +package com.tangem.features.staking.impl.presentation.state.converters + +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.BalanceItem +import com.tangem.domain.staking.model.BalanceType +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.utils.Provider +import com.tangem.utils.Strings.PLUS +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addOrReplace +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class RewardsValidatorStateConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val yield: Yield, +) : Converter { + override fun convert(value: Unit): StakingStates.RewardsValidatorsState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + return if (yieldBalance is YieldBalance.Data) { + val balances = yieldBalance.balance.items + StakingStates.RewardsValidatorsState.Data( + isPrimaryButtonEnabled = true, + rewards = balances + // todo remove when real data is available + .addOrReplace( + item = balances.first().copy( + type = BalanceType.REWARDS, + ), + predicate = { true }, + ) + .filter { it.type == BalanceType.REWARDS } + .mapRewardBalances(cryptoCurrencyStatus) + .toPersistentList(), + ) + } else { + StakingStates.RewardsValidatorsState.Empty() + } + } + + private fun List.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) = + this.mapNotNull { balance -> + val validator = yield.validators.firstOrNull { + it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true) + } + val cryptoValue = balance.amount.times(balance.pricePerShare) + val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue) + + validator?.toBalanceState( + cryptoCurrencyStatus = cryptoCurrencyStatus, + cryptoValue = cryptoValue, + fiatValue = fiatValue, + ) + } + + private fun Yield.Validator.toBalanceState( + cryptoCurrencyStatus: CryptoCurrencyStatus, + cryptoValue: BigDecimal, + fiatValue: BigDecimal?, + ): BalanceState { + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + + val cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoValue, + cryptoCurrency = cryptoCurrency, + ), + ) + val fiatAmount = combinedReference( + stringReference(PLUS), + stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ), + ) + + return BalanceState( + validator = this, + cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals), + cryptoAmount = cryptoAmount, + fiatAmount = fiatAmount, + rawCurrencyId = cryptoCurrency.id.rawCurrencyId, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt new file mode 100644 index 0000000000..28ede76079 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -0,0 +1,138 @@ +package com.tangem.features.staking.impl.presentation.state.converters + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.* +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.BalanceGroupType +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.isNullOrZero +import kotlinx.collections.immutable.toPersistentList + +internal class YieldBalancesConverter( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val yield: Yield, +) : Converter { + override fun convert(value: Unit): InnerYieldBalanceState { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + + val cryptoCurrency = cryptoCurrencyStatus.currency + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + + return if (yieldBalance is YieldBalance.Data) { + val cryptoRewardsValue = yieldBalance.getRewardStakingBalance() + val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue) + val groupedBalances = getGroupedBalance(yieldBalance.balance) + + InnerYieldBalanceState.Data( + rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoRewardsValue, + cryptoCurrency = cryptoCurrency, + ), + rewardsFiat = BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatRewardsValue, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(), + balance = groupedBalances, + ) + } else { + InnerYieldBalanceState.Empty + } + } + + private fun getGroupedBalance(balance: YieldBalanceItem) = balance.items + // todo remove when real data is available + .addOrReplace( + item = balance.items.first().copy( + type = BalanceType.REWARDS, + ), + predicate = { true }, + ) + .groupBy { it.type.toGroup() } + .mapNotNull { item -> + val (title, footer) = getGroupTitle(item.key) + title?.let { + BalanceGroupedState( + items = item.value.mapBalances().toPersistentList(), + footer = footer, + title = it, + ) + } + } + + private fun List.mapBalances(): List { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + return this.mapNotNull { balance -> + val validator = yield.validators.firstOrNull { + balance.validatorAddress?.contains(it.address, ignoreCase = true) == true + } + val cryptoAmount = balance.amount * balance.pricePerShare + val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount) + + validator?.let { + BalanceState( + validator = validator, + cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), + cryptoAmount = stringReference( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + fiatAmount = stringReference( + BigDecimalFormatter.formatFiatAmount( + fiatAmount = fiatAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ), + ), + rawCurrencyId = balance.rawCurrencyId, + ) + } + } + } + + private fun BalanceType.toGroup() = when (this) { + BalanceType.PREPARING, + BalanceType.STAKED, + BalanceType.REWARDS, + BalanceType.AVAILABLE, + BalanceType.LOCKED, + -> BalanceGroupType.ACTIVE + BalanceType.UNSTAKING, + BalanceType.UNLOCKING, + BalanceType.UNSTAKED, + -> BalanceGroupType.UNSTAKED + BalanceType.UNKNOWN, + -> BalanceGroupType.UNKNOWN + } + + private fun getGroupTitle(type: BalanceGroupType) = when (type) { + BalanceGroupType.ACTIVE -> resourceReference( + R.string.staking_active, + ) to resourceReference( + R.string.staking_active_footer, + ) + BalanceGroupType.UNSTAKED -> resourceReference( + R.string.staking_unstaked, + ) to resourceReference( + R.string.staking_unstaked_footer, + ) + BalanceGroupType.UNKNOWN -> null to null + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt new file mode 100644 index 0000000000..34916ee421 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmStakingStatePreviewData.kt @@ -0,0 +1,87 @@ +package com.tangem.features.staking.impl.presentation.state.previewdata + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType.Coin +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal + +internal object ConfirmStakingStatePreviewData { + + private val validatorList = listOf( + Yield.Validator( + address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951", + status = "active", + name = "Luganodes", + image = "https://assets.stakek.it/validators/luganodes.png", + apr = BigDecimal("0.054823398040640445"), + commission = 0.1, + stakedBalance = "355544384.45009977", + website = "https://luganodes.com/", + votingPower = 0.09778360195377911, + preferred = true, + ), + Yield.Validator( + address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32", + status = "active", + name = "InfStones", + image = "https://assets.stakek.it/validators/infstones.png", + apr = BigDecimal("0.057786472172836965"), + commission = 0.05, + stakedBalance = "12495684.05643019", + website = "https://infstones.com/", + votingPower = 0.0034366257754399774, + preferred = true, + ), + Yield.Validator( + address = "0xd14a87025109013b0a2354a775cb335f926af65a", + status = "active", + name = "Kiln", + image = "https://assets.stakek.it/validators/kiln.png", + apr = BigDecimal("0.057786472172836965"), + commission = 0.05, + stakedBalance = "85400369.96393165", + website = "https://infstones.com/", + votingPower = 0.023487238579718264, + preferred = true, + ), + ) + + private val fee = Fee.Common( + amount = Amount( + currencySymbol = "MATIC", + value = BigDecimal(0.159806), + decimals = 18, + type = Coin, + ), + ) + + val confirmStakingState = StakingStates.ConfirmStakingState.Data( + isPrimaryButtonEnabled = true, + feeState = FeeState.Content( + fee = fee, + rate = BigDecimal.ONE, + appCurrency = AppCurrency.Default, + isFeeApproximate = false, + isFeeConvertibleToFiat = true, + ), + validatorState = ValidatorState.Content( + chosenValidator = validatorList[0], + availableValidators = validatorList, + ), + footerText = "You stake \$715.11 and will be receiving ~\$35 monthly", + notifications = persistentListOf( + StakingNotification.Warning.EarnRewards( + currencyName = "Solana", + days = 2, + ), + ), + innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt new file mode 100644 index 0000000000..4eadd4916a --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -0,0 +1,59 @@ +package com.tangem.features.staking.impl.presentation.state.previewdata + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import kotlinx.collections.immutable.persistentListOf + +internal object InitialStakingStatePreview { + val defaultState = StakingStates.InitialInfoState.Data( + isPrimaryButtonEnabled = true, + available = "15 SOL", + onStake = "0 SOL", + aprRange = stringReference("2.54-5.12%"), + unbondingPeriod = "3d", + minimumRequirement = "12 SOL", + rewardClaiming = "Auto", + warmupPeriod = "Days", + rewardSchedule = "Block", + onInfoClick = {}, + yieldBalance = InnerYieldBalanceState.Empty, + ) + + val stateWithYield = defaultState.copy( + yieldBalance = InnerYieldBalanceState.Data( + rewardsFiat = "100 $", + rewardsCrypto = "100 SOL", + isRewardsToClaim = false, + balance = listOf( + BalanceGroupedState( + title = stringReference("Staked"), + footer = null, + items = persistentListOf( + BalanceState( + cryptoValue = "100", + cryptoAmount = stringReference("100 SOL"), + fiatAmount = stringReference("100 $"), + rawCurrencyId = null, + validator = Yield.Validator( + address = "address", + status = "status", + name = "Binance", + image = null, + website = null, + apr = "5".toBigDecimal(), + commission = null, + stakedBalance = null, + votingPower = null, + preferred = false, + ), + ), + ), + ), + ), + ), + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt new file mode 100644 index 0000000000..6320dce75c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -0,0 +1,34 @@ +package com.tangem.features.staking.impl.presentation.state.stub + +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents + +object StakingClickIntentsStub : StakingClickIntents { + + override fun onBackClick() {} + + override fun onNextClick() {} + + override fun onPrevClick() {} + + override fun onInfoClick(infoType: InfoType) {} + + override fun onAmountValueChange(value: String) {} + + override fun onAmountPasteTriggerDismiss() {} + + override fun onMaxValueClick() {} + + override fun onCurrencyChangeClick(isFiat: Boolean) {} + + override fun onAmountNext() {} + + override fun openValidators() {} + + override fun onValidatorSelect(validator: Yield.Validator) {} + + override fun openRewardsValidators() {} + + override fun selectRewardValidator(rewardValue: String) {} +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt new file mode 100644 index 0000000000..251b237e9d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissBottomSheetStateTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class DismissBottomSheetStateTransformer : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy(bottomSheetConfig = null) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt new file mode 100644 index 0000000000..84128a62a1 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/HideBalanceStateTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class HideBalanceStateTransformer( + private val isBalanceHidden: Boolean, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy(isBalanceHidden = isBalanceHidden) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt new file mode 100644 index 0000000000..f058a772c3 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateConfirmTransformer.kt @@ -0,0 +1,52 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.transaction.StakingGasEstimate +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.Provider +import com.tangem.utils.transformer.Transformer +import com.tangem.blockchain.common.Amount + +@Suppress("UnusedPrivateMember") +internal class SetConfirmStateConfirmTransformer( + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, + private val stakingGasEstimate: StakingGasEstimate, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + confirmStakingState = prevState.confirmStakingState.copyWrapped(stakingGasEstimate), + ) + } + + private fun StakingStates.ConfirmStakingState.copyWrapped( + gasEstimate: StakingGasEstimate, + ): StakingStates.ConfirmStakingState { + if (this is StakingStates.ConfirmStakingState.Data) { + return copy( + feeState = FeeState.Content( + fee = Fee.Common( + Amount( + currencySymbol = gasEstimate.token.symbol, + value = gasEstimate.amount, + decimals = gasEstimate.token.decimals, + ), + ), + rate = cryptoCurrencyStatusProvider().value.fiatRate, + isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate, + appCurrency = appCurrencyProvider(), + isFeeApproximate = false, + ), + isPrimaryButtonEnabled = true, + innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, + ) + } else { + return this + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt new file mode 100644 index 0000000000..edad01bfea --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateInProgressTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class SetConfirmStateInProgressTransformer : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + confirmStakingState = prevState.confirmStakingState.copyWrapped(), + ) + } + + private fun StakingStates.ConfirmStakingState.copyWrapped(): StakingStates.ConfirmStakingState { + return if (this is StakingStates.ConfirmStakingState.Data) { + copy( + isPrimaryButtonEnabled = false, + innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.IN_PROGRESS, + ) + } else { + this + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt new file mode 100644 index 0000000000..838a5db5cd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmStateLoadingTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +@Suppress("UnusedPrivateMember") +internal class SetConfirmStateLoadingTransformer( + private val yield: Yield, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + val possibleConfirmStakingState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data + val possibleValidatorState = possibleConfirmStakingState?.validatorState as? ValidatorState.Content + val chosenValidator = possibleValidatorState?.chosenValidator ?: yield.validators[0] + + return prevState.copy( + confirmStakingState = StakingStates.ConfirmStakingState.Data( + isPrimaryButtonEnabled = false, + feeState = FeeState.Loading, + validatorState = ValidatorState.Content( + chosenValidator = chosenValidator, + availableValidators = yield.validators, + ), + notifications = persistentListOf( + StakingNotification.Warning.EarnRewards( + currencyName = yield.token.name, + days = yield.metadata.cooldownPeriod.days, + ), + ), + footerText = "", + innerState = StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM, + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt new file mode 100644 index 0000000000..e66d509f0d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -0,0 +1,134 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.amountScreen.converters.AmountStateConverter +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.YieldBalance +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter +import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.utils.Provider +import com.tangem.utils.extensions.orZero +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +internal class SetInitialDataStateTransformer( + private val clickIntents: StakingClickIntents, + private val yield: Yield, + private val cryptoCurrencyStatusProvider: Provider, + private val userWalletProvider: Provider, + private val appCurrencyProvider: Provider, +) : Transformer { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { + AmountStateConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + userWalletProvider = userWalletProvider, + iconStateConverter = iconStateConverter, + ) + } + + private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) { + RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + } + + private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) { + YieldBalancesConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) + } + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + clickIntents = clickIntents, + currentStep = StakingStep.InitialInfo, + initialInfoState = createInitialInfoState(), + amountState = createInitialAmountState(), + confirmStakingState = createInitialConfirmationState(), + rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit), + bottomSheetConfig = null, + ) + } + + private fun createInitialInfoState(): StakingStates.InitialInfoState.Data { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val yieldBalance = cryptoCurrencyStatus.value.yieldBalance + + return StakingStates.InitialInfoState.Data( + isPrimaryButtonEnabled = true, + available = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = cryptoCurrencyStatus.value.amount, + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ), + onStake = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(), + cryptoCurrency = cryptoCurrencyStatus.currency.symbol, + decimals = cryptoCurrencyStatus.currency.decimals, + ), + aprRange = getAprRange(), + unbondingPeriod = yield.metadata.cooldownPeriod.days.toString(), + minimumRequirement = yield.metadata.minimumStake.toString(), + rewardClaiming = yield.metadata.rewardClaiming, + warmupPeriod = yield.metadata.warmupPeriod.days.toString(), + rewardSchedule = yield.metadata.rewardSchedule, + onInfoClick = clickIntents::onInfoClick, + yieldBalance = yieldBalancesConverter.convert(Unit), + ) + } + + private fun createInitialAmountState(): AmountState { + return amountStateConverter.convert("") + } + + private fun createInitialConfirmationState(): StakingStates.ConfirmStakingState { + return ConfirmStakingStatePreviewData.confirmStakingState.copy( + validatorState = ValidatorState.Content( + chosenValidator = yield.validators.first(), + availableValidators = yield.validators, + ), + ) + } + + private fun getAprRange(): TextReference { + val aprValues = yield.validators.mapNotNull { it.apr } + + val minApr = aprValues.min() + val maxApr = aprValues.max() + + val formattedMinApr = BigDecimalFormatter.formatPercent( + percent = minApr, + useAbsoluteValue = true, + ) + val formattedMaxApr = BigDecimalFormatter.formatPercent( + percent = maxApr, + useAbsoluteValue = true, + ) + + if (maxApr - minApr < EQUALITY_THRESHOLD) { + return stringReference("$formattedMinApr%") + } + return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr)) + } + + companion object { + private val EQUALITY_THRESHOLD = BigDecimal(1E-10) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt new file mode 100644 index 0000000000..ff1701bc9b --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ShowInfoBottomSheetStateTransformer.kt @@ -0,0 +1,53 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig +import com.tangem.utils.transformer.Transformer + +internal class ShowInfoBottomSheetStateTransformer( + private val infoType: InfoType, + private val onDismiss: () -> Unit, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + onDismissRequest = onDismiss, + isShow = true, + content = when (infoType) { + InfoType.APY -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_apy), + text = resourceReference(R.string.staking_details_apy_info), + ) + InfoType.UNBOUNDING_PERIOD -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_unbonding_period), + text = resourceReference(R.string.staking_details_unbonding_period_info), + ) + InfoType.REWARD_CLAIMING -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_reward_claiming), + text = resourceReference(R.string.staking_details_reward_claiming_info), + ) + InfoType.WARMUP_PERIOD -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_warmup_period), + text = resourceReference(R.string.staking_details_warmup_period_info), + ) + InfoType.REWARD_SCHEDULE -> StakingInfoBottomSheetConfig( + title = resourceReference(R.string.staking_details_reward_schedule), + text = resourceReference(R.string.staking_details_reward_schedule_info), + ) + }, + ), + ) + } +} + +enum class InfoType { + APY, + UNBOUNDING_PERIOD, + REWARD_CLAIMING, + WARMUP_PERIOD, + REWARD_SCHEDULE, +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt new file mode 100644 index 0000000000..110de484fc --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountChangeStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: String, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountFieldChangeTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt new file mode 100644 index 0000000000..0410bf8745 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountCurrencyChangeStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: Boolean, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt new file mode 100644 index 0000000000..3d7dc9125d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -0,0 +1,16 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountMaxValueStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt new file mode 100644 index 0000000000..b2d7752952 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountPasteDismissStateTransformer.kt @@ -0,0 +1,14 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountPasteDismissStateTransformer : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountPastedTriggerDismissTransformer().transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt new file mode 100644 index 0000000000..b72f227693 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -0,0 +1,24 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.validator + +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.utils.transformer.Transformer + +internal class ValidatorSelectChangeTransformer( + private val selectedValidator: Yield.Validator, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data ?: return prevState + val validatorState = confirmState.validatorState as? ValidatorState.Content ?: return prevState + + return prevState.copy( + confirmStakingState = confirmState.copy( + validatorState = validatorState.copy( + chosenValidator = selectedValidator, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt new file mode 100644 index 0000000000..64a2f51b56 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -0,0 +1,73 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.tangem.core.ui.components.inputrow.InputRowImageInfo +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.utils.extensions.orZero + +@Composable +internal fun StakingClaimRewardsValidatorContent( + state: StakingStates.RewardsValidatorsState, + clickIntents: StakingClickIntents, + modifier: Modifier = Modifier, +) { + if (state !is StakingStates.RewardsValidatorsState.Data) return + Column( + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = TangemTheme.dimens.spacing12) + .verticalScroll(rememberScrollState()), + ) { + state.rewards.forEachIndexed { index, item -> + key(item.validator.address) { + InputRowImageInfo( + subtitle = stringReference(item.validator.name), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + appendSpace() + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + append( + BigDecimalFormatter.formatPercent( + percent = item.validator.apr.orZero(), + useAbsoluteValue = true, + ), + ) + } + }, + ), + ), + infoTitle = item.fiatAmount, + infoSubtitle = item.cryptoAmount, + imageUrl = item.validator.image.orEmpty(), + modifier = modifier + .roundedShapeItemDecoration(index, state.rewards.lastIndex, false) + .background(TangemTheme.colors.background.action) + .clickable( + onClick = { + clickIntents.selectRewardValidator(item.cryptoValue) + }, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt new file mode 100644 index 0000000000..be8589fa2f --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmContent.kt @@ -0,0 +1,81 @@ +package com.tangem.features.staking.impl.presentation.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.core.ui.components.SpacerHMax +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub +import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock +import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock +import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents + +@Composable +internal fun StakingConfirmContent( + amountState: AmountState, + state: StakingStates.ConfirmStakingState, + clickIntents: StakingClickIntents, +) { + if (state !is StakingStates.ConfirmStakingState.Data) return + + Column( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.tertiary) + .padding(TangemTheme.dimens.spacing16) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + AmountBlock( + amountState = amountState, + isClickDisabled = true, + isEditingDisabled = true, + onClick = {}, + ) + ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators) + StakingFeeBlock(feeState = state.feeState) + NotificationsBlock(notifications = state.notifications) + SpacerHMax() + FooterText(text = state.footerText) + } +} + +@Composable +private fun FooterText(text: String) { + Text( + modifier = Modifier.fillMaxWidth(), + text = text, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_StakingConfirmContent() { + TangemThemePreview { + Column(Modifier.background(TangemTheme.colors.background.primary)) { + StakingConfirmContent( + amountState = AmountStatePreviewData.amountState, + state = ConfirmStakingStatePreviewData.confirmStakingState, + clickIntents = StakingClickIntentsStub, + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt new file mode 100644 index 0000000000..5ca7d71e8e --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -0,0 +1,315 @@ +package com.tangem.features.staking.impl.presentation.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.components.inputrow.InputRowDefault +import com.tangem.core.ui.components.inputrow.InputRowImageInfo +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.components.rows.RoundableCornersRow +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub +import com.tangem.features.staking.impl.presentation.state.transformers.InfoType +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.utils.Strings.DOT +import com.tangem.utils.extensions.orZero + +@Composable +internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, clickIntents: StakingClickIntents) { + if (state !is StakingStates.InitialInfoState.Data) return + + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = Modifier // Do not put fillMaxSize() in here + .background(TangemTheme.colors.background.tertiary) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ) + .verticalScroll(rememberScrollState()), + ) { + AnimatedVisibility(state.yieldBalance == InnerYieldBalanceState.Empty) { + MetricsBlock(state) + } + StakingDetailsRows(state) + AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") { + if (it is InnerYieldBalanceState.Data) { + StakingRewardBlock( + rewardCrypto = it.rewardsCrypto, + rewardFiat = it.rewardsFiat, + isRewardsToClaim = it.isRewardsToClaim, + onRewardsClick = clickIntents::openRewardsValidators, + ) + } + } + AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") { + if (it is InnerYieldBalanceState.Data) { + ActiveStakingBlock(it.balance) + } + } + } +} + +@Composable +private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) { + Column( + modifier = Modifier + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(TangemTheme.dimens.radius12), + ) + .padding(TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) { + Text( + text = stringResource(id = R.string.staking_details_metrics_block_header), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + Spacer(modifier = Modifier.height(TangemTheme.dimens.size8)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.weight(1F)) { + Text( + text = stringResource(id = R.string.staking_details_apr), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = state.aprRange.resolveReference(), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.accent, + ) + } + Column(modifier = Modifier.weight(1F)) { + Row { + Text( + modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), + text = stringResource(id = R.string.staking_details_market_rating), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .align(Alignment.CenterVertically), + painter = painterResource(id = R.drawable.ic_alert_24), + contentDescription = null, + tint = TangemTheme.colors.text.tertiary, + ) + } + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + text = "1", // TODO staking + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.accent, + ) + } + } + } +} + +@Composable +internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) { + Column { + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_available), + endText = state.available, + cornersToRound = CornersToRound.TOP_2, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_on_stake), + endText = state.onStake, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_apy), + endText = state.aprRange.resolveReference(), + cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.APY) }, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_unbonding_period), + endText = state.unbondingPeriod, + cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.UNBOUNDING_PERIOD) }, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_minimum_requirement), + endText = state.minimumRequirement, + cornersToRound = CornersToRound.ZERO, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_reward_claiming), + endText = state.rewardClaiming, + cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.REWARD_CLAIMING) }, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_warmup_period), + endText = state.warmupPeriod, + cornersToRound = CornersToRound.ZERO, + iconClick = { state.onInfoClick(InfoType.WARMUP_PERIOD) }, + ) + InitialInfoContentRow( + startText = stringResource(id = R.string.staking_details_reward_schedule), + endText = state.rewardSchedule, + cornersToRound = CornersToRound.BOTTOM_2, + iconClick = { state.onInfoClick(InfoType.REWARD_SCHEDULE) }, + ) + } +} + +@Composable +private fun StakingRewardBlock( + rewardCrypto: String, + rewardFiat: String, + isRewardsToClaim: Boolean, + onRewardsClick: () -> Unit, +) { + val (text, textColor) = if (isRewardsToClaim) { + annotatedReference( + buildAnnotatedString { + append("+") + appendSpace() + append(rewardFiat) + appendSpace() + append(DOT) + appendSpace() + append(rewardCrypto) + }, + ) to TangemTheme.colors.text.primary1 + } else { + resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary + } + InputRowDefault( + title = resourceReference(R.string.staking_rewards), + text = text, + iconRes = R.drawable.ic_chevron_right_24, + textColor = textColor, + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = onRewardsClick, + ), + ) +} + +@Composable +private fun ActiveStakingBlock(groups: List) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + groups.forEach { group -> + key(group.title) { + FooterContainer( + footer = group.footer?.resolveReference(), + modifier = Modifier, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) { + group.items.forEachIndexed { index, balance -> + key(balance.validator.address) { + InputRowImageInfo( + title = group.title.takeIf { index == 0 }, + subtitle = stringReference(balance.validator.name), + caption = stringReference( + BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true), + ), + infoTitle = balance.fiatAmount, + infoSubtitle = balance.cryptoAmount, + imageUrl = balance.validator.image.orEmpty(), + ) + } + } + } + } + } + } + } +} + +@Composable +private fun InitialInfoContentRow( + startText: String, + endText: String, + cornersToRound: CornersToRound, + iconClick: (() -> Unit)? = null, +) { + RoundableCornersRow( + startText = startText, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.body2, + endText = endText, + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = cornersToRound, + iconResId = R.drawable.ic_information_24, + iconClick = iconClick, + ) +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StakingInitialInfoContent_Preview( + @PreviewParameter(StakingInitialInfoContentPreviewProvider::class) feeState: StakingStates.InitialInfoState.Data, +) { + TangemThemePreview { + StakingInitialInfoContent( + state = feeState, + clickIntents = StakingClickIntentsStub, + ) + } +} + +private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + InitialStakingStatePreview.defaultState, + InitialStakingStatePreview.stateWithYield, + ) +} +// endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt new file mode 100644 index 0000000000..1ee7aca2ac --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt @@ -0,0 +1,146 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +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.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW12 +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState + +@Composable +internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + StakingNavigationButton( + uiState = uiState, + modifier = Modifier, + ) + } +} + +@Composable +private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier = Modifier) { + val hapticFeedback = LocalHapticFeedback.current + + val isButtonsVisible = isPrevButtonVisible(uiState.currentStep) + val isStakingState = uiState.currentStep == StakingStep.Confirm + + val (buttonTextId, buttonClick) = getButtonData( + currentState = uiState, + ) + val isButtonEnabled = isButtonEnabled(uiState) + val buttonIcon = if (isStakingState) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + } + + Row(modifier = modifier) { + AnimatedVisibility( + visible = isButtonsVisible, + enter = expandHorizontally(expandFrom = Alignment.End), + exit = shrinkHorizontally(shrinkTowards = Alignment.End), + ) { + Row { + Icon( + painter = painterResource(R.drawable.ic_back_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) + .background(TangemTheme.colors.button.secondary) + .clickable { uiState.clickIntents.onPrevClick() } + .padding(TangemTheme.dimens.spacing12), + ) + SpacerW12() + } + } + + AnimatedVisibility( + visible = buttonClick != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + TangemButton( + text = stringResource(buttonTextId), + icon = buttonIcon, + enabled = isButtonEnabled && buttonClick != null, + onClick = { + if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + if (buttonClick != null) buttonClick() + }, + showProgress = false, + modifier = Modifier.fillMaxWidth(), + colors = TangemButtonsDefaults.primaryButtonColors, + ) + } + } +} + +private fun getButtonData(currentState: StakingUiState): Pair Unit)?> { + return when (currentState.currentStep) { + StakingStep.InitialInfo -> { + val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data + if (initialState?.yieldBalance is InnerYieldBalanceState.Data) { + R.string.staking_stake_more to currentState.clickIntents::onNextClick + } else { + R.string.common_next to currentState.clickIntents::onNextClick + } + } + StakingStep.Amount, + -> R.string.common_next to currentState.clickIntents::onNextClick + StakingStep.Confirm -> R.string.common_stake to currentState.clickIntents::onNextClick + StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick + StakingStep.Success -> R.string.common_close to currentState.clickIntents::onBackClick + else -> R.string.common_next to null + } +} + +private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) { + StakingStep.InitialInfo, + StakingStep.RewardsValidators, + StakingStep.Confirm, + StakingStep.Success, + -> false + StakingStep.Amount, + StakingStep.Validators, + -> true +} + +private fun isButtonEnabled(uiState: StakingUiState): Boolean { + return when (uiState.currentStep) { + StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled + StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled + StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled + StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled + StakingStep.Success -> true + StakingStep.Validators -> true + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt new file mode 100644 index 0000000000..1b92f7fcea --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -0,0 +1,173 @@ +package com.tangem.features.staking.impl.presentation.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.core.tween +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.amountScreen.AmountScreenContent +import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig +import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.withIndex + +@Composable +internal fun StakingScreen(uiState: StakingUiState) { + BackHandler(onBack = uiState.clickIntents::onBackClick) + Column( + modifier = Modifier + .background(color = TangemTheme.colors.background.tertiary) + .fillMaxSize() + .imePadding() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SendAppBar( + uiState = uiState, + ) + StakingScreenContent( + uiState = uiState, + modifier = Modifier.weight(1f), + ) + StakingNavigationButtons( + uiState = uiState, + ) + StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) + } +} + +@Composable +fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { + if (bottomSheetConfig == null) return + when (bottomSheetConfig.content) { + is StakingInfoBottomSheetConfig -> StakingInfoBottomSheet(bottomSheetConfig) + } +} + +@Composable +private fun SendAppBar(uiState: StakingUiState) { + val titleRes = when (uiState.currentStep) { + StakingStep.Amount -> stringResource(id = R.string.send_amount_label) + StakingStep.InitialInfo, + StakingStep.RewardsValidators, + StakingStep.Validators, + StakingStep.Confirm, + -> stringResource(id = R.string.common_stake) + StakingStep.Success -> "" + } + val backIcon = when (uiState.currentStep) { + StakingStep.Amount, + StakingStep.Validators, + StakingStep.Confirm, + StakingStep.Success, + -> { + R.drawable.ic_close_24 + } + StakingStep.RewardsValidators, + StakingStep.InitialInfo, + -> { + R.drawable.ic_back_24 + } + } + AppBarWithBackButtonAndIcon( + text = titleRes, + backIconRes = backIcon, + onBackClick = uiState.clickIntents::onBackClick, + backgroundColor = TangemTheme.colors.background.tertiary, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { + val currentScreen = uiState.currentStep + var currentStateProxy by remember { mutableStateOf(currentScreen) } + var isTransitionAnimationRunning by remember { mutableStateOf(false) } + + // Prevent quick screen changes to avoid some of the transition animation distortions + LaunchedEffect(currentScreen) { + snapshotFlow { isTransitionAnimationRunning } + .withIndex() + .map { (index, running) -> + if (running && index != 0) { + delay(timeMillis = 200) + } + running + } + .first { !it } + + currentStateProxy = currentScreen + } + // Restrict pressing the back button while screen transition is running to avoid most of the animation distortions + BackHandler(enabled = isTransitionAnimationRunning) {} + + // Box is needed to fix animation with resizing of AnimatedContent + Box(modifier = modifier.fillMaxSize()) { + AnimatedContent( + targetState = currentStateProxy, + contentAlignment = Alignment.TopCenter, + label = "Staking Screen Navigation", + transitionSpec = { + val direction = if (initialState.ordinal < targetState.ordinal) { + AnimatedContentTransitionScope.SlideDirection.Start + } else { + AnimatedContentTransitionScope.SlideDirection.End + } + + slideIntoContainer(towards = direction, animationSpec = tween()) + .togetherWith(slideOutOfContainer(towards = direction, animationSpec = tween())) + }, + ) { state -> + isTransitionAnimationRunning = transition.targetState != transition.currentState + + when (state) { + StakingStep.InitialInfo -> StakingInitialInfoContent( + state = uiState.initialInfoState, + clickIntents = uiState.clickIntents, + ) + StakingStep.RewardsValidators -> { + StakingClaimRewardsValidatorContent( + state = uiState.rewardsValidatorsState, + clickIntents = uiState.clickIntents, + ) + } + StakingStep.Amount -> AmountScreenContent( + amountState = uiState.amountState, + isBalanceHiding = uiState.isBalanceHidden, + clickIntents = uiState.clickIntents, + ) + StakingStep.Confirm -> StakingConfirmContent( + amountState = uiState.amountState, + state = uiState.confirmStakingState, + clickIntents = uiState.clickIntents, + ) + StakingStep.Validators -> { + val confirmState = uiState.confirmStakingState + if (confirmState !is StakingStates.ConfirmStakingState.Data) return@AnimatedContent + StakingValidatorListContent( + state = confirmState.validatorState, + clickIntents = uiState.clickIntents, + ) + } + else -> TODO() + } + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt new file mode 100644 index 0000000000..1d6e05103d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -0,0 +1,129 @@ +package com.tangem.features.staking.impl.presentation.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.inputrow.InputRowImageSelector +import com.tangem.core.ui.components.rows.CornersToRound +import com.tangem.core.ui.extensions.annotatedReference +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.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import java.math.BigDecimal + +/** + * Staking screen with validators + */ +@Composable +internal fun StakingValidatorListContent( + state: ValidatorState, + clickIntents: StakingClickIntents, + modifier: Modifier = Modifier, +) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + LazyColumn( + contentPadding = PaddingValues(bottom = bottomBarHeight), + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + item(key = "HEADER") { + Text( + text = stringResource(R.string.staking_validator), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier + .fillMaxWidth() + .clip(CornersToRound.TOP_2.getShape()) + .background(TangemTheme.colors.background.action) + .padding( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing8, + ), + ) + } + if (state is ValidatorState.Content) { + val validators = state.availableValidators + items( + count = validators.size, + key = { validators[it].address }, + contentType = { validators[it]::class.java }, + ) { index -> + val item = validators[index] + + InputRowImageSelector( + subtitle = stringReference(item.name), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) { + append( + BigDecimalFormatter.formatPercent(item.apr ?: BigDecimal.ZERO, true), + ) + } + }, + ), + ), + imageUrl = item.image.orEmpty(), + isSelected = item == state.chosenValidator, + onSelect = { clickIntents.onValidatorSelect(item) }, + modifier = Modifier + .clip( + if (index == validators.lastIndex) { + CornersToRound.BOTTOM_2 + } else { + CornersToRound.ZERO + }.getShape(), + ) + .background(TangemTheme.colors.background.action), + ) + } + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StakingValidatorListContent_Preview( + @PreviewParameter(StakingValidatorListContentPreviewProvider::class) + data: ValidatorState, +) { + TangemThemePreview { + StakingValidatorListContent( + state = data, + clickIntents = StakingClickIntentsStub, + ) + } +} + +private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(ConfirmStakingStatePreviewData.confirmStakingState.validatorState) +} +// endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt new file mode 100644 index 0000000000..68d638aaaa --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -0,0 +1,13 @@ +package com.tangem.features.staking.impl.presentation.ui.block + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.presentation.state.StakingNotification + +@Composable +internal fun NotificationsBlock(notifications: List) { + notifications.forEach { + Notification(config = it.config, iconTint = TangemTheme.colors.icon.accent) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt new file mode 100644 index 0000000000..4dff5385fb --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -0,0 +1,153 @@ +package com.tangem.features.staking.impl.presentation.ui.block + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.rows.SelectorRowItem +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.utils.getCryptoReference +import com.tangem.common.ui.amountScreen.utils.getFiatReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.features.staking.impl.presentation.state.FeeState +import java.math.BigDecimal + +@Composable +internal fun StakingFeeBlock(feeState: FeeState) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = stringResource(R.string.common_network_fee_title), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + + Box( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) { + when (feeState) { + is FeeState.Content -> { + val feeAmount = feeState.fee?.amount + val (title, icon) = R.string.common_fee_selector_option_market to R.drawable.ic_bird_24 + SelectorRowItem( + titleRes = title, + iconRes = icon, + preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate), + postDot = if (feeState.isFeeConvertibleToFiat) { + getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) + } else { + null + }, + ellipsizeOffset = feeAmount?.currencySymbol?.length, + isSelected = true, + showDivider = false, + showSelectedAppearance = false, + paddingValues = PaddingValues(), + ) + } + is FeeState.Loading -> { + FeeLoading(feeState) + } + is FeeState.Error -> { + FeeError(feeState) + } + } + } + } +} + +@Composable +private fun BoxScope.FeeLoading(feeState: FeeState) { + AnimatedContent( + targetState = feeState, + label = "Fee Loading State Change", + modifier = Modifier.align(Alignment.CenterEnd), + ) { + if (it == FeeState.Loading) { + RectangleShimmer( + radius = TangemTheme.dimens.radius3, + modifier = Modifier.size( + height = TangemTheme.dimens.size12, + width = TangemTheme.dimens.size90, + ), + ) + } + } +} + +@Composable +private fun BoxScope.FeeError(feeState: FeeState) { + AnimatedContent( + targetState = feeState, + label = "Fee Error State Change", + modifier = Modifier.align(Alignment.CenterEnd), + ) { + if (it == FeeState.Error) { + Text( + text = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2, + ) + } + } +} + +// region Preview +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: FeeState.Content) { + TangemThemePreview { + StakingFeeBlock( + feeState = value, + ) + } +} + +private class FeeBlockPreviewProvider : PreviewParameterProvider { + + override val values: Sequence + get() = sequenceOf( + feeState, + ) + + private val fee = Fee.Common( + amount = Amount( + currencySymbol = "MATIC", + value = BigDecimal(0.159806), + decimals = 18, + type = AmountType.Coin, + ), + ) + + private val feeState = FeeState.Content( + fee = fee, + rate = BigDecimal.ONE, + appCurrency = AppCurrency.Default, + isFeeApproximate = false, + isFeeConvertibleToFiat = true, + ) +} + +// endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt new file mode 100644 index 0000000000..4cee3bcba5 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -0,0 +1,69 @@ +package com.tangem.features.staking.impl.presentation.ui.block + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.tangem.core.ui.components.inputrow.InputRowImageChevron +import com.tangem.core.ui.extensions.annotatedReference +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.res.TangemTheme +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.ValidatorState +import java.math.BigDecimal + +@Composable +internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = onClick, + ) + .padding(TangemTheme.dimens.spacing12), + ) { + Text( + text = stringResource(R.string.staking_validator), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6), + ) + if (validatorState is ValidatorState.Content) { + InputRowImageChevron( + subtitle = stringReference(validatorState.chosenValidator.name), + caption = combinedReference( + resourceReference(R.string.staking_details_apr), + annotatedReference( + buildAnnotatedString { + append(" ") + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + val apr = validatorState.chosenValidator.apr ?: BigDecimal.ZERO + append(BigDecimalFormatter.formatPercent(apr, true)) + } + }, + ), + ), + imageUrl = validatorState.chosenValidator.image.orEmpty(), + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt new file mode 100644 index 0000000000..0b1104c35b --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/bottomsheet/StakingInfoBottomSheet.kt @@ -0,0 +1,38 @@ +package com.tangem.features.staking.impl.presentation.ui.bottomsheet + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.appbar.AppBar +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig + +@Composable +fun StakingInfoBottomSheet(config: TangemBottomSheetConfig) { + val scrollState = rememberScrollState() + TangemBottomSheet( + config = config, + ) { content: StakingInfoBottomSheetConfig -> + Column( + modifier = Modifier.verticalScroll(scrollState), + ) { + AppBar(text = content.title) + Text( + text = content.text.resolveReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + modifier = Modifier.padding( + horizontal = TangemTheme.dimens.spacing28, + vertical = TangemTheme.dimens.spacing16, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt new file mode 100644 index 0000000000..62f5fc6c5c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -0,0 +1,26 @@ +package com.tangem.features.staking.impl.presentation.viewmodel + +import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.domain.staking.model.Yield +import com.tangem.features.staking.impl.presentation.state.transformers.InfoType + +internal interface StakingClickIntents : AmountScreenClickIntents { + + fun onBackClick() + + fun onNextClick() + + fun onPrevClick() + + fun onInfoClick(infoType: InfoType) + + override fun onAmountNext() = onNextClick() + + fun openValidators() + + fun onValidatorSelect(validator: Yield.Validator) + + fun openRewardsValidators() + + fun selectRewardValidator(rewardValue: String) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt new file mode 100644 index 0000000000..7b76ca2694 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -0,0 +1,293 @@ +package com.tangem.features.staking.impl.presentation.viewmodel + +import android.os.Bundle +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse +import com.tangem.blockchain.common.TransactionData +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.staking.InitializeStakingProcessUseCase +import com.tangem.domain.staking.model.Yield +import com.tangem.domain.staking.model.transaction.StakingTransaction +import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.staking.impl.navigation.InnerStakingRouter +import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.StakingStateController +import com.tangem.features.staking.impl.presentation.state.StakingStateRouter +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.transformers.* +import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer +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 timber.log.Timber +import javax.inject.Inject +import kotlin.properties.Delegates + +@Suppress("LongParameterList") +@HiltViewModel +internal class StakingViewModel @Inject constructor( + private val stateController: StakingStateController, + private val dispatchers: CoroutineDispatcherProvider, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val initializeStakingProcessUseCase: InitializeStakingProcessUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { + + val uiState: StateFlow = stateController.uiState + val value: StakingUiState get() = uiState.value + + var stakingStateRouter: StakingStateRouter by Delegates.notNull() + private set + + private var stakingTransaction: StakingTransaction? = null + + private val cryptoCurrencyId: CryptoCurrency.ID = + savedStateHandle.get(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY) + ?.unbundle(CryptoCurrency.ID.serializer()) + ?: error("This screen can't be opened without `CryptoCurrency.ID`") + + private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.Staking.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) + ?: error("This screen can't be opened without `UserWalletId`") + + private val yield: Yield = savedStateHandle.get(AppRoute.Staking.YIELD_KEY) + ?.unbundle(Yield.serializer()) + ?: error("This screen can't be opened without `Yield`") + + private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + + private var innerRouter: InnerStakingRouter by Delegates.notNull() + private var userWallet: UserWallet by Delegates.notNull() + private var appCurrency: AppCurrency by Delegates.notNull() + + init { + subscribeOnSelectedAppCurrency() + subscribeOnBalanceHiding() + subscribeOnCurrencyStatusUpdates() + } + + override fun onBackClick() { + stakingStateRouter.onBackClick() + } + + override fun onNextClick() { + handleOnNextConfirmClick() + stakingStateRouter.onNextClick() + if (value.currentStep == StakingStep.Confirm) { + initStaking() + } + } + + private fun handleOnNextConfirmClick() { + if (value.currentStep == StakingStep.Confirm && + (value.confirmStakingState as? StakingStates.ConfirmStakingState.Data)?.innerState == + StakingStates.ConfirmStakingState.Data.InnerConfirmStakingState.CONFIRM + ) { + viewModelScope.launch { + stakingTransaction?.unsignedTransaction?.let { + sendStakingTransaction(TransactionData.Compiled(value = it.hexToBytes())) + } ?: error("No unsigned transaction available") + } + } + } + + private fun initStaking() { + viewModelScope.launch { + stateController.update( + SetConfirmStateLoadingTransformer( + yield = yield, + ), + ) + + val actionWithTransaction = initializeStakingProcessUseCase( + integrationId = yield.id, + amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided"), + address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("No available address"), + validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"), + token = yield.token, + ) + + val (enterAction, stakingTransaction) = actionWithTransaction.getOrElse { + error(it) + } + + this@StakingViewModel.stakingTransaction = stakingTransaction + + val stakingGasEstimate = stakingTransaction.gasEstimate ?: error("Can't get fee info") + + stateController.update( + SetConfirmStateConfirmTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + stakingGasEstimate = stakingGasEstimate, + ), + ) + } + } + + override fun onPrevClick() { + stakingStateRouter.onPrevClick() + } + + override fun onInfoClick(infoType: InfoType) { + stateController.update( + ShowInfoBottomSheetStateTransformer(infoType) { + stateController.update(DismissBottomSheetStateTransformer()) + }, + ) + } + + override fun onAmountValueChange(value: String) { + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value)) + } + + override fun onAmountPasteTriggerDismiss() { + stateController.update(AmountPasteDismissStateTransformer()) + } + + override fun onMaxValueClick() { + stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus)) + } + + override fun onCurrencyChangeClick(isFiat: Boolean) { + stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat)) + } + + override fun openValidators() = stakingStateRouter.showValidators() + + override fun onValidatorSelect(validator: Yield.Validator) { + stateController.update(ValidatorSelectChangeTransformer(validator)) + } + + override fun openRewardsValidators() { + stakingStateRouter.showRewardsValidators() + } + + override fun selectRewardValidator(rewardValue: String) { + stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, rewardValue)) + stakingStateRouter.onNextClick() + } + + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { + innerRouter = router + this.stakingStateRouter = stateRouter + } + + private fun subscribeOnCurrencyStatusUpdates() { + viewModelScope.launch { + getUserWalletUseCase(userWalletId).fold( + ifRight = { wallet -> + userWallet = wallet + }, + ifLeft = { + // TODO staking error + }, + ) + getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold( + ifRight = { + cryptoCurrencyStatus = it + stateController.update( + transformer = SetInitialDataStateTransformer( + clickIntents = this@StakingViewModel, + yield = yield, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + ), + ) + }, + ifLeft = { + // TODO staking error + }, + ) + } + } + + private fun subscribeOnBalanceHiding() { + getBalanceHidingSettingsUseCase() + .conflate() + .distinctUntilChanged() + .onEach { + stateController.update(transformer = HideBalanceStateTransformer(it.isBalanceHidden)) + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + + private fun subscribeOnSelectedAppCurrency() { + getSelectedAppCurrencyUseCase() + .conflate() + .distinctUntilChanged() + .onEach { maybeAppCurrency -> + appCurrency = maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + + private suspend fun sendStakingTransaction(txData: TransactionData) { + stateController.update(SetConfirmStateInProgressTransformer()) + + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = cryptoCurrencyStatus.currency.network, + ).fold( + ifLeft = { error -> + Timber.e(error.toString()) + val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold + stateController.update( + SetConfirmStateConfirmTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + stakingGasEstimate = gasEstimate, + ), + ) + // uiState = eventStateFactory.getSendTransactionErrorState( + // error = error, + // onConsume = { uiState = eventStateFactory.onConsumeEventState() }, + // ) + // analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) + }, + ifRight = { + val gasEstimate = stakingTransaction?.gasEstimate ?: return@fold + stateController.update( + SetConfirmStateConfirmTransformer( + appCurrencyProvider = Provider { appCurrency }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + stakingGasEstimate = gasEstimate, + ), + ) + // sendScreenAnalyticSender.sendTransaction() + }, + ) + } +} \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 73348d6d0b..55f5e6eea3 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -32,8 +32,10 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.appCurrency.models) implementation(projects.domain.txhistory.models) + implementation(projects.domain.transaction.models) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.domain.staking) /** Core modules */ implementation(projects.core.utils) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index df53e5e0e7..1e04778c76 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,34 +2,30 @@ package com.tangem.feature.swap.domain import arrow.core.Either import arrow.core.getOrElse -import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.minimalAmount -import com.tangem.common.extensions.hexToBytes import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository +import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.FeePaidCurrency -import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount -import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.EstimateFeeUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase -import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.transaction.models.TransactionType +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -43,9 +39,9 @@ import com.tangem.feature.swap.domain.models.ui.* import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager -import com.tangem.lib.crypto.models.* -import com.tangem.lib.crypto.models.transactions.SendTxResult -import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.lib.crypto.models.ProxyAmount +import com.tangem.lib.crypto.models.ProxyFee +import com.tangem.lib.crypto.models.ProxyFees import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber @@ -62,24 +58,21 @@ internal class SwapInteractorImpl @Inject constructor( private val allowPermissionsHandler: AllowPermissionsHandler, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, - private val walletManagersFacade: WalletManagersFacade, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, + private val createTransactionExtrasUseCase: CreateTransactionDataExtrasUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val quotesRepository: QuotesRepository, - private val dispatcher: CoroutineDispatcherProvider, private val swapTransactionRepository: SwapTransactionRepository, private val currencyChecksRepository: CurrencyChecksRepository, private val appCurrencyRepository: AppCurrencyRepository, private val currenciesRepository: CurrenciesRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, - private val transactionRepository: TransactionRepository, + private val demoConfig: DemoConfig, + private val validateTransactionUseCase: ValidateTransactionUseCase, + private val estimateFeeUseCase: EstimateFeeUseCase, ) : SwapInteractor { - private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { - EstimateFeeUseCase(walletManagersFacade, dispatcher) - } - private val getSelectedAppCurrencyUseCase by lazy(LazyThreadSafetyMode.NONE) { GetSelectedAppCurrencyUseCase(appCurrencyRepository) } @@ -221,35 +214,51 @@ internal class SwapInteractorImpl @Inject constructor( } else { permissionOptions.approveData.approveData } - val result = transactionManager.sendApproveTransaction( - txData = ApproveTxData( - networkId = networkId, - feeAmount = permissionOptions.txFee.feeValue, - gasLimit = permissionOptions.txFee.gasLimit, - destinationAddress = getTokenAddress(permissionOptions.fromToken), - dataToSign = dataToSign, + val approveTransaction = createTransactionUseCase( + amount = BigDecimal.ZERO.convertToAmount(permissionOptions.fromToken), + fee = getFeeForTransaction( + fee = permissionOptions.txFee, + blockchain = Blockchain.fromId(permissionOptions.fromToken.network.id.value), ), - derivationPath = derivationPath, - analyticsData = AnalyticsData( - feeType = permissionOptions.txFee.feeType.getNameForAnalytics(), - tokenSymbol = permissionOptions.fromToken.symbol, - permissionType = permissionOptions.approveType.getNameForAnalytics(), + memo = null, + destination = getTokenAddress(permissionOptions.fromToken), + network = permissionOptions.fromToken.network, + userWalletId = requireNotNull(getSelectedWallet()).walletId, + txExtras = createDexTxExtras( + dataToSign, + permissionOptions.fromToken.network, + permissionOptions.txFee.gasLimit, ), + isSwap = false, + ).getOrElse { + Timber.e(it, "Failed to create approveTransaction") + return SwapTransactionState.UnknownError + } + + val result = sendTransactionUseCase( + txData = approveTransaction, + userWallet = requireNotNull(getSelectedWallet()), + network = permissionOptions.fromToken.network, ) - return when (result) { - is SendTxResult.Success -> { + return result.fold( + ifRight = { hash -> allowPermissionsHandler.addAddressToInProgress(permissionOptions.forTokenContractAddress) SwapTransactionState.TxSent( - txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), + txHash = hash, timestamp = System.currentTimeMillis(), ) - } - SendTxResult.UserCancelledError -> SwapTransactionState.UserCancelled - is SendTxResult.BlockchainSdkError -> SwapTransactionState.BlockchainError - is SendTxResult.TangemSdkError -> SwapTransactionState.TangemSdkError - is SendTxResult.NetworkError -> SwapTransactionState.NetworkError - is SendTxResult.UnknownError -> SwapTransactionState.UnknownError - } + }, + ifLeft = { + when (it) { + SendTransactionError.UserCancelledError -> SwapTransactionState.UserCancelled + is SendTransactionError.BlockchainSdkError -> SwapTransactionState.BlockchainError + is SendTransactionError.TangemSdkError -> SwapTransactionState.TangemSdkError + is SendTransactionError.NetworkError -> SwapTransactionState.NetworkError + is SendTransactionError.DemoCardError -> SwapTransactionState.DemoMode + else -> SwapTransactionState.UnknownError + } + }, + ) } override suspend fun findBestQuote( @@ -498,35 +507,33 @@ internal class SwapInteractorImpl @Inject constructor( ), ) - transactionRepository.validateTransaction( + validateTransactionUseCase( amount = amount.value.convertToAmount(fromToken), fee = fee, memo = null, - txExtras = null, destination = getTokenAddress(fromToken), userWalletId = userWalletId, network = fromToken.network, - ) - .fold( - onFailure = { - addCardanoTransactionValidationError( - warnings = warnings, - error = it as? BlockchainSdkError.Cardano ?: return@fold, - fromToken = fromToken, - userWalletId = userWalletId, + ).fold( + ifLeft = { + addCardanoTransactionValidationError( + warnings = warnings, + error = it as? BlockchainSdkError.Cardano ?: return@fold, + fromToken = fromToken, + userWalletId = userWalletId, + ) + }, + ifRight = { + minAdaValue?.let { + warnings.add( + Warning.Cardano.MinAdaValueCharged( + tokenName = fromToken.name, + minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), + ), ) - }, - onSuccess = { - minAdaValue?.let { - warnings.add( - Warning.Cardano.MinAdaValueCharged( - tokenName = fromToken.name, - minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), - ), - ) - } - }, - ) + } + }, + ) } private suspend fun addCardanoTransactionValidationError( @@ -666,11 +673,11 @@ internal class SwapInteractorImpl @Inject constructor( destination = swapData.transaction.txTo, userWalletId = userWalletId, network = currencyToSendStatus.currency.network, - txExtras = createDexTxExtras(fee.gasLimit, dataToSign), + txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, fee.gasLimit), hash = dataToSign, isSwap = true, ).getOrElse { - Timber.e(it) + Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.UnknownError } @@ -720,13 +727,13 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private fun createDexTxExtras(gasLimit: Int, data: String): TransactionExtras { - // for now we support only Ethereum like DEX - // need to be extended if we support other blockchains in DEX - return EthereumTransactionExtras( - gasLimit = gasLimit.toBigInteger(), - data = data.removePrefix(HEX_PREFIX).hexToBytes(), - ) + private fun createDexTxExtras(data: String, network: Network, gasLimit: Int?): TransactionExtras { + return createTransactionExtrasUseCase( + data = data, + network = network, + transactionType = TransactionType.APPROVE, + gasLimit = gasLimit?.toBigInteger(), + ).getOrNull() ?: error("failed to create extras") } @Suppress("LongMethod") @@ -757,27 +764,27 @@ internal class SwapInteractorImpl @Inject constructor( val exchangeDataCex = exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.UnknownError - val txExtras = transactionManager.getMemoExtras( - currencyToSend.currency.network.backendId, - exchangeDataCex.txExtraId, - ) - if (txExtras == null && exchangeDataCex.txExtraId != null) { - return SwapTransactionState.UnknownError - } + val cardId = getSelectedWallet()?.scanResponse?.card?.cardId ?: return SwapTransactionState.UnknownError + if (demoConfig.isDemoCardId(cardId)) return SwapTransactionState.UnknownError + val txData = createTransactionUseCase( amount = amount.value.convertToAmount(currencyToSend.currency), fee = getFeeForTransaction( fee = txFee, blockchain = Blockchain.fromId(currencyToSend.currency.network.id.value), ), - memo = null, + memo = exchangeDataCex.txExtraId, destination = exchangeDataCex.txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, ).getOrElse { - Timber.e(it) + Timber.e(it, "Failed to create swap CEX tx data") return SwapTransactionState.UnknownError - }.copy(extras = txExtras) + } + + if (txData.extras == null && exchangeDataCex.txExtraId != null) { + return SwapTransactionState.UnknownError + } val result = sendTransactionUseCase( txData = txData, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index a99ef1d6d7..3f20bc09bc 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.GetCardTokensListUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -11,8 +12,7 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.usecase.CreateTransactionUseCase -import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -42,16 +42,16 @@ class SwapDomainModule { getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, @SwapScope sendTransactionUseCase: SendTransactionUseCase, @SwapScope createTransactionUseCase: CreateTransactionUseCase, + createTransactionDataExtrasUseCase: CreateTransactionDataExtrasUseCase, isDemoCardUseCase: IsDemoCardUseCase, quotesRepository: QuotesRepository, swapTransactionRepository: SwapTransactionRepository, appCurrencyRepository: AppCurrencyRepository, currencyChecksRepository: CurrencyChecksRepository, - walletManagersFacade: WalletManagersFacade, - coroutineDispatcherProvider: CoroutineDispatcherProvider, initialToCurrencyResolver: InitialToCurrencyResolver, currenciesRepository: CurrenciesRepository, - transactionRepository: TransactionRepository, + validateTransactionUseCase: ValidateTransactionUseCase, + estimateFeeUseCase: EstimateFeeUseCase, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -62,16 +62,17 @@ class SwapDomainModule { getMultiCryptoCurrencyStatusUseCase = getCryptoCurrencyStatusUseCase, sendTransactionUseCase = sendTransactionUseCase, createTransactionUseCase = createTransactionUseCase, + createTransactionExtrasUseCase = createTransactionDataExtrasUseCase, isDemoCardUseCase = isDemoCardUseCase, quotesRepository = quotesRepository, - walletManagersFacade = walletManagersFacade, - dispatcher = coroutineDispatcherProvider, swapTransactionRepository = swapTransactionRepository, appCurrencyRepository = appCurrencyRepository, currencyChecksRepository = currencyChecksRepository, currenciesRepository = currenciesRepository, initialToCurrencyResolver = initialToCurrencyResolver, - transactionRepository = transactionRepository, + demoConfig = DemoConfig(), + validateTransactionUseCase = validateTransactionUseCase, + estimateFeeUseCase = estimateFeeUseCase, ) } @@ -96,12 +97,14 @@ class SwapDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyStatusesSyncUseCase { return GetCryptoCurrencyStatusesSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatchers, ) } @@ -113,11 +116,13 @@ class SwapDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetCardTokensListUseCase { return GetCardTokensListUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, ) } diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 643cf8f201..c41603a025 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -19,7 +19,8 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.utils) implementation(projects.core.ui) - implementation(projects.common) + implementation(projects.common.routing) + implementation(projects.common.ui) /** Domain modules **/ implementation(projects.domain.appCurrency) @@ -35,6 +36,7 @@ dependencies { implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.domain.staking) /** AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 59fd338f5a..fd4f8c9775 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -1,9 +1,9 @@ package com.tangem.feature.swap.analytics +import com.tangem.common.ui.bottomsheets.state.ApproveType import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.FeeType -import com.tangem.feature.swap.models.ApproveType private const val SWAP_CATEGORY = "Swap" private const val PROMO_CATEGORY = "Promo" diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 40dd04e697..390b8e7c7e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.converters -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency @@ -11,8 +11,8 @@ import com.tangem.feature.swap.models.CurrenciesGroupWithFromCurrency import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState -import com.tangem.utils.Provider import com.tangem.feature.swap.presentation.R +import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList @@ -76,10 +76,10 @@ class TokensDataConverter( ) } - private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): TokenIconState { + private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): CurrencyIconState { return when (currency) { is CryptoCurrency.Coin -> { - TokenIconState.CoinIcon( + CurrencyIconState.CoinIcon( url = currency.iconUrl, fallbackResId = currency.networkIconResId, isGrayscale = !isAvailable, @@ -90,11 +90,11 @@ class TokensDataConverter( val isGrayscale = currency.network.isTestnet val background = currency.tryGetBackgroundForTokenIcon(isGrayscale) val tint = getTintForTokenIcon(background) - TokenIconState.TokenIcon( + CurrencyIconState.TokenIcon( url = currency.iconUrl, isGrayscale = !isAvailable, showCustomBadge = currency.isCustom, - networkBadgeIconResId = currency.networkIconResId, + topBadgeIconResId = currency.networkIconResId, fallbackTint = tint, fallbackBackground = background, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt index 441abf4a2e..db8287c844 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapPresentationModule.kt @@ -1,10 +1,10 @@ package com.tangem.feature.swap.di +import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.feature.swap.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -23,11 +23,13 @@ class SwapPresentationModule { dispatcherProvider: CoroutineDispatcherProvider, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + stakingRepository: StakingRepository, ): GetCryptoCurrencyStatusSyncUseCase { return GetCryptoCurrencyStatusSyncUseCase( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + stakingRepository = stakingRepository, dispatchers = dispatcherProvider, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 585afb1921..d99e967096 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.models -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList @@ -20,7 +20,7 @@ sealed class TokenToSelectState { val id: String, val name: String, val symbol: String, - val tokenIcon: TokenIconState, + val tokenIcon: CurrencyIconState, val available: Boolean = true, val addedTokenBalanceData: TokenBalanceData? = null, ) : TokenToSelectState() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index cd22de2a3b..e3d7c3d5c8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionState import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig @@ -22,7 +23,7 @@ data class SwapStateHolder( val providerState: ProviderState, val fee: FeeItemState = FeeItemState.Empty, - val permissionState: SwapPermissionState = SwapPermissionState.Empty, + val permissionState: GiveTxPermissionState = GiveTxPermissionState.Empty, val priceImpact: PriceImpact, val successState: SwapSuccessStateHolder? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt index cbe749c6a8..5d05e969b8 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSuccessStateHolder.kt @@ -1,6 +1,6 @@ package com.tangem.feature.swap.models -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference data class SwapSuccessStateHolder( @@ -16,8 +16,8 @@ data class SwapSuccessStateHolder( val toTokenAmount: TextReference, val fromTokenFiatAmount: TextReference, val toTokenFiatAmount: TextReference, - val fromTokenIconState: TokenIconState?, - val toTokenIconState: TokenIconState?, + val fromTokenIconState: CurrencyIconState?, + val toTokenIconState: CurrencyIconState?, val onExploreButtonClick: () -> Unit, val onStatusButtonClick: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 6bbee12717..c0c890750e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.models +import com.tangem.common.ui.bottomsheets.state.ApproveType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.TxFee diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt deleted file mode 100644 index 9a8bd7a519..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/GivePermissionBottomSheetConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.feature.swap.models.states - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.feature.swap.models.SwapPermissionState - -data class GivePermissionBottomSheetConfig( - val data: SwapPermissionState.ReadyForRequest, - val onCancel: () -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt index 6409755135..60df4a856b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapFragment.kt @@ -2,12 +2,13 @@ package com.tangem.feature.swap.presentation import android.os.Bundle import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter + import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.swap.router.CustomTabsManager @@ -28,7 +29,7 @@ class SwapFragment : ComposeFragment() { override lateinit var uiDependencies: UiDependencies @Inject - lateinit var reduxNavController: ReduxNavController + lateinit var appRouter: AppRouter private val viewModel by viewModels() @@ -37,9 +38,8 @@ class SwapFragment : ComposeFragment() { lifecycle.addObserver(viewModel) viewModel.setRouter( SwapRouter( - fragmentManager = WeakReference(parentFragmentManager), customTabsManager = CustomTabsManager(WeakReference(context)), - reduxNavController = reduxNavController, + router = appRouter, ), ) } @@ -47,17 +47,17 @@ class SwapFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { viewModel.onScreenOpened() - - val backgroundColor = TangemTheme.colors.background.secondary - SystemBarsEffect { setSystemBarsColor(backgroundColor) } - ScreenContent(viewModel = viewModel) } @Suppress("TopLevelComposableFunctions") @Composable private fun ScreenContent(viewModel: SwapViewModel) { - Crossfade(targetState = viewModel.currentScreen, label = "") { screen -> + Crossfade( + modifier = Modifier.background(TangemTheme.colors.background.secondary), + targetState = viewModel.currentScreen, + label = "", + ) { screen -> when (screen) { SwapNavScreen.Main -> SwapScreen(stateHolder = viewModel.uiState) SwapNavScreen.Success -> { @@ -84,8 +84,4 @@ class SwapFragment : ComposeFragment() { lifecycle.removeObserver(viewModel) super.onDestroy() } - - companion object { - const val CURRENCY_BUNDLE_KEY = "swap_currency" - } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt index 081730868d..bc5009da3c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/SwapRouter.kt @@ -3,20 +3,14 @@ package com.tangem.feature.swap.router import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.os.bundleOf -import androidx.fragment.app.FragmentManager -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter -import java.lang.ref.WeakReference internal class SwapRouter( - private val fragmentManager: WeakReference, private val customTabsManager: CustomTabsManager, - private val reduxNavController: ReduxNavController, + private val router: AppRouter, ) { var currentScreen by mutableStateOf(SwapNavScreen.Main) @@ -30,7 +24,7 @@ internal class SwapRouter( if (currentScreen == SwapNavScreen.SelectToken) { currentScreen = SwapNavScreen.Main } else { - fragmentManager.get()?.popBackStack() + router.pop() } } @@ -39,13 +33,10 @@ internal class SwapRouter( } fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, - ), + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, ), ) } 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 1925e60ee7..2af78f3dfd 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 @@ -2,8 +2,9 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.ui.bottomsheets.state.* import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.* import com.tangem.core.ui.utils.BigDecimalFormatter @@ -649,7 +650,7 @@ internal class StateBuilder( ), receiveCardData = receiveCardData, warnings = warnings, - permissionState = SwapPermissionState.Empty, + permissionState = GiveTxPermissionState.Empty, fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, @@ -908,7 +909,7 @@ internal class StateBuilder( } fun updateApproveType(uiState: SwapStateHolder, approveType: ApproveType): SwapStateHolder { - val config = uiState.bottomSheetConfig?.content as? GivePermissionBottomSheetConfig + val config = uiState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig return if (config != null) { uiState.copy( bottomSheetConfig = uiState.bottomSheetConfig.copy( @@ -982,7 +983,7 @@ internal class StateBuilder( ), ) return uiState.copy( - permissionState = SwapPermissionState.InProgress, + permissionState = GiveTxPermissionState.InProgress, warnings = warnings, ) } @@ -1144,27 +1145,27 @@ internal class StateBuilder( } private fun convertPermissionState( - lastPermissionState: SwapPermissionState, + lastPermissionState: GiveTxPermissionState, permissionDataState: PermissionDataState, onGivePermissionClick: () -> Unit, onChangeApproveType: (ApproveType) -> Unit, - ): SwapPermissionState { - val approveType = if (lastPermissionState is SwapPermissionState.ReadyForRequest) { + ): GiveTxPermissionState { + val approveType = if (lastPermissionState is GiveTxPermissionState.ReadyForRequest) { lastPermissionState.approveType } else { ApproveType.UNLIMITED } return when (permissionDataState) { - PermissionDataState.Empty -> SwapPermissionState.Empty - PermissionDataState.PermissionFailed -> SwapPermissionState.Empty - PermissionDataState.PermissionLoading -> SwapPermissionState.InProgress + PermissionDataState.Empty -> GiveTxPermissionState.Empty + PermissionDataState.PermissionFailed -> GiveTxPermissionState.Empty + PermissionDataState.PermissionLoading -> GiveTxPermissionState.InProgress is PermissionDataState.PermissionReadyForRequest -> { val permissionFee = when (val fee = permissionDataState.requestApproveData.fee) { TxFeeState.Empty -> error("Fee shouldn't be empty") is TxFeeState.MultipleFeeState -> fee.priorityFee is TxFeeState.SingleFeeState -> fee.fee } - SwapPermissionState.ReadyForRequest( + GiveTxPermissionState.ReadyForRequest( currency = permissionDataState.currency, amount = permissionDataState.amount, approveType = approveType, @@ -1197,8 +1198,8 @@ internal class StateBuilder( fun showPermissionBottomSheet(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder { val permissionState = uiState.permissionState - if (permissionState is SwapPermissionState.ReadyForRequest) { - val config = GivePermissionBottomSheetConfig( + if (permissionState is GiveTxPermissionState.ReadyForRequest) { + val config = GiveTxPermissionBottomSheetConfig( data = permissionState, onCancel = onDismiss, ) @@ -1406,7 +1407,7 @@ internal class StateBuilder( return NotificationConfig( title = resourceReference(R.string.express_provider_permission_needed), subtitle = resourceReference( - id = R.string.swapping_permission_subheader, + id = R.string.give_permission_subtitle, formatArgs = wrappedList(fromTokenSymbol), ), iconResId = R.drawable.ic_locked_24, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt deleted file mode 100644 index 8ac175588a..0000000000 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheet.kt +++ /dev/null @@ -1,317 +0,0 @@ -package com.tangem.feature.swap.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.swap.models.ApprovePermissionButton -import com.tangem.feature.swap.models.ApproveType -import com.tangem.feature.swap.models.CancelPermissionButton -import com.tangem.feature.swap.models.SwapPermissionState -import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig -import com.tangem.feature.swap.presentation.R -import kotlinx.collections.immutable.ImmutableList - -@Composable -fun SwapPermissionBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: GivePermissionBottomSheetConfig -> - SwapPermissionBottomSheetContent(content = content) - } -} - -@Composable -private fun SwapPermissionBottomSheetContent(content: GivePermissionBottomSheetConfig) { - var isPermissionAlertShow by remember { mutableStateOf(false) } - val data = content.data - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier.align(Alignment.Center), - text = stringResource(id = R.string.swapping_permission_header), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - ) - IconButton( - modifier = Modifier.align(Alignment.CenterEnd), - onClick = { isPermissionAlertShow = true }, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_question_24), - contentDescription = null, - ) - } - } - - SpacerH10() - - Text( - text = stringResource( - id = R.string.swapping_permission_subheader, - data.currency, - ), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing8), - ) - - SpacerH16() - - ApprovalBottomSheetInfo(data) - - SpacerH28() - - PrimaryButtonIconEnd( - text = stringResource(id = R.string.swapping_permission_buttons_approve), - iconResId = R.drawable.ic_tangem_24, - modifier = Modifier.fillMaxWidth(), - onClick = data.approveButton.onClick, - ) - - SpacerH12() - - SecondaryButton( - text = stringResource(id = R.string.common_cancel), - modifier = Modifier.fillMaxWidth(), - onClick = { - content.onCancel() - }, - ) - - SpacerH16() - - // region dialog - if (isPermissionAlertShow) { - BasicDialog( - message = stringResource(id = R.string.swapping_approve_information_text), - title = stringResource(id = R.string.swapping_approve_information_title), - confirmButton = DialogButton { isPermissionAlertShow = false }, - onDismissDialog = {}, - ) - } - } -} - -@Composable -private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AmountItem( - currency = data.currency, - approveType = data.approveType, - onChangeApproveType = data.onChangeApproveType, - approveItems = data.approveItems, - ) - SubtitleItem( - subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer), - modifier = Modifier.fillMaxWidth(), - ) - SpacerH24() - DividerBottomSheet() - FeeItem(fee = data.fee.resolveReference()) - SubtitleItem( - subtitle = stringResource(id = R.string.swapping_permission_fee_footer), - modifier = Modifier.fillMaxWidth(), - ) - } -} - -@Composable -private fun DividerBottomSheet() { - Divider( - color = TangemTheme.colors.stroke.primary, - thickness = TangemTheme.dimens.size0_5, - ) -} - -@Composable -private fun InformationItem(subtitle: String, value: String) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = subtitle, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - maxLines = 1, - ) - - MiddleEllipsisText( - text = value, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - modifier = Modifier.padding(start = TangemTheme.dimens.spacing16), - ) - } -} - -@Composable -private fun AmountItem( - currency: String, - approveType: ApproveType, - approveItems: ImmutableList, - onChangeApproveType: (ApproveType) -> Unit, -) { - var isExpandSelector by remember { - mutableStateOf(false) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing16), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(id = R.string.swapping_permission_rows_amount, currency), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle1, - maxLines = 1, - ) - Box { - SelectorItem( - getTitleForApproveType(approveType = approveType), - ) { - isExpandSelector = true - } - DropdownSelector( - isExpanded = isExpandSelector, - onDismiss = { isExpandSelector = false }, - onItemClick = { approveType -> - isExpandSelector = false - onChangeApproveType.invoke(approveType) - }, - items = approveItems, - ) - } - } -} - -@Composable -private fun SelectorItem(title: String, onClick: () -> Unit) { - Row( - modifier = Modifier.clickable { onClick() }, - ) { - Text( - text = title, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - maxLines = 1, - ) - Icon( - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } -} - -@Composable -private fun DropdownSelector( - isExpanded: Boolean, - onDismiss: () -> Unit, - onItemClick: (ApproveType) -> Unit, - items: ImmutableList, -) { - DropdownMenu( - expanded = isExpanded, - onDismissRequest = onDismiss, - modifier = Modifier - .wrapContentSize() - .background(TangemTheme.colors.background.secondary), - ) { - items.forEach { item -> - DropdownMenuItem( - onClick = { - onItemClick.invoke(item) - }, - ) { - Text( - text = getTitleForApproveType(approveType = item), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body1, - maxLines = 1, - ) - } - } - } -} - -@Composable -private fun FeeItem(fee: String) { - InformationItem( - subtitle = stringResource(id = R.string.common_network_fee_title), - value = fee, - ) -} - -@Composable -private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = subtitle, - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - ) -} - -@Composable -private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) { - ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction) - ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited) -} - -// region preview - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_AgreementBottomSheet() { - TangemThemePreview { - SwapPermissionBottomSheetContent(content = previewData) - } -} - -private val previewData = GivePermissionBottomSheetConfig( - data = SwapPermissionState.ReadyForRequest( - currency = "DAI", - amount = "∞", - walletAddress = "", - spenderAddress = "", - fee = TextReference.Str("2,14$"), - approveType = ApproveType.UNLIMITED, - approveButton = ApprovePermissionButton(true) {}, - cancelButton = CancelPermissionButton(true), - onChangeApproveType = { ApproveType.UNLIMITED }, - ), - onCancel = {}, -) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 04a53a581c..9ae4b97bca 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -6,12 +6,14 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import com.tangem.common.ui.bottomsheets.GiveTxPermissionBottomSheet +import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionBottomSheetConfig import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig -import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig import com.tangem.feature.swap.presentation.R @@ -28,7 +30,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { iconRes = R.drawable.ic_close_24, ) }, - contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0), + contentWindowInsets = WindowInsetsZero, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> @@ -39,8 +41,8 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { stateHolder.bottomSheetConfig?.let { config -> when (config.content) { - is GivePermissionBottomSheetConfig -> { - SwapPermissionBottomSheet(config = config) + is GiveTxPermissionBottomSheetConfig -> { + GiveTxPermissionBottomSheet(config = config) } is ChooseProviderBottomSheetConfig -> { ChooseProviderBottomSheet(config = config) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 7be20ace87..582ba1f779 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout -import com.tangem.common.Strings.STARS +import com.tangem.common.ui.bottomsheets.state.GiveTxPermissionState import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig @@ -38,6 +38,7 @@ import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R +import com.tangem.utils.Strings.STARS @Suppress("LongMethod") @Composable @@ -414,7 +415,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U state.warnings.any { it is SwapWarning.PermissionNeeded } -> { PrimaryButton( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.swapping_give_permission), + text = stringResource(id = R.string.give_permission_title), enabled = true, onClick = onPermissionWarningClick, ) @@ -495,7 +496,7 @@ private val state = SwapStateHolder( onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {}, - permissionState = SwapPermissionState.InProgress, + permissionState = GiveTxPermissionState.InProgress, blockchainId = "POLYGON", providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 081b492357..6b753b705a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -10,7 +10,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -18,18 +18,21 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.tangem.common.Strings -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.SpacerH12 +import com.tangem.core.ui.components.SpacerW2 import com.tangem.core.ui.components.appbar.ExpandableSearchView -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.swap.models.* +import com.tangem.feature.swap.models.SwapSelectTokenStateHolder +import com.tangem.feature.swap.models.TokenBalanceData +import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.presentation.R +import com.tangem.utils.Strings import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -212,7 +215,7 @@ private fun TokenItem( ), verticalAlignment = Alignment.CenterVertically, ) { - TokenIcon( + CurrencyIcon( state = token.tokenIcon, shouldDisplayNetwork = true, ) @@ -280,7 +283,7 @@ private fun TokenItem( } private val token = TokenToSelectState.TokenToSelect( - tokenIcon = TokenIconState.CoinIcon( + tokenIcon = CurrencyIconState.CoinIcon( url = "", fallbackResId = 0, isGrayscale = false, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 6f1b0517ff..335a399520 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButton -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.inputrow.InputRowBestRate import com.tangem.core.ui.components.inputrow.InputRowDefault import com.tangem.core.ui.components.inputrow.InputRowImage @@ -65,7 +65,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad title = TextReference.Res(R.string.swapping_from_title), subtitle = state.fromTokenAmount, caption = state.fromTokenFiatAmount, - tokenIconState = state.fromTokenIconState ?: TokenIconState.Loading, + tokenIconState = state.fromTokenIconState ?: CurrencyIconState.Loading, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), @@ -76,7 +76,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad title = TextReference.Res(R.string.swapping_to_title), subtitle = state.toTokenAmount, caption = state.toTokenFiatAmount, - tokenIconState = state.toTokenIconState ?: TokenIconState.Loading, + tokenIconState = state.toTokenIconState ?: CurrencyIconState.Loading, modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), @@ -161,8 +161,8 @@ private val state = SwapSuccessStateHolder( toTokenAmount = TextReference.Str("1 000 MATIC"), fromTokenFiatAmount = TextReference.Str("1 000 $"), toTokenFiatAmount = TextReference.Str("1 000 $"), - fromTokenIconState = TokenIconState.Loading, - toTokenIconState = TokenIconState.Loading, + fromTokenIconState = CurrencyIconState.Loading, + toTokenIconState = CurrencyIconState.Loading, rate = TextReference.Str("1 000 DAI ~ 1 000 MATIC"), onExploreButtonClick = {}, onStatusButtonClick = {}, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index aada4b7d2b..969fadc27a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.viewmodels +import com.tangem.common.ui.bottomsheets.state.ApproveType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider @@ -7,7 +8,6 @@ import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.TxFee -import com.tangem.feature.swap.models.ApproveType data class SwapProcessDataState( // Initial network id 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 58731f8a00..6237a44e56 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 @@ -1,11 +1,15 @@ package com.tangem.feature.swap.viewmodels +import android.os.Bundle import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.Either import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.bottomsheets.state.ApproveType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -32,7 +36,6 @@ import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* -import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -70,8 +73,10 @@ internal class SwapViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { - private val initialCryptoCurrency: CryptoCurrency = - savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter CryptoCurrency found`") + private val initialCryptoCurrency: CryptoCurrency = savedStateHandle.get(AppRoute.Swap.CURRENCY_BUNDLE_KEY) + ?.unbundle(CryptoCurrency.serializer()) + ?: error("no expected parameter CryptoCurrency found`") + private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus private var isBalanceHidden = true @@ -1172,6 +1177,13 @@ internal class SwapViewModel @Inject constructor( ) } + private fun ApproveType.toDomainApproveType(): SwapApproveType { + return when (this) { + ApproveType.LIMITED -> SwapApproveType.LIMITED + ApproveType.UNLIMITED -> SwapApproveType.UNLIMITED + } + } + private fun triggerPromoProviderEvent(recommendedProvider: SwapProvider?, bestQuotesProvider: SwapProvider?) { // for now send event only for changelly if (recommendedProvider == null || diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt deleted file mode 100644 index a64be25542..0000000000 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/AppRestarter.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.tester.api - -/** - * Interface for app restarter - */ -interface AppRestarter { - - fun restart() -} \ No newline at end of file diff --git a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt index 913c04a076..7e647c8323 100644 --- a/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt +++ b/features/tester/api/src/main/java/com/tangem/features/tester/api/TesterRouter.kt @@ -1,5 +1,7 @@ package com.tangem.features.tester.api +import android.content.Intent + /** * Outer tester feature router * @@ -8,5 +10,5 @@ package com.tangem.features.tester.api interface TesterRouter { /** Open tester menu */ - fun startTesterScreen() + fun getEntryIntent(): Intent } \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 86db03dca7..01480f6da6 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.core.featuretoggles) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.navigation) /** Feature Apis */ implementation(projects.features.tester.api) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt deleted file mode 100644 index 22d2d4d3a9..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/ActivityClassWrapper.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.feature.tester - -import android.app.Activity - -/** - * Wraps the main activity class to avoid type erasure issues during injection. - * - * @property clazz activity class - */ -class ActivityClassWrapper( - val clazz: Class, -) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt deleted file mode 100644 index 34cd927e71..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/apprestarter/DefaultAppRestarter.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.tester.apprestarter - -import android.app.Activity -import android.content.Context -import android.content.Intent -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.features.tester.api.AppRestarter - -/** - * Entity that kills the process and restarts the main activity - * @property context Activity context - */ -internal class DefaultAppRestarter( - private val context: Context, - private val activityClassWrapper: ActivityClassWrapper, -) : AppRestarter { - - override fun restart() { - if (context !is Activity) return - - context.finish() - context.startActivity( - Intent(context, activityClassWrapper.clazz).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP }, - ) - Runtime.getRuntime().exit(0) - } -} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt deleted file mode 100644 index 4891a60f12..0000000000 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/di/RestarterModule.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.feature.tester.di - -import android.content.Context -import com.tangem.feature.tester.ActivityClassWrapper -import com.tangem.feature.tester.apprestarter.DefaultAppRestarter -import com.tangem.features.tester.api.AppRestarter -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.qualifiers.ActivityContext -import dagger.hilt.android.scopes.ActivityScoped - -@Module -@InstallIn(ActivityComponent::class) -internal object RestarterModule { - - @Provides - @ActivityScoped - fun provideAppRestarter( - @ActivityContext context: Context, - activityClassWrapper: ActivityClassWrapper, - ): AppRestarter { - return DefaultAppRestarter(context, activityClassWrapper) - } -} \ No newline at end of file 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 ef7ab21998..62206ea5e5 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 @@ -6,8 +6,9 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity import com.tangem.feature.tester.presentation.actions.TesterActionsScreen @@ -18,7 +19,6 @@ import com.tangem.feature.tester.presentation.menu.state.TesterMenuContentState import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.feature.tester.presentation.navigation.TesterScreen -import com.tangem.features.tester.api.AppRestarter import com.tangem.features.tester.api.TesterRouter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -35,7 +35,7 @@ internal class TesterActivity : ComposeActivity() { lateinit var testerRouter: TesterRouter @Inject - lateinit var appRestarter: AppRestarter + lateinit var appFinisher: AppFinisher private val innerTesterRouter: InnerTesterRouter get() = requireNotNull(testerRouter as? InnerTesterRouter) { @@ -45,9 +45,8 @@ internal class TesterActivity : ComposeActivity() { @Composable override fun ScreenContent(modifier: Modifier) { val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } + val systemUiController = rememberSystemUiController() + systemUiController.setSystemBarsColor(systemBarsColor) TesterNavHost() } @@ -70,7 +69,7 @@ internal class TesterActivity : ComposeActivity() { composable(route = TesterScreen.FEATURE_TOGGLES.name) { val viewModel = hiltViewModel().apply { - setupInteractions(innerTesterRouter, appRestarter) + setupInteractions(innerTesterRouter, appFinisher) } FeatureTogglesScreen(state = viewModel.uiState) 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 d3459ac31f..2d810e1ebf 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 @@ -7,10 +7,10 @@ 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.navigation.finisher.AppFinisher import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter -import com.tangem.features.tester.api.AppRestarter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -40,10 +40,10 @@ internal class FeatureTogglesViewModel @Inject constructor( } /** Setup navigation state property by router [router] and provides app restart method by [appRestarter] */ - fun setupInteractions(router: InnerTesterRouter, appRestarter: AppRestarter) { + fun setupInteractions(router: InnerTesterRouter, appFinisher: AppFinisher) { uiState = uiState.copy( onBackClick = router::back, - onApplyChangesClick = appRestarter::restart, + onApplyChangesClick = appFinisher::restart, ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt index 97126f3540..0d3c250e91 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/DefaultTesterRouter.kt @@ -22,10 +22,8 @@ internal class DefaultTesterRouter @Inject constructor( private var navController: NavController? = null - override fun startTesterScreen() { - context.startActivity( - Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), - ) + override fun getEntryIntent(): Intent { + return Intent(context, TesterActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } override fun setNavController(navController: NavController) { diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt index a0f630c125..a361d75438 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/navigation/TokenDetailsRouter.kt @@ -5,9 +5,4 @@ import androidx.fragment.app.Fragment interface TokenDetailsRouter { fun getEntryFragment(): Fragment - - companion object { - const val USER_WALLET_ID_KEY = "token_details_user_wallet_id" - const val CRYPTO_CURRENCY_KEY = "token_details_crypto_currency" - } } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index bff55fb9ad..73f39eae8c 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -38,13 +38,14 @@ dependencies { implementation(deps.tangem.card.core) implementation(deps.timber) implementation(deps.lifecycle.compose) + implementation(deps.kotlin.serialization) /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) /** Core modules */ - implementation(projects.common) + implementation(projects.common.routing) implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) @@ -85,4 +86,6 @@ dependencies { /** Feature Apis */ implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) + implementation(projects.features.staking.api) + } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt index b87eb5b538..94147e5592 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/di/TokenDetailsRouterModule.kt @@ -1,6 +1,8 @@ package com.tangem.feature.tokendetails.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.feature.tokendetails.presentation.router.DefaultTokenDetailsRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import dagger.Module @@ -15,7 +17,11 @@ internal object TokenDetailsRouterModule { @Provides @ActivityScoped - fun provideTokenDetailsRouter(reduxNavController: ReduxNavController): TokenDetailsRouter { - return DefaultTokenDetailsRouter(reduxNavController) + fun provideTokenDetailsRouter( + appRouter: AppRouter, + urlOpener: UrlOpener, + shareManager: ShareManager, + ): TokenDetailsRouter { + return DefaultTokenDetailsRouter(appRouter, urlOpener, shareManager) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index bd8ea330aa..25e7d71ad8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -6,8 +6,7 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen @@ -34,14 +33,8 @@ internal class TokenDetailsFragment : ComposeFragment() { override fun ScreenContent(modifier: Modifier) { val viewModel = hiltViewModel() viewModel.router = this@TokenDetailsFragment.internalTokenDetailsRouter - LocalLifecycleOwner.current.lifecycle.addObserver(viewModel) - - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - + NavigationBar3ButtonsScrim() TokenDetailsScreen(state = viewModel.uiState.collectAsStateWithLifecycle().value) } } \ 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 cadf3b9549..c2ddee4828 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -1,41 +1,50 @@ package com.tangem.feature.tokendetails.presentation.router -import androidx.core.os.bundleOf import androidx.fragment.app.Fragment -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.navigation.share.ShareManager +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.tokendetails.presentation.TokenDetailsFragment -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter internal class DefaultTokenDetailsRouter( - private val reduxNavController: ReduxNavController, + private val router: AppRouter, + private val urlOpener: UrlOpener, + private val shareManager: ShareManager, ) : InnerTokenDetailsRouter { override fun getEntryFragment(): Fragment = TokenDetailsFragment() override fun popBackStack() { - reduxNavController.navigate(NavigationAction.PopBackTo()) + router.pop() } override fun openUrl(url: String) { - reduxNavController.navigate(NavigationAction.OpenUrl(url = url)) + urlOpener.openUrl(url) } override fun share(text: String) { - reduxNavController.navigate(NavigationAction.Share(text)) + shareManager.shareText(text) } override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency, - ), + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, + ), + ) + } + + override fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yield: Yield) { + router.push( + AppRoute.Staking( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrency.id, + yield = yield, ), ) } 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 56eead22a5..7060190ec5 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 @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.router +import com.tangem.domain.staking.model.Yield import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.tokendetails.navigation.TokenDetailsRouter @@ -16,4 +17,6 @@ internal interface InnerTokenDetailsRouter : TokenDetailsRouter { fun share(text: String) fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) + + fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yield: Yield) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 470cc0ac26..d685def542 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -9,7 +9,9 @@ import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton @@ -17,7 +19,9 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow +import java.math.BigDecimal +@Suppress("LargeClass") internal object TokenDetailsPreviewData { val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig( @@ -100,17 +104,52 @@ internal object TokenDetailsPreviewData { TokenDetailsActionButton.Swap(dimContent = false, onClick = {}), ) - val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons) + private val balanceSegmentedButtonConfig = persistentListOf( + TokenBalanceSegmentedButtonConfig( + title = resourceReference(R.string.common_all), + type = BalanceType.ALL, + ), + TokenBalanceSegmentedButtonConfig( + title = resourceReference(R.string.staking_details_available), + type = BalanceType.AVAILABLE, + ), + ) + + val balanceLoading = TokenDetailsBalanceBlockState.Loading( + actionButtons = actionButtons, + balanceSegmentedButtonConfig = balanceSegmentedButtonConfig, + selectedBalanceType = BalanceType.ALL, + ) val balanceContent = TokenDetailsBalanceBlockState.Content( actionButtons = actionButtons, - fiatBalance = "91,50$", - cryptoBalance = "966,96 XLM", + fiatBalance = BigDecimal.ZERO, + cryptoBalance = BigDecimal.ZERO, + balanceSegmentedButtonConfig = balanceSegmentedButtonConfig, + selectedBalanceType = BalanceType.ALL, + onBalanceSelect = {}, + displayCryptoBalance = "966,96 XLM", + displayFiatBalance = "91,50$", + isBalanceSelectorEnabled = false, + ) + val balanceError = TokenDetailsBalanceBlockState.Error( + actionButtons = actionButtons, + balanceSegmentedButtonConfig = balanceSegmentedButtonConfig, + selectedBalanceType = BalanceType.ALL, ) - val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons) private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT") - private val stakingLoading = StakingBlockState.Loading(iconState = iconState) + private val stakingLoading = StakingBlocksState( + stakingAvailable = StakingAvailable.Loading(iconState), + stakingBalance = StakingBalance.Content( + cryptoValue = stringReference("5 SOL"), + fiatValue = stringReference("456.34 $"), + rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), + cryptoAmount = BigDecimal.ZERO, + fiatAmount = BigDecimal.ZERO, + onStakeClicked = {}, + ), + ) private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig( isRefreshing = false, @@ -246,7 +285,7 @@ internal object TokenDetailsPreviewData { tokenInfoBlockState = tokenInfoBlockState, tokenBalanceBlockState = balanceLoading, marketPriceBlockState = marketPriceLoading, - stakingBlockState = stakingLoading, + stakingBlocksState = stakingLoading, notifications = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( @@ -260,7 +299,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, - isStakingAvailable = false, + isStakingBlockShown = false, event = consumedEvent(), ) @@ -278,11 +317,22 @@ internal object TokenDetailsPreviewData { type = PriceChangeType.UP, ), ), - stakingBlockState = StakingBlockState.Content( - interestRate = "7.38", - periodInDays = 4, - tokenSymbol = "XLM", - iconState = iconState, + stakingBlocksState = StakingBlocksState( + stakingAvailable = StakingAvailable.Content( + interestRate = "7.38", + periodInDays = 4, + tokenSymbol = "XLM", + iconState = iconState, + onStakeClicked = {}, + ), + stakingBalance = StakingBalance.Content( + cryptoValue = stringReference("5 SOL"), + fiatValue = stringReference("456.34 $"), + rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")), + cryptoAmount = BigDecimal.ZERO, + fiatAmount = BigDecimal.ZERO, + onStakeClicked = {}, + ), ), notifications = persistentListOf(), txHistoryState = TxHistoryState.NotSupported( @@ -296,7 +346,7 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, - isStakingAvailable = true, + isStakingBlockShown = true, event = consumedEvent(), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt index f1668235b6..d1a635dd87 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockState.kt @@ -1,20 +1,41 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import java.math.BigDecimal + +internal data class StakingBlocksState( + val stakingAvailable: StakingAvailable, + val stakingBalance: StakingBalance, +) @Immutable -internal sealed interface StakingBlockState { - +internal sealed interface StakingAvailable { val iconState: IconState - data class Error(override val iconState: IconState) : StakingBlockState + data class Error(override val iconState: IconState) : StakingAvailable - data class Loading(override val iconState: IconState) : StakingBlockState + data class Loading(override val iconState: IconState) : StakingAvailable data class Content( override val iconState: IconState, val interestRate: String, val periodInDays: Int, val tokenSymbol: String, - ) : StakingBlockState + val onStakeClicked: () -> Unit, + ) : StakingAvailable +} + +@Immutable +sealed class StakingBalance { + data object Empty : StakingBalance() + + data class Content( + val cryptoValue: TextReference, + val fiatValue: TextReference, + val rewardValue: TextReference, + val cryptoAmount: BigDecimal?, + val fiatAmount: BigDecimal?, + val onStakeClicked: () -> Unit, + ) : StakingBalance() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 89f5925433..52f0679e6c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -1,7 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.ExchangeStatus @@ -23,11 +23,11 @@ internal data class SwapTransactionsState( val toCryptoCurrency: CryptoCurrency, val toCryptoAmount: String, val toFiatAmount: String, - val toCurrencyIcon: TokenIconState, + val toCurrencyIcon: CurrencyIconState, val fromCryptoCurrency: CryptoCurrency, val fromCryptoAmount: String, val fromFiatAmount: String, - val fromCurrencyIcon: TokenIconState, + val fromCurrencyIcon: CurrencyIconState, val showProviderLink: Boolean, val onClick: () -> Unit, val onGoToProviderClick: (String) -> Unit, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenBalanceSegmentedButtonConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenBalanceSegmentedButtonConfig.kt new file mode 100644 index 0000000000..d741d3e609 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenBalanceSegmentedButtonConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import com.tangem.core.ui.extensions.TextReference + +data class TokenBalanceSegmentedButtonConfig( + val title: TextReference, + val type: BalanceType, +) + +enum class BalanceType { + ALL, + AVAILABLE, +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt index 2f066dae01..3828253254 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockState.kt @@ -2,23 +2,36 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal internal sealed class TokenDetailsBalanceBlockState { abstract val actionButtons: ImmutableList + abstract val balanceSegmentedButtonConfig: ImmutableList + abstract val selectedBalanceType: BalanceType data class Loading( override val actionButtons: ImmutableList, + override val balanceSegmentedButtonConfig: ImmutableList, + override val selectedBalanceType: BalanceType, ) : TokenDetailsBalanceBlockState() data class Content( override val actionButtons: ImmutableList, - val fiatBalance: String, - val cryptoBalance: String, + override val balanceSegmentedButtonConfig: ImmutableList, + override val selectedBalanceType: BalanceType, + val fiatBalance: BigDecimal?, + val cryptoBalance: BigDecimal?, + val onBalanceSelect: (TokenBalanceSegmentedButtonConfig) -> Unit, + val displayCryptoBalance: String, + val displayFiatBalance: String, + val isBalanceSelectorEnabled: Boolean, ) : TokenDetailsBalanceBlockState() data class Error( override val actionButtons: ImmutableList, + override val balanceSegmentedButtonConfig: ImmutableList, + override val selectedBalanceType: BalanceType, ) : TokenDetailsBalanceBlockState() fun copyActionButtons(buttons: ImmutableList): TokenDetailsBalanceBlockState { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 26f36f81ad..b9594669da 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -17,7 +17,7 @@ internal data class TokenDetailsState( val tokenInfoBlockState: TokenInfoBlockState, val tokenBalanceBlockState: TokenDetailsBalanceBlockState, val marketPriceBlockState: MarketPriceBlockState, - val stakingBlockState: StakingBlockState, + val stakingBlocksState: StakingBlocksState, val notifications: ImmutableList, val pendingTxs: PersistentList, val swapTxs: PersistentList, @@ -27,6 +27,6 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, - val isStakingAvailable: Boolean, + val isStakingBlockShown: Boolean, val event: StateEvent, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index c5b6078527..d1ef7792c8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -91,4 +91,19 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { dimContent = dimContent, ), ) + + /** + * Staking + * + * @property dimContent determines whether the button content will be dimmed + * @property onClick lambda be invoked when Swap button is clicked + */ + data class Stake(val dimContent: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_stake), + iconResId = R.drawable.ic_arrow_down_24, // TODO staking + onClick = onClick, + dimContent = dimContent, + ), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt index dbf8a99f3c..afe2413ce6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsActionButtonsConverter.kt @@ -38,6 +38,12 @@ internal class TokenDetailsActionButtonsConverter( onLongClick = clickIntents::onCopyAddress, ) } + is TokenActionsState.ActionState.Stake -> { + TokenDetailsActionButton.Stake( + dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, + onClick = { clickIntents.onStakeClick(action.unavailabilityReason) }, + ) + } is TokenActionsState.ActionState.Sell -> { TokenDetailsActionButton.Sell( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt new file mode 100644 index 0000000000..a42e0f24d6 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -0,0 +1,75 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory + +import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.tokendetails.presentation.tokendetails.state.* +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance +import com.tangem.utils.Provider +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class TokenDetailsBalanceSelectStateConverter( + private val currentStateProvider: Provider, + private val appCurrencyProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, +) : Converter { + + override fun convert(value: TokenBalanceSegmentedButtonConfig): TokenDetailsState { + return with(currentStateProvider()) { + if (stakingBlocksState.stakingBalance !is StakingBalance.Content) return this + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() ?: return this + + val stakingCryptoAmount = stakingBlocksState.stakingBalance.cryptoAmount + val stakingFiatAmount = stakingBlocksState.stakingBalance.fiatAmount + + copy( + tokenBalanceBlockState = if (tokenBalanceBlockState is TokenDetailsBalanceBlockState.Content) { + tokenBalanceBlockState.copy( + selectedBalanceType = value.type, + displayFiatBalance = formatFiatAmount( + status = cryptoCurrencyStatus.value, + stakingFiatAmount = stakingFiatAmount, + selectedBalanceType = value.type, + appCurrency = appCurrencyProvider(), + ), + displayCryptoBalance = formatCryptoAmount( + status = cryptoCurrencyStatus, + stakingCryptoAmount = stakingCryptoAmount, + selectedBalanceType = value.type, + ), + ) + } else { + tokenBalanceBlockState + }, + ) + } + } + + private fun formatFiatAmount( + status: CryptoCurrencyStatus.Value, + stakingFiatAmount: BigDecimal?, + selectedBalanceType: BalanceType, + appCurrency: AppCurrency, + ): String { + val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount) + + return BigDecimalFormatter.formatFiatAmount( + fiatAmount = totalAmount, + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } + + private fun formatCryptoAmount( + status: CryptoCurrencyStatus, + stakingCryptoAmount: BigDecimal?, + selectedBalanceType: BalanceType, + ): String { + val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) + + return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 63d735d3ad..5a1524feaa 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -6,19 +6,27 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.transactions.state.TxHistoryState +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.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter +import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles +import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, @@ -26,6 +34,7 @@ internal class TokenDetailsLoadedBalanceConverter( private val symbol: String, private val decimals: Int, private val clickIntents: TokenDetailsClickIntents, + private val stakingFeatureToggles: StakingFeatureToggles, ) : Converter, TokenDetailsState> { private val txHistoryItemConverter by lazy { @@ -33,13 +42,21 @@ internal class TokenDetailsLoadedBalanceConverter( } override fun convert(value: Either): TokenDetailsState { - return value.fold(ifLeft = { convertError() }, ifRight = ::convert) + return value.fold( + ifLeft = { convertError() }, + ifRight = { convert(it) }, + ) } private fun convertError(): TokenDetailsState { val state = currentStateProvider() return state.copy( - tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(state.tokenBalanceBlockState.actionButtons), + isStakingBlockShown = false, + tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error( + actionButtons = state.tokenBalanceBlockState.actionButtons, + balanceSegmentedButtonConfig = state.tokenBalanceBlockState.balanceSegmentedButtonConfig, + selectedBalanceType = state.tokenBalanceBlockState.selectedBalanceType, + ), marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencySymbol), notifications = persistentListOf(TokenDetailsNotification.NetworksUnreachable), ) @@ -49,8 +66,13 @@ internal class TokenDetailsLoadedBalanceConverter( val state = currentStateProvider() val currencyName = state.marketPriceBlockState.currencySymbol val pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList() + return state.copy( - tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status), + tokenBalanceBlockState = getBalanceState( + currentState = state.tokenBalanceBlockState, + status = status, + ), + stakingBlocksState = getYieldBalance(status, state), marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName), pendingTxs = pendingTxs, txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) { @@ -65,6 +87,9 @@ internal class TokenDetailsLoadedBalanceConverter( currentState: TokenDetailsBalanceBlockState, status: CryptoCurrencyStatus, ): TokenDetailsBalanceBlockState { + val stakingCryptoAmount = (status.value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance() + val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } + val isBalanceSelectorEnabled = stakingFeatureToggles.isStakingEnabled && !stakingCryptoAmount.isNullOrZero() return when (status.value) { is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.Loaded, @@ -72,17 +97,82 @@ internal class TokenDetailsLoadedBalanceConverter( is CryptoCurrencyStatus.Custom, -> TokenDetailsBalanceBlockState.Content( actionButtons = currentState.actionButtons, - fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()), - cryptoBalance = formatCryptoAmount(status), + cryptoBalance = status.value.amount, + fiatBalance = status.value.fiatAmount, + displayFiatBalance = formatFiatAmount( + status.value, + stakingFiatAmount, + currentState.selectedBalanceType, + appCurrencyProvider(), + ), + displayCryptoBalance = formatCryptoAmount( + status, + stakingCryptoAmount, + currentState.selectedBalanceType, + ), + balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, + onBalanceSelect = clickIntents::onBalanceSelect, + selectedBalanceType = currentState.selectedBalanceType, + isBalanceSelectorEnabled = isBalanceSelectorEnabled, + ) + is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading( + actionButtons = currentState.actionButtons, + balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, + selectedBalanceType = currentState.selectedBalanceType, ) - is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(currentState.actionButtons) is CryptoCurrencyStatus.MissedDerivation, is CryptoCurrencyStatus.Unreachable, is CryptoCurrencyStatus.NoAmount, - -> TokenDetailsBalanceBlockState.Error(currentState.actionButtons) + -> TokenDetailsBalanceBlockState.Error( + actionButtons = currentState.actionButtons, + balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig, + selectedBalanceType = currentState.selectedBalanceType, + ) } } + private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlocksState { + val yieldBalance = status.value.yieldBalance as? YieldBalance.Data + + val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() + val stakingRewardAmount = yieldBalance?.getRewardStakingBalance() + val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) } + + val stakingBalance = if (stakingCryptoAmount.isNullOrZero()) { + StakingBalance.Empty + } else { + StakingBalance.Content( + cryptoAmount = stakingCryptoAmount, + fiatAmount = stakingFiatAmount, + cryptoValue = stringReference( + BigDecimalFormatter.formatCryptoAmount(stakingCryptoAmount, symbol, decimals), + ), + fiatValue = stringReference( + BigDecimalFormatter.formatFiatAmount( + stakingFiatAmount, + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ), + ), + rewardValue = resourceReference( + R.string.staking_details_rewards_to_claim, + wrappedList( + BigDecimalFormatter.formatFiatAmount( + stakingRewardAmount, + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ), + ), + ), + onStakeClicked = clickIntents::onStakeBannerClick, + ) + } + return state.stakingBlocksState.copy( + stakingAvailable = state.stakingBlocksState.stakingAvailable, + stakingBalance = stakingBalance, + ) + } + private fun getMarketPriceState( status: CryptoCurrencyStatus.Value, currencySymbol: String, @@ -140,19 +230,30 @@ internal class TokenDetailsLoadedBalanceConverter( ) } - private fun formatFiatAmount(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { + private fun formatFiatAmount( + status: CryptoCurrencyStatus.Value, + stakingFiatAmount: BigDecimal?, + selectedBalanceType: BalanceType, + appCurrency: AppCurrency, + ): String { val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount) return BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatAmount, + fiatAmount = totalAmount, fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) } - private fun formatCryptoAmount(status: CryptoCurrencyStatus): String { + private fun formatCryptoAmount( + status: CryptoCurrencyStatus, + stakingCryptoAmount: BigDecimal?, + selectedBalanceType: BalanceType, + ): String { val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) - return BigDecimalFormatter.formatCryptoAmount(amount, status.currency.symbol, status.currency.decimals) + return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 2c29e74ce3..3268e37904 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.networkIconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.feature.tokendetails.presentation.tokendetails.state.* @@ -17,7 +16,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.Toke import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.lib.crypto.BlockchainUtils.isBitcoin -import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -27,7 +25,6 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsSkeletonStateConverter( private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, - private val stakingAvailabilityProvider: Provider, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -51,9 +48,16 @@ internal class TokenDetailsSkeletonStateConverter( ) }, ), - tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()), + tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading( + actionButtons = createButtons(), + balanceSegmentedButtonConfig = createBalanceSegmentedButtonConfig(), + selectedBalanceType = BalanceType.ALL, + ), marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol), - stakingBlockState = StakingBlockState.Loading(iconState = iconState), + stakingBlocksState = StakingBlocksState( + stakingAvailable = StakingAvailable.Loading(iconState), + stakingBalance = StakingBalance.Empty, + ), notifications = persistentListOf(), pendingTxs = persistentListOf(), swapTxs = persistentListOf(), @@ -67,7 +71,7 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, - isStakingAvailable = stakingAvailabilityProvider.invoke() is StakingAvailability.Available, + isStakingBlockShown = false, event = consumedEvent(), ) } @@ -102,6 +106,19 @@ internal class TokenDetailsSkeletonStateConverter( ) } + private fun createBalanceSegmentedButtonConfig(): ImmutableList { + return persistentListOf( + TokenBalanceSegmentedButtonConfig( + title = resourceReference(R.string.common_all), + type = BalanceType.ALL, + ), + TokenBalanceSegmentedButtonConfig( + title = resourceReference(R.string.staking_details_available), + type = BalanceType.AVAILABLE, + ), + ) + } + private fun createPullToRefresh(): TokenDetailsPullToRefreshConfig = TokenDetailsPullToRefreshConfig( isRefreshing = false, onRefresh = clickIntents::onRefreshSwipe, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index c47c971328..952b0d2dc6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -24,15 +24,14 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError -import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter.TokenDetailsLoadingTxHistoryModel import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider @@ -44,9 +43,10 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, - private val stakingAvailabilityProvider: Provider, + private val cryptoCurrencyStatusProvider: Provider, private val clickIntents: TokenDetailsClickIntents, private val featureToggles: TokenDetailsFeatureToggles, + stakingFeatureToggles: StakingFeatureToggles, symbol: String, decimals: Int, ) { @@ -55,7 +55,6 @@ internal class TokenDetailsStateFactory( TokenDetailsSkeletonStateConverter( clickIntents = clickIntents, featureToggles = featureToggles, - stakingAvailabilityProvider = stakingAvailabilityProvider, ) } @@ -70,6 +69,7 @@ internal class TokenDetailsStateFactory( symbol = symbol, decimals = decimals, clickIntents = clickIntents, + stakingFeatureToggles = stakingFeatureToggles, ) } @@ -105,6 +105,15 @@ internal class TokenDetailsStateFactory( private val stakingStateConverter by lazy { TokenStakingStateConverter( currentStateProvider = currentStateProvider, + clickIntents = clickIntents, + ) + } + + private val balanceSelectStateConverter by lazy { + TokenDetailsBalanceSelectStateConverter( + currentStateProvider = currentStateProvider, + appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, ) } @@ -212,9 +221,15 @@ internal class TokenDetailsStateFactory( ) } + fun getStateWithUpdatedStakingAvailability(stakingAvailability: StakingAvailability): TokenDetailsState { + return currentStateProvider().copy( + isStakingBlockShown = stakingAvailability != StakingAvailability.Unavailable, + ) + } + fun getStateWithStaking(stakingEither: Either): TokenDetailsState { return currentStateProvider().copy( - stakingBlockState = stakingStateConverter.convert(stakingEither), + stakingBlocksState = stakingStateConverter.convert(stakingEither), ) } @@ -344,6 +359,12 @@ internal class TokenDetailsStateFactory( } } + fun getStateWithUpdatedBalanceSegmentedButtonConfig( + buttonConfig: TokenBalanceSegmentedButtonConfig, + ): TokenDetailsState { + return balanceSelectStateConverter.convert(buttonConfig) + } + private fun TokenDetailsAppBarMenuConfig.updateMenu( cardTypesResolver: CardTypesResolver, isBitcoin: Boolean, @@ -370,6 +391,12 @@ internal class TokenDetailsStateFactory( private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.StakingUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_staking_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } is ScenarioUnavailabilityReason.PendingTransaction -> { when (unavailabilityReason.withdrawalScenario) { ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 6c1a71f7af..a0231a352a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt index 371e2e766d..29fe830039 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenStakingStateConverter.kt @@ -3,31 +3,42 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlocksState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter internal class TokenStakingStateConverter( + private val clickIntents: TokenDetailsClickIntents, private val currentStateProvider: Provider, -) : Converter, StakingBlockState> { +) : Converter, StakingBlocksState> { - override fun convert(value: Either): StakingBlockState { + override fun convert(value: Either): StakingBlocksState { value.fold( ifLeft = { - return StakingBlockState.Error( - iconState = currentStateProvider().tokenInfoBlockState.iconState, + return StakingBlocksState( + stakingAvailable = StakingAvailable.Error( + iconState = currentStateProvider().tokenInfoBlockState.iconState, + ), + stakingBalance = StakingBalance.Empty, ) }, ifRight = { - return StakingBlockState.Content( - interestRate = BigDecimalFormatter.formatPercent( - percent = it.interestRate, - useAbsoluteValue = true, + return StakingBlocksState( + stakingAvailable = StakingAvailable.Content( + interestRate = BigDecimalFormatter.formatPercent( + percent = it.interestRate, + useAbsoluteValue = true, + ), + periodInDays = it.periodInDays, + tokenSymbol = it.tokenSymbol, + iconState = currentStateProvider().tokenInfoBlockState.iconState, + onStakeClicked = clickIntents::onStakeBannerClick, ), - periodInDays = it.periodInDays, - tokenSymbol = it.tokenSymbol, - iconState = currentStateProvider().tokenInfoBlockState.iconState, + stakingBalance = StakingBalance.Empty, ) }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt new file mode 100644 index 0000000000..faa0144943 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/utils/BalanceUtils.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils + +import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType +import java.math.BigDecimal + +fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal { + return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null) { + this.plus(stakingAmount) + } else { + this + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index c24ca651d8..95bd0ed963 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -3,10 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.ExperimentalMaterialApi @@ -14,6 +11,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable @@ -21,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -42,10 +41,10 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.* import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar @@ -53,6 +52,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.StakingBalanceBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock // TODO: Split to blocks [REDACTED_JIRA] @Suppress("LongMethod") @@ -60,11 +61,13 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.e @Composable internal fun TokenDetailsScreen(state: TokenDetailsState) { BackHandler(onBack = state.topAppBarConfig.onBackClick) + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val snackbarHostState = remember { SnackbarHostState() } Scaffold( topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> val pullRefreshState = rememberPullRefreshState( @@ -90,7 +93,9 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = TangemTheme.dimens.spacing16), + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, + ), ) { item { TokenInfoBlock( @@ -135,11 +140,28 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { ) } - if (state.isStakingAvailable) { + if (state.isStakingBlockShown) { + if (state.stakingBlocksState.stakingBalance is StakingBalance.Content) { + item( + key = StakingBalance::class.java, + contentType = StakingBalance::class.java, + content = { + StakingBalanceBlock( + state = state.stakingBlocksState.stakingBalance, + modifier = itemModifier, + ) + }, + ) + } item( - key = StakingBlockState::class.java, - contentType = StakingBlockState::class.java, - content = { TokenStakingBlock(modifier = itemModifier, state = state.stakingBlockState) }, + key = StakingAvailable::class.java, + contentType = StakingAvailable::class.java, + content = { + TokenStakingBlock( + modifier = itemModifier, + state = state.stakingBlocksState.stakingAvailable, + ) + }, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 3f448ae33a..394e52f37b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -11,9 +11,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.buttons.HorizontalActionChips +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -21,6 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPre import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.Strings.STARS import kotlinx.collections.immutable.toImmutableList @Composable @@ -35,20 +37,23 @@ internal fun TokenDetailsBalanceBlock( color = TangemTheme.colors.background.primary, ) { Column { - Box( + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12) .padding(horizontal = TangemTheme.dimens.spacing12) .fillMaxWidth() .heightIn(min = TangemTheme.dimens.spacing24), - contentAlignment = Alignment.CenterStart, ) { Text( text = stringResource(id = R.string.common_balance_title), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.subtitle2, maxLines = 1, + modifier = Modifier + .weight(1f) + .padding(top = TangemTheme.dimens.spacing12), ) + BalanceButtons(state) } FiatBalance( state = state, @@ -89,7 +94,7 @@ private fun FiatBalance( ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = if (isBalanceHidden) STARS else state.fiatBalance, + text = if (isBalanceHidden) STARS else state.displayFiatBalance, style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) @@ -117,7 +122,7 @@ private fun CryptoBalance( ) is TokenDetailsBalanceBlockState.Content -> Text( modifier = modifier, - text = if (isBalanceHidden) STARS else state.cryptoBalance, + text = if (isBalanceHidden) STARS else state.displayCryptoBalance, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) @@ -130,6 +135,35 @@ private fun CryptoBalance( } } +@Composable +private fun BalanceButtons(state: TokenDetailsBalanceBlockState) { + if (state !is TokenDetailsBalanceBlockState.Content || !state.isBalanceSelectorEnabled) return + + SegmentedButtons( + config = state.balanceSegmentedButtonConfig, + onClick = state.onBalanceSelect, + showIndication = false, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing11) + .width(IntrinsicSize.Min), + ) { config -> + Text( + text = config.title.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.caption1, + maxLines = 1, + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing5, + end = TangemTheme.dimens.spacing5, + top = TangemTheme.dimens.spacing3, + bottom = TangemTheme.dimens.spacing3, + ) + .align(Alignment.Center), + ) + } +} + @Preview(widthDp = 328, heightDp = 152) @Preview(widthDp = 328, heightDp = 152, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt index 6aee05217a..319e6f44fe 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeEstimate.kt @@ -8,7 +8,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.inputrow.InputRowApprox import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -19,8 +19,8 @@ import com.tangem.features.tokendetails.impl.R @Composable internal fun ExchangeEstimate( timestamp: TextReference, - fromTokenIconState: TokenIconState, - toTokenIconState: TokenIconState, + fromTokenIconState: CurrencyIconState, + toTokenIconState: CurrencyIconState, fromCryptoAmount: TextReference, fromCryptoSymbol: String, toCryptoAmount: TextReference, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index 1945621d19..4b2a5a1ab4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -23,8 +23,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.constraintlayout.compose.* import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.swap.domain.models.domain.ExchangeStatus @@ -72,8 +72,8 @@ internal fun LazyListScope.swapTransactionsItems( @Composable private fun ExchangeStatusItem( providerName: String, - fromTokenIconState: TokenIconState, - toTokenIconState: TokenIconState, + fromTokenIconState: CurrencyIconState, + toTokenIconState: CurrencyIconState, fromAmount: String, fromSymbol: String, toSymbol: String, @@ -102,7 +102,7 @@ private fun ExchangeStatusItem( top.linkTo(parent.top) }, ) - TokenIcon( + CurrencyIcon( state = fromTokenIconState, shouldDisplayNetwork = false, modifier = Modifier @@ -139,7 +139,7 @@ private fun ExchangeStatusItem( bottom.linkTo(parent.bottom) }, ) - TokenIcon( + CurrencyIcon( state = toTokenIconState, shouldDisplayNetwork = false, modifier = Modifier @@ -205,8 +205,8 @@ private fun ExchangeStatusItemPreview( TangemThemePreview { ExchangeStatusItem( providerName = "ChangeNow", - fromTokenIconState = TokenIconState.Loading, - toTokenIconState = TokenIconState.Loading, + fromTokenIconState = CurrencyIconState.Loading, + toTokenIconState = CurrencyIconState.Loading, fromAmount = amount, fromSymbol = "USDT", toSymbol = "USDT", diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt new file mode 100644 index 0000000000..34986f925b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/StakingBalanceBlock.kt @@ -0,0 +1,107 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.Strings + +@Composable +fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(), + onClick = state.onStakeClicked, + ) + .padding(TangemTheme.dimens.spacing12), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(R.string.staking_native), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing2), + ) + Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { + Text( + text = state.fiatValue.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = Strings.DOT, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.cryptoValue.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + Text( + text = state.rewardValue.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_24), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + ) + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun StakingBalanceBlock_Preview( + @PreviewParameter(StakingBalanceBlockPreviewProvider::class) data: StakingBalance, +) { + TangemThemePreview { + StakingBalanceBlock( + state = data as StakingBalance.Content, + modifier = Modifier.padding(TangemTheme.dimens.spacing16), + ) + } +} + +private class StakingBalanceBlockPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TokenDetailsPreviewData.tokenDetailsState_1.stakingBlocksState.stakingBalance, + TokenDetailsPreviewData.tokenDetailsState_2.stakingBlocksState.stakingBalance, + ) +} +// endregion \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt similarity index 84% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index 6ce576304f..e9f66e62ab 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -1,11 +1,12 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -14,14 +15,17 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.* -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA import com.tangem.core.ui.utils.GrayscaleColorFilter import com.tangem.core.ui.utils.NORMAL_ALPHA import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.CurrencyIcon import com.tangem.features.tokendetails.impl.R /** @@ -31,7 +35,7 @@ import com.tangem.features.tokendetails.impl.R * @param modifier modifier */ @Composable -internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Modifier) { +internal fun TokenStakingBlock(state: StakingAvailable, modifier: Modifier = Modifier) { Column( modifier = modifier .background( @@ -49,7 +53,7 @@ internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Mo } @Composable -private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) { +private fun Content(state: StakingAvailable, modifier: Modifier = Modifier) { AnimatedContent( modifier = modifier.heightIn(min = TangemTheme.dimens.size60), targetState = state, @@ -57,24 +61,24 @@ private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) { label = "Update the content", ) { stakingBlockState -> when (stakingBlockState) { - is StakingBlockState.Content -> { + is StakingAvailable.Content -> { StakingContent( stakingBlockState = stakingBlockState, iconState = stakingBlockState.iconState, ) } - is StakingBlockState.Loading -> { + is StakingAvailable.Loading -> { StakingLoading( iconState = stakingBlockState.iconState, ) } - is StakingBlockState.Error -> Row {} // TODO staking + is StakingAvailable.Error -> Row {} // TODO staking } } } @Composable -private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconState: IconState) { +private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconState: IconState) { Column { Row { val (alpha, colorFilter) = remember(iconState.isGrayscale) { @@ -121,8 +125,8 @@ private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconSta } SecondaryButton( modifier = Modifier.fillMaxWidth(), - text = "Stake", - onClick = { /* [REDACTED_TODO_COMMENT] */ }, + text = stringResource(id = R.string.common_stake), + onClick = stakingBlockState.onStakeClicked, ) } } @@ -177,23 +181,24 @@ private fun StakingLoading(iconState: IconState) { @Composable private fun Preview_TokenStakingBlock( @PreviewParameter(StakingBlockStateProvider::class) - state: StakingBlockState, + state: StakingAvailable, ) { TangemThemePreview { TokenStakingBlock(state = state) } } -private class StakingBlockStateProvider : CollectionPreviewParameterProvider( +private class StakingBlockStateProvider : CollectionPreviewParameterProvider( collection = listOf( - StakingBlockState.Content( + StakingAvailable.Content( iconState = iconState, interestRate = "10", periodInDays = 4, tokenSymbol = "SOL", + onStakeClicked = {}, ), - StakingBlockState.Loading(iconState = iconState), - StakingBlockState.Error(iconState = iconState), + StakingAvailable.Loading(iconState = iconState), + StakingAvailable.Error(iconState = iconState), ), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 751b8dcd92..9332738ab0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig @Suppress("TooManyFunctions") interface TokenDetailsClickIntents { @@ -12,6 +13,8 @@ interface TokenDetailsClickIntents { fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onSendClick(unavailabilityReason: ScenarioUnavailabilityReason) fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) @@ -55,4 +58,8 @@ interface TokenDetailsClickIntents { fun onCopyAddress(): TextReference? fun onAssociateClick() + + fun onStakeBannerClick() + + fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) } \ 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 4d115db24e..bccaa00b68 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 @@ -1,9 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels +import android.os.Bundle import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.bundle.unbundle import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.deeplink.DeepLinksRegistry @@ -28,6 +31,7 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase +import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction @@ -58,12 +62,13 @@ import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRout import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig +import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.featuretoggles.TokenDetailsFeatureToggles import com.tangem.features.tokendetails.impl.R -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -100,7 +105,9 @@ internal class TokenDetailsViewModel @Inject constructor( private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase, private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase, + private val stakingFeatureToggles: StakingFeatureToggles, private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase, + private val getYieldUseCase: GetYieldUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val quotesRepository: QuotesRepository, @@ -117,12 +124,14 @@ internal class TokenDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents { - private val userWalletId: UserWalletId = savedStateHandle.get(TokenDetailsRouter.USER_WALLET_ID_KEY) - ?.let { stringValue -> UserWalletId(stringValue) } + private val userWalletId: UserWalletId = savedStateHandle.get(AppRoute.CurrencyDetails.USER_WALLET_ID_KEY) + ?.unbundle(UserWalletId.serializer()) ?: error("This screen can't open without `UserWalletId`") - private val cryptoCurrency: CryptoCurrency = savedStateHandle[TokenDetailsRouter.CRYPTO_CURRENCY_KEY] - ?: error("This screen can't open without `CryptoCurrency`") + private val cryptoCurrency: CryptoCurrency = + savedStateHandle.get(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) + ?.unbundle(CryptoCurrency.serializer()) + ?: error("This screen can't open without `CryptoCurrency`") private val userWallet: UserWallet @@ -141,13 +150,12 @@ internal class TokenDetailsViewModel @Inject constructor( private val stateFactory = TokenDetailsStateFactory( currentStateProvider = Provider { uiState.value }, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - stakingAvailabilityProvider = Provider { - getStakingAvailabilityUseCase.invoke(cryptoCurrency.network.id.value) - }, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, clickIntents = this, symbol = cryptoCurrency.symbol, decimals = cryptoCurrency.decimals, featureToggles = tokenDetailsFeatureToggles, + stakingFeatureToggles = stakingFeatureToggles, ) private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -214,7 +222,10 @@ internal class TokenDetailsViewModel @Inject constructor( subscribeOnCurrencyStatusUpdates() subscribeOnExchangeTransactionsUpdates() updateTxHistory(refresh = false, showItemsLoading = true) - updateStakingInfo() + + if (stakingFeatureToggles.isStakingEnabled) { + updateStakingInfo() + } } private fun handleBalanceHiding(owner: LifecycleOwner) { @@ -369,8 +380,12 @@ internal class TokenDetailsViewModel @Inject constructor( } private fun updateStakingInfo() { - viewModelScope.launch(dispatchers.main) { - val stakingAvailability = getStakingAvailabilityUseCase(cryptoCurrency.network.id.value) + viewModelScope.launch { + val stakingAvailability = getStakingAvailabilityUseCase( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ) + internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability) if (stakingAvailability is StakingAvailability.Available) { val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId) internalUiState.value = stateFactory.getStateWithStaking(stakingInfo) @@ -428,6 +443,15 @@ internal class TokenDetailsViewModel @Inject constructor( router.openTokenDetails(userWalletId = userWalletId, currency = cryptoCurrency) } + override fun onStakeBannerClick() { + viewModelScope.launch { + val yield = getYieldUseCase.invoke(cryptoCurrency.id, cryptoCurrency.symbol).getOrNull() + yield ?: error("Staking is unavailable") + + router.openStaking(userWalletId, cryptoCurrency, yield) + } + } + override fun onReloadClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReload(cryptoCurrency.symbol)) internalUiState.value = stateFactory.getLoadingTxHistoryState() @@ -525,6 +549,10 @@ internal class TokenDetailsViewModel @Inject constructor( } } + override fun onStakeClick(unavailabilityReason: ScenarioUnavailabilityReason) { + Timber.e("Not implemented yet") + } + override fun onGenerateExtendedKey() { viewModelScope.launch(dispatchers.main) { val extendedKey = getExtendedPublicKeyForCurrencyUseCase( @@ -786,6 +814,10 @@ internal class TokenDetailsViewModel @Inject constructor( } } + override fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) { + internalUiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config) + } + private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false diff --git a/features/wallet-settings/api/.gitignore b/features/wallet-settings/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/wallet-settings/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/wallet-settings/api/build.gradle.kts b/features/wallet-settings/api/build.gradle.kts new file mode 100644 index 0000000000..7c48ae5a59 --- /dev/null +++ b/features/wallet-settings/api/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.feature.walletsettings.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.wallets.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) +} \ No newline at end of file diff --git a/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/RenameWalletComponent.kt b/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/RenameWalletComponent.kt new file mode 100644 index 0000000000..4d3b21b9f1 --- /dev/null +++ b/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/RenameWalletComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.walletsettings.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface RenameWalletComponent : ComposableDialogComponent { + + data class Params( + val userWalletId: UserWalletId, + val currentName: String, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/WalletSettingsComponent.kt b/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/WalletSettingsComponent.kt new file mode 100644 index 0000000000..2c8cdd889a --- /dev/null +++ b/features/wallet-settings/api/src/main/kotlin/com/tangem/feature/walletsettings/component/WalletSettingsComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.walletsettings.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +interface WalletSettingsComponent : ComposableContentComponent { + + data class Params( + val userWalletId: UserWalletId, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/wallet-settings/impl/.gitignore b/features/wallet-settings/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/wallet-settings/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts new file mode 100644 index 0000000000..a94b469537 --- /dev/null +++ b/features/wallet-settings/impl/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.feature.walletsettings.impl" +} + +dependencies { + + /* Project - API */ + implementation(projects.features.walletSettings.api) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.featuretoggles) + implementation(projects.core.navigation) + implementation(projects.core.analytics.models) + implementation(projects.common.routing) + + /* Project - Domain */ + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + + /* AndroidX */ + implementation(deps.androidx.fragment.ktx) + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) + + /* Compose */ + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.shimmer) + implementation(deps.decompose.ext.compose) + + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + + /* Other */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.timber) +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt new file mode 100644 index 0000000000..a4c28c649c --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultRenameWalletComponent.kt @@ -0,0 +1,92 @@ +package com.tangem.feature.walletsettings.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UpdateWalletError +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.RenameWalletUseCase +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.entity.RenameWalletUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.feature.walletsettings.ui.RenameWalletDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DefaultRenameWalletComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: RenameWalletComponent.Params, + private val renameWalletUseCase: RenameWalletUseCase, +) : RenameWalletComponent, AppComponentContext by context { + + private val userWalletId = params.userWalletId + private val currentWalletName = params.currentName + + override val doOnDismiss: () -> Unit = params.onDismiss + + private val stateFlow: MutableStateFlow = MutableStateFlow( + value = RenameWalletUM( + walletNameValue = TextFieldValue(text = params.currentName), + isNameCorrect = false, + updateValue = ::updateValue, + onConfirm = { renameWallet(params.userWalletId) }, + ), + ) + + @Composable + override fun Dialog() { + val model by stateFlow.collectAsStateWithLifecycle() + + RenameWalletDialog( + model = model, + onDismiss = doOnDismiss, + ) + } + + private fun updateValue(value: TextFieldValue) { + stateFlow.update { + it.copy( + walletNameValue = value, + isNameCorrect = value.text.isNotBlank() && value.text != currentWalletName, + ) + } + } + + private fun renameWallet(userWalletId: UserWalletId) = componentScope.launch { + val newName = stateFlow.value.walletNameValue + val maybeError = renameWalletUseCase(userWalletId, newName.text).leftOrNull() + + if (maybeError != null) { + val message = when (maybeError) { + UpdateWalletError.DataError -> resourceReference( + id = R.string.common_unknown_error, + ) + UpdateWalletError.NameAlreadyExists -> resourceReference( + id = R.string.user_wallet_list_rename_popup_error_already_exists, + formatArgs = wrappedList(newName), + ) + } + + messageSender.send(message = SnackbarMessage(message)) + } + + doOnDismiss() + } + + @AssistedFactory + interface Factory : RenameWalletComponent.Factory { + override fun create( + context: AppComponentContext, + params: RenameWalletComponent.Params, + ): DefaultRenameWalletComponent + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt new file mode 100644 index 0000000000..39ed1d65a5 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt @@ -0,0 +1,74 @@ +package com.tangem.feature.walletsettings.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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.decompose.ComposableDialogComponent +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.model.WalletSettingsModel +import com.tangem.feature.walletsettings.ui.WalletSettingsScreen +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultWalletSettingsComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: WalletSettingsComponent.Params, + private val renameWalletComponentFactory: RenameWalletComponent.Factory, +) : WalletSettingsComponent, AppComponentContext by context { + + private val model: WalletSettingsModel = getOrCreateModel(params) + + private val dialog = childSlot( + source = model.dialogNavigation, + serializer = DialogConfig.serializer(), + handleBackButton = true, + childFactory = ::dialogChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + val dialog by dialog.subscribeAsState() + + WalletSettingsScreen( + modifier = modifier, + state = state, + dialog = { dialog.child?.instance?.Dialog() }, + ) + } + + private fun dialogChild( + dialogConfig: DialogConfig, + componentContext: ComponentContext, + ): ComposableDialogComponent = when (dialogConfig) { + is DialogConfig.RenameWallet -> { + renameWalletComponentFactory.create( + context = childByContext(componentContext), + params = RenameWalletComponent.Params( + userWalletId = dialogConfig.userWalletId, + currentName = dialogConfig.currentName, + onDismiss = model.dialogNavigation::dismiss, + ), + ) + } + } + + @AssistedFactory + interface Factory : WalletSettingsComponent.Factory { + override fun create( + context: AppComponentContext, + params: WalletSettingsComponent.Params, + ): DefaultWalletSettingsComponent + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt new file mode 100644 index 0000000000..4c7837a830 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewRenameWalletComponent.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.walletsettings.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.entity.RenameWalletUM +import com.tangem.feature.walletsettings.ui.RenameWalletDialog + +internal class PreviewRenameWalletComponent : RenameWalletComponent { + + private val previewState = RenameWalletUM( + walletNameValue = TextFieldValue(text = "My Wallet"), + isNameCorrect = false, + updateValue = {}, + onConfirm = {}, + ) + + override val doOnDismiss: () -> Unit = {} + + @Composable + override fun Dialog() { + RenameWalletDialog(model = previewState, onDismiss = {}) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt new file mode 100644 index 0000000000..1ba3218c38 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt @@ -0,0 +1,32 @@ +package com.tangem.feature.walletsettings.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.navigation.DummyRouter +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.ui.WalletSettingsScreen +import com.tangem.feature.walletsettings.utils.ItemsBuilder + +internal class PreviewWalletSettingsComponent : WalletSettingsComponent { + + private val previewState = WalletSettingsUM( + popBack = {}, + items = ItemsBuilder( + router = DummyRouter(), + ).buildItems( + walletName = "My wallet", + renameWallet = {}, + forgetWallet = {}, + ), + ) + + @Composable + override fun Content(modifier: Modifier) { + WalletSettingsScreen( + modifier = modifier, + state = previewState, + dialog = {}, + ) + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt new file mode 100644 index 0000000000..90ff4e1255 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ComponentModule.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.walletsettings.di + +import com.tangem.feature.walletsettings.component.RenameWalletComponent +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.component.impl.DefaultRenameWalletComponent +import com.tangem.feature.walletsettings.component.impl.DefaultWalletSettingsComponent +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 ComponentModule { + + @Binds + @Singleton + fun bindWalletSettingsComponentFactory( + factory: DefaultWalletSettingsComponent.Factory, + ): WalletSettingsComponent.Factory + + @Binds + @Singleton + fun bindRenameWalletComponentFactory(factory: DefaultRenameWalletComponent.Factory): RenameWalletComponent.Factory +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt new file mode 100644 index 0000000000..f7c93024a5 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/di/ModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.walletsettings.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.walletsettings.model.WalletSettingsModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(WalletSettingsModel::class) + fun provideWalletSettingsModel(model: WalletSettingsModel): Model +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/DialogConfig.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/DialogConfig.kt new file mode 100644 index 0000000000..b338e5d3eb --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/DialogConfig.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.walletsettings.entity + +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface DialogConfig { + + @Serializable + data class RenameWallet( + val userWalletId: UserWalletId, + val currentName: String, + ) : DialogConfig +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt new file mode 100644 index 0000000000..4dd5b8c7a7 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/RenameWalletUM.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.walletsettings.entity + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.text.input.TextFieldValue + +@Immutable +internal data class RenameWalletUM( + val walletNameValue: TextFieldValue, + val isNameCorrect: Boolean, + val updateValue: (value: TextFieldValue) -> Unit, + val onConfirm: () -> Unit, +) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt new file mode 100644 index 0000000000..9596f1fa02 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.walletsettings.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class WalletSettingsItemUM { + + abstract val id: String + + data class WithItems( + override val id: String, + val description: TextReference, + val blocks: ImmutableList, + ) : WalletSettingsItemUM() + + data class WithText( + override val id: String, + val title: TextReference, + val text: TextReference, + val onClick: () -> Unit, + ) : WalletSettingsItemUM() +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt new file mode 100644 index 0000000000..749a2ce005 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsUM.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.walletsettings.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.PersistentList + +@Immutable +internal data class WalletSettingsUM( + val popBack: () -> Unit, + val items: PersistentList, +) \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt new file mode 100644 index 0000000000..7926ce7f87 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -0,0 +1,107 @@ +package com.tangem.feature.walletsettings.model + +import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.routing.AppRoute +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.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.DeleteWalletUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.walletsettings.component.WalletSettingsComponent +import com.tangem.feature.walletsettings.entity.DialogConfig +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.impl.R +import com.tangem.feature.walletsettings.utils.ItemsBuilder +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@ComponentScoped +internal class WalletSettingsModel @Inject constructor( + private val router: Router, + private val messageSender: UiMessageSender, + private val getWalletUseCase: GetUserWalletUseCase, + private val deleteWalletUseCase: DeleteWalletUseCase, + private val itemsBuilder: ItemsBuilder, + private val paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + val params: WalletSettingsComponent.Params = paramsContainer.require() + val dialogNavigation = SlotNavigation() + + val state: MutableStateFlow = MutableStateFlow( + value = WalletSettingsUM( + popBack = router::pop, + items = persistentListOf(), + ), + ) + + init { + getWalletUseCase.invokeFlow(params.userWalletId) + .distinctUntilChanged() + .onEach { maybeWallet -> + val wallet = maybeWallet.getOrElse { error -> + error( + """ + Failed to get user wallet + |- User wallet ID: $params + |- Cause: $error + """.trimIndent(), + ) + } + + state.update { value -> + value.copy(items = buildItems(wallet, dialogNavigation)) + } + } + .launchIn(modelScope) + } + + private fun buildItems( + userWallet: UserWallet, + dialogNavigation: SlotNavigation, + ): PersistentList = itemsBuilder.buildItems( + walletName = userWallet.name, + renameWallet = { openRenameWalletDialog(userWallet, dialogNavigation) }, + forgetWallet = { forgetWallet(userWallet.walletId) }, + ) + + private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation) { + val config = DialogConfig.RenameWallet( + userWalletId = userWallet.walletId, + currentName = userWallet.name, + ) + + dialogNavigation.activate(config) + } + + private fun forgetWallet(userWalletId: UserWalletId) = modelScope.launch { + val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + messageSender.send( + message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), + ) + + return@launch + } + + if (hasUserWallets) { + router.pop() + } else { + router.replaceAll(AppRoute.Home) + } + } +} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt new file mode 100644 index 0000000000..62a9f39c72 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/RenameWalletDialog.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.walletsettings.ui + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.AdditionalTextInputDialogParams +import com.tangem.core.ui.components.DialogButton +import com.tangem.core.ui.components.TextInputDialog +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.walletsettings.component.preview.PreviewRenameWalletComponent +import com.tangem.feature.walletsettings.entity.RenameWalletUM +import com.tangem.feature.walletsettings.impl.R + +@Composable +internal fun RenameWalletDialog(model: RenameWalletUM, onDismiss: () -> Unit) { + val value by rememberUpdatedState(newValue = model.walletNameValue) + + TextInputDialog( + title = stringResource(id = R.string.user_wallet_list_rename_popup_title), + fieldValue = value, + confirmButton = DialogButton( + title = stringResource(id = R.string.common_ok), + enabled = model.isNameCorrect, + onClick = { + model.onConfirm() + onDismiss() + }, + ), + dismissButton = DialogButton( + title = stringResource(id = R.string.common_cancel), + onClick = onDismiss, + ), + onDismissDialog = onDismiss, + onValueChange = model.updateValue, + textFieldParams = AdditionalTextInputDialogParams( + label = stringResource(id = R.string.user_wallet_list_rename_popup_placeholder), + ), + ) +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_RenameWalletDialog() { + TangemThemePreview { + PreviewRenameWalletComponent().Dialog() + } +} +// endregion Preview \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt new file mode 100644 index 0000000000..9dcfd53ddb --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -0,0 +1,171 @@ +package com.tangem.feature.walletsettings.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.appbar.models.TopAppBarMedium +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.BlockItem +import com.tangem.core.ui.components.snackbar.TangemSnackbarHost +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.LocalSnackbarHostState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.feature.walletsettings.component.preview.PreviewWalletSettingsComponent +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.entity.WalletSettingsUM +import com.tangem.feature.walletsettings.impl.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun WalletSettingsScreen( + state: WalletSettingsUM, + dialog: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + val backgroundColor = TangemTheme.colors.background.secondary + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + BackHandler(onBack = state.popBack) + + Scaffold( + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + containerColor = backgroundColor, + snackbarHost = { + TangemSnackbarHost( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing16), + hostState = LocalSnackbarHostState.current, + ) + }, + topBar = { + TopAppBarMedium( + title = resourceReference(R.string.wallet_settings_title), + scrollBehavior = scrollBehavior, + onBackClick = state.popBack, + ) + }, + content = { paddingValues -> + Content( + modifier = Modifier.padding(paddingValues), + state = state, + ) + + dialog() + }, + ) +} + +@Composable +private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { + LazyColumn( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + contentPadding = PaddingValues( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + items( + items = state.items, + key = WalletSettingsItemUM::id, + ) { item -> + val itemModifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16) + + when (item) { + is WalletSettingsItemUM.WithItems -> ItemsBlock( + modifier = itemModifier, + model = item, + ) + is WalletSettingsItemUM.WithText -> TextBlock( + modifier = itemModifier, + model = item, + ) + } + } + } +} + +@Composable +private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .background( + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + ), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + ) { + model.blocks.forEach { block -> + BlockItem(model = block) + } + } + + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + text = model.description.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } +} + +@Composable +private fun TextBlock(model: WalletSettingsItemUM.WithText, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier.fillMaxWidth(), + onClick = model.onClick, + ) { + Column( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Text( + text = model.title.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + text = model.text.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_WalletSettingsScreen() { + TangemThemePreview { + PreviewWalletSettingsComponent().Content(modifier = Modifier.fillMaxSize()) + } +} +// endregion Preview \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt new file mode 100644 index 0000000000..52f95c08b8 --- /dev/null +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -0,0 +1,65 @@ +package com.tangem.feature.walletsettings.utils + +import com.tangem.common.routing.AppRoute +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.ui.components.block.model.BlockUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM +import com.tangem.feature.walletsettings.impl.R +import kotlinx.collections.immutable.persistentListOf +import javax.inject.Inject + +@ComponentScoped +internal class ItemsBuilder @Inject constructor( + private val router: Router, +) { + + fun buildItems(walletName: String, forgetWallet: () -> Unit, renameWallet: () -> Unit) = persistentListOf( + buildNameItem(walletName, renameWallet), + buildCardItem(), + buildForgetItem(forgetWallet), + ) + + private fun buildNameItem(walletName: String, renameWallet: () -> Unit) = WalletSettingsItemUM.WithText( + id = "wallet_name", + title = resourceReference(id = R.string.settings_wallet_name_title), + text = stringReference(walletName), + onClick = renameWallet, + ) + + private fun buildCardItem() = WalletSettingsItemUM.WithItems( + id = "card", + blocks = buildCardBlocks(), + description = resourceReference(R.string.settings_card_settings_footer), + ) + + private fun buildCardBlocks() = persistentListOf( + BlockUM( + text = resourceReference(R.string.card_settings_title), + iconRes = R.drawable.ic_card_settings_24, + onClick = { router.push(AppRoute.CardSettings) }, + ), + BlockUM( + text = resourceReference(R.string.referral_title), + iconRes = R.drawable.ic_add_friends_24, + onClick = { router.push(AppRoute.ReferralProgram) }, + ), + ) + + private fun buildForgetItem(forgetWallet: () -> Unit) = WalletSettingsItemUM.WithItems( + id = "forget", + blocks = buildForgetBlocks(forgetWallet), + description = resourceReference(R.string.settings_forget_wallet_footer), + ) + + private fun buildForgetBlocks(forgetWallet: () -> Unit) = persistentListOf( + BlockUM( + text = resourceReference(R.string.settings_forget_wallet), + iconRes = R.drawable.ic_card_foget_24, + onClick = forgetWallet, + accentType = BlockUM.AccentType.WARNING, + ), + ) +} \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index dad4976a7d..bbfc624efd 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -52,7 +52,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(projects.common) + implementation(projects.common.routing) implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) @@ -77,6 +77,7 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.analytics) implementation(projects.domain.visa) + implementation(projects.domain.staking) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) @@ -88,4 +89,6 @@ dependencies { implementation(projects.features.tester.api) implementation(projects.features.manageTokens.api) implementation(projects.features.details.api) + implementation(projects.features.pushNotifications.api) + implementation(projects.features.markets.api) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt index 1e64fc4122..4e031b0729 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletRouterModule.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.di -import com.tangem.core.navigation.ReduxNavController +import com.tangem.common.routing.AppRouter +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.redux.ReduxStateHolder import com.tangem.feature.wallet.presentation.router.DefaultWalletRouter import com.tangem.features.wallet.navigation.WalletRouter import dagger.Module @@ -15,7 +17,11 @@ internal object WalletRouterModule { @Provides @ActivityScoped - fun provideWalletRouter(reduxNavController: ReduxNavController): WalletRouter { - return DefaultWalletRouter(reduxNavController = reduxNavController) + fun provideWalletRouter( + appRouter: AppRouter, + urlOpener: UrlOpener, + reduxStateHolder: ReduxStateHolder, + ): WalletRouter { + return DefaultWalletRouter(appRouter, urlOpener, reduxStateHolder) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 5a2c923015..7c3629f145 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -3,11 +3,8 @@ package com.tangem.feature.wallet.presentation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.wallet.navigation.WalletRouter import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -23,9 +20,6 @@ internal class WalletFragment : ComposeFragment() { @Inject override lateinit var uiDependencies: UiDependencies - @Inject - internal lateinit var manageTokensUi: ManageTokensUi - /** Feature router */ @Inject internal lateinit var walletRouter: WalletRouter @@ -37,14 +31,8 @@ internal class WalletFragment : ComposeFragment() { @Composable override fun ScreenContent(modifier: Modifier) { - val systemBarsColor = TangemTheme.colors.background.secondary - SystemBarsEffect { - setSystemBarsColor(systemBarsColor) - } - _walletRouter.Initialize( onFinish = requireActivity()::finish, - manageTokensUi = manageTokensUi, ) } 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 92abf8f92f..e1f6a29f77 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.common import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference @@ -67,7 +67,7 @@ internal object WalletPreviewData { } val coinIconState - get() = TokenIconState.CoinIcon( + get() = CurrencyIconState.CoinIcon( url = null, fallbackResId = R.drawable.img_polygon_22, isGrayscale = false, @@ -75,9 +75,9 @@ internal object WalletPreviewData { ) private val tokenIconState - get() = TokenIconState.TokenIcon( + get() = CurrencyIconState.TokenIcon( url = null, - networkBadgeIconResId = R.drawable.img_polygon_22, + topBadgeIconResId = R.drawable.img_polygon_22, fallbackTint = TangemColorPalette.Black, fallbackBackground = TangemColorPalette.Meadow, isGrayscale = false, @@ -85,10 +85,10 @@ internal object WalletPreviewData { ) private val customTokenIconState - get() = TokenIconState.CustomTokenIcon( + get() = CurrencyIconState.CustomTokenIcon( tint = TangemColorPalette.Black, background = TangemColorPalette.Meadow, - networkBadgeIconResId = R.drawable.img_polygon_22, + topBadgeIconResId = R.drawable.img_polygon_22, isGrayscale = false, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt index eb3f107e08..01c4e11d48 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/TokenItem.kt @@ -16,7 +16,7 @@ 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.Constraints -import com.tangem.core.ui.components.currency.tokenicon.TokenIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.res.TangemTheme @@ -49,7 +49,7 @@ internal fun TokenItem( .tokenClickable(state = state) .background(color = TangemTheme.colors.background.primary), ) { - TokenIcon( + CurrencyIcon( state = state.iconState, modifier = Modifier .layoutId(layoutId = LayoutId.ICON) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt index 1d8ddf1d9d..ff7c37e28b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenCryptoAmount.kt @@ -8,10 +8,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -import com.tangem.common.Strings import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R +import com.tangem.utils.Strings import com.tangem.feature.wallet.presentation.common.state.TokenItemState.CryptoAmountState as TokenCryptoAmountState @Composable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt index 8e0535314e..6bc1be0dbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/token/TokenFiatAmount.kt @@ -7,9 +7,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.text.style.TextOverflow -import com.tangem.common.Strings import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.res.TangemTheme +import com.tangem.utils.Strings import com.tangem.feature.wallet.presentation.common.state.TokenItemState.FiatAmountState as TokenFiatAmountState @Composable 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 509a1b3085..635afd0fc9 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 @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.common.preview -import androidx.compose.runtime.mutableStateOf -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference @@ -12,13 +11,12 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.features.managetokens.navigation.ExpandableState import kotlinx.collections.immutable.persistentListOf internal object WalletScreenPreviewData { private val tokenItemState = TokenItemState.Content( id = "1", - iconState = TokenIconState.Locked, + iconState = CurrencyIconState.Locked, titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), cryptoAmountState = TokenItemState.CryptoAmountState.Content(text = "0,35853044 BTC"), @@ -58,7 +56,7 @@ internal object WalletScreenPreviewData { WalletTokensListState.TokensListItemState.Token( state = TokenItemState.Unreachable( id = "3", - iconState = TokenIconState.Locked, + iconState = CurrencyIconState.Locked, titleState = TokenItemState.TitleState.Content(text = "Polygon"), onItemClick = {}, onItemLongClick = {}, @@ -151,7 +149,6 @@ internal object WalletScreenPreviewData { internal val walletScreenState = WalletScreenState( onBackClick = {}, - manageTokensExpandableState = mutableStateOf(ExpandableState.COLLAPSED), topBarConfig = topBarConfig, selectedWalletIndex = 0, wallets = persistentListOf( @@ -161,6 +158,5 @@ internal object WalletScreenPreviewData { onWalletChange = {}, event = consumedEvent(), isHidingMode = false, - manageTokenRedesignToggle = false, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt index 80fdc10cdc..c5bc2e083e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/state/TokenItemState.kt @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.common.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.tokenicon.TokenIconState +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType /** Token item state */ @@ -10,7 +10,7 @@ internal sealed class TokenItemState { abstract val id: String - abstract val iconState: TokenIconState + abstract val iconState: CurrencyIconState abstract val titleState: TitleState @@ -23,7 +23,7 @@ internal sealed class TokenItemState { /** Loading token state */ data class Loading( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState.Content, ) : TokenItemState() { override val fiatAmountState: FiatAmountState = FiatAmountState.Loading @@ -33,7 +33,7 @@ internal sealed class TokenItemState { /** Locked token state */ data class Locked(override val id: String) : TokenItemState() { - override val iconState: TokenIconState = TokenIconState.Locked + override val iconState: CurrencyIconState = CurrencyIconState.Locked override val titleState: TitleState = TitleState.Locked override val fiatAmountState: FiatAmountState = FiatAmountState.Locked override val cryptoAmountState: CryptoAmountState = CryptoAmountState.Locked @@ -51,7 +51,7 @@ internal sealed class TokenItemState { */ data class Content( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState, override val fiatAmountState: FiatAmountState, override val cryptoAmountState: CryptoAmountState.Content, @@ -69,7 +69,7 @@ internal sealed class TokenItemState { */ data class Draggable( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState, override val cryptoAmountState: CryptoAmountState, ) : TokenItemState() { @@ -88,7 +88,7 @@ internal sealed class TokenItemState { */ data class Unreachable( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState, val onItemClick: () -> Unit, val onItemLongClick: () -> Unit, @@ -108,7 +108,7 @@ internal sealed class TokenItemState { */ data class NoAddress( override val id: String, - override val iconState: TokenIconState, + override val iconState: CurrencyIconState, override val titleState: TitleState, val onItemLongClick: () -> Unit, ) : TokenItemState() { 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 4efcf827cb..6c3aea984f 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 @@ -16,11 +16,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -28,6 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig @@ -36,6 +36,7 @@ import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkGroupItem @@ -57,8 +58,12 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier Scaffold( modifier = modifier, topBar = { - TopBar(state.header, tokensListState) + TopBar( + config = state.header, + tokensListState = tokensListState, + ) }, + contentWindowInsets = WindowInsetsZero, content = { paddingValues -> TokenList( modifier = Modifier @@ -72,7 +77,9 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier }, floatingActionButtonPosition = FabPosition.Center, floatingActionButton = { - Actions(state.actions) + Box(modifier = Modifier.navigationBarsPadding()) { + Actions(state.actions) + } }, containerColor = TangemTheme.colors.background.secondary, ) @@ -104,9 +111,11 @@ private fun TokenList( onDragEnd = onDragEnd, ) + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val listContentPadding = PaddingValues( top = TangemTheme.dimens.spacing4, - bottom = TangemTheme.dimens.spacing92, + bottom = TangemTheme.dimens.spacing92 + bottomBarHeight, start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, ) @@ -140,7 +149,7 @@ private fun TokenList( } } - BottomGradient(modifier = Modifier.align(Alignment.BottomCenter)) + BottomFade(modifier = Modifier.align(Alignment.BottomCenter)) } } @@ -191,23 +200,6 @@ private fun LazyItemScope.DraggableItem( } } -@Composable -private fun BottomGradient(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size116) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - TangemTheme.colors.background.secondary, - ), - ), - ), - ) -} - @Composable private fun TopBar( config: OrganizeTokensState.HeaderConfig, @@ -228,6 +220,7 @@ private fun TopBar( modifier = modifier .shadow(elevation) .background(TangemTheme.colors.background.secondary) + .statusBarsPadding() .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), ) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 2288a0c763..93f8267b5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,6 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus 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 6276cdb2e9..815f0493b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -1,12 +1,9 @@ package com.tangem.feature.wallet.presentation.router import android.annotation.SuppressLint -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalLifecycleOwner -import androidx.compose.ui.unit.dp -import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -16,27 +13,25 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController -import com.tangem.core.navigation.StateDialog +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.onboarding.navigation.OnboardingRouter import com.tangem.feature.wallet.presentation.WalletFragment import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel -import com.tangem.features.details.DetailsEntryPoint -import com.tangem.features.managetokens.navigation.ExpandableState -import com.tangem.features.managetokens.navigation.ManageTokensUi -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import kotlin.properties.Delegates /** Default implementation of wallet feature router */ internal class DefaultWalletRouter( - private val reduxNavController: ReduxNavController, + private val router: AppRouter, + private val urlOpener: UrlOpener, + private val reduxStateHolder: ReduxStateHolder, ) : InnerWalletRouter { private var navController: NavHostController by Delegates.notNull() @@ -45,7 +40,7 @@ internal class DefaultWalletRouter( override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(onFinish: () -> Unit, manageTokensUi: ManageTokensUi) { + override fun Initialize(onFinish: () -> Unit) { this.onFinish = onFinish NavHost( @@ -58,20 +53,8 @@ internal class DefaultWalletRouter( subscribeToLifecycle(LocalLifecycleOwner.current) } - var bottomSheetHeaderHeight by remember { mutableStateOf(0.dp) } - WalletScreen( state = viewModel.uiState.collectAsStateWithLifecycle().value, - bottomSheetHeaderHeightProvider = { bottomSheetHeaderHeight }, - bottomSheetContent = { - val state = remember { mutableStateOf(ExpandableState.COLLAPSED) } - // Manage Tokens - manageTokensUi.Content( - onHeaderSizeChange = { bottomSheetHeaderHeight = it }, - state = state, - ) - viewModel.setExpandableState(state) - }, ) } @@ -87,7 +70,6 @@ internal class DefaultWalletRouter( val uiState by viewModel.uiState.collectAsStateWithLifecycle() OrganizeTokensScreen( - modifier = Modifier.statusBarsPadding(), state = uiState, ) } @@ -95,7 +77,7 @@ internal class DefaultWalletRouter( } @SuppressLint("RestrictedApi") - override fun popBackStack(screen: AppScreen?) { + override fun popBackStack() { /* * It's hack that avoid issue with closing the wallet screen. * We are using NavGraph only inside feature so first backstack's element is entry of NavGraph and @@ -103,11 +85,7 @@ internal class DefaultWalletRouter( * If backstack contains only NavGraph entry and wallet screen entry then we close the wallet fragment. */ if (navController.currentBackStack.value.size == BACKSTACK_ENTRY_COUNT_TO_CLOSE_WALLET_SCREEN) { - if (screen != null) { - reduxNavController.navigate(action = NavigationAction.PopBackTo(screen)) - } else { - onFinish.invoke() - } + onFinish.invoke() } else { navController.popBackStack() } @@ -118,64 +96,53 @@ internal class DefaultWalletRouter( } override fun openDetailsScreen(selectedWalletId: UserWalletId) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.Details, - bundle = bundleOf( - DetailsEntryPoint.USER_WALLET_ID_KEY to selectedWalletId.stringValue, - ), + router.push( + AppRoute.Details( + userWalletId = selectedWalletId, ), ) } override fun openOnboardingScreen() { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.OnboardingWallet, - bundle = bundleOf(OnboardingRouter.CAN_SKIP_BACKUP to false), - ), + router.push( + AppRoute.OnboardingWallet(canSkipBackup = false), ) } override fun openUrl(url: String) { - reduxNavController.navigate(action = NavigationAction.OpenUrl(url)) + urlOpener.openUrl(url) } override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { val networkAddress = currencyStatus.value.networkAddress if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) { - reduxNavController.navigate( - action = NavigationAction.NavigateTo( - screen = AppScreen.WalletDetails, - bundle = bundleOf( - TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue, - TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currencyStatus.currency, - ), + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currencyStatus.currency, ), ) } } override fun openStoriesScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(screen = AppScreen.Home)) + router.push(AppRoute.Home) } override fun openSaveUserWalletScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.SaveWallet)) + router.push(AppRoute.SaveWallet) } - override fun isWalletLastScreen(): Boolean = reduxNavController.getBackStack().lastOrNull() == AppScreen.Wallet + override fun isWalletLastScreen(): Boolean { + return router.stack.lastOrNull() is AppRoute.Wallet + } override fun openManageTokensScreen() { - reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.ManageTokens)) + router.push(AppRoute.ManageTokens) } - override fun openScanFailedDialog() { - reduxNavController.navigate( - action = NavigationAction.OpenDialog( - StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN), - ), - ) + override fun openScanFailedDialog(onTryAgain: () -> Unit) { + reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) } private companion object { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index bf8aba542f..d213d227f8 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 @@ -2,10 +2,8 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable -import com.tangem.core.navigation.AppScreen import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.features.managetokens.navigation.ManageTokensUi import com.tangem.features.wallet.navigation.WalletRouter /** @@ -26,10 +24,10 @@ internal interface InnerWalletRouter : WalletRouter { */ @Suppress("TopLevelComposableFunctions") @Composable - fun Initialize(onFinish: () -> Unit, manageTokensUi: ManageTokensUi) + fun Initialize(onFinish: () -> Unit) /** Pop back stack */ - fun popBackStack(screen: AppScreen? = null) + fun popBackStack() /** Open organize tokens screen */ fun openOrganizeTokensScreen(userWalletId: UserWalletId) @@ -59,5 +57,5 @@ internal interface InnerWalletRouter : WalletRouter { fun openManageTokensScreen() /** Open scan failed dialog */ - fun openScanFailedDialog() + fun openScanFailedDialog(onTryAgain: () -> Unit) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 895c894ab0..713d704e94 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.state -import androidx.compose.runtime.mutableStateOf import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.wallets.models.UserWalletId @@ -11,8 +10,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarCon import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer -import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles -import com.tangem.features.managetokens.navigation.ExpandableState import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,9 +24,7 @@ import javax.inject.Singleton [REDACTED_AUTHOR] */ @Singleton -internal class WalletStateController @Inject constructor( - private val manageTokensFeatureToggles: ManageTokensFeatureToggles, -) { +internal class WalletStateController @Inject constructor() { val uiState: StateFlow get() = mutableUiState @@ -89,8 +84,6 @@ internal class WalletStateController @Inject constructor( onWalletChange = {}, event = consumedEvent(), isHidingMode = false, - manageTokenRedesignToggle = manageTokensFeatureToggles.isRedesignedScreenEnabled, - manageTokensExpandableState = mutableStateOf(ExpandableState.EXPANDED), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt new file mode 100644 index 0000000000..71638bc6ce --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/PushNotificationsBottomSheetConfig.kt @@ -0,0 +1,11 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +data class PushNotificationsBottomSheetConfig( + val isFirstTimeAsking: Boolean, + val onRequest: () -> Unit, + val onAllow: () -> Unit, + val onDeny: () -> Unit, + val openSettings: () -> Unit, +) : TangemBottomSheetConfigContent \ 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 d4feb6a295..d20706f5f8 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 @@ -2,9 +2,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable -import com.tangem.common.Strings import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.Strings /** Wallet card state */ @Immutable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt index afbd667405..19da0ca6a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt @@ -89,6 +89,19 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { ), ) + data class Stake( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + ) : WalletManageButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_stake), + iconResId = R.drawable.ic_arrow_down_24, // TODO staking + onClick = onClick, + dimContent = dimContent, + ), + ) + /** * Sell * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index 5c9424f9ee..9285575d9e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -1,18 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.state.model -import androidx.compose.runtime.MutableState import com.tangem.core.ui.event.StateEvent -import com.tangem.features.managetokens.navigation.ExpandableState import kotlinx.collections.immutable.ImmutableList internal data class WalletScreenState( val onBackClick: () -> Unit, - val manageTokensExpandableState: MutableState, val topBarConfig: WalletTopBarConfig, val selectedWalletIndex: Int, val wallets: ImmutableList, val onWalletChange: (Int) -> Unit, val event: StateEvent, val isHidingMode: Boolean, - val manageTokenRedesignToggle: Boolean, ) \ No newline at end of file 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 ea53efb4c6..d42d27f165 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 @@ -19,24 +19,25 @@ internal class SetBalancesAndLimitsTransformer( private val userWallet: UserWallet, private val maybeVisaCurrency: Either, private val clickIntents: WalletClickIntents, -) : WalletStateTransformer(userWallet.walletId) { +) : TypedWalletStateTransformer( + userWalletId = userWallet.walletId, + targetStateClass = WalletState.Visa.Content::class, +) { - override fun transform(prevState: WalletState): WalletState { - return prevState.transformWhenInState { state -> - val visaCurrency = maybeVisaCurrency.getOrElse { - return state.copy( - walletCardState = getErrorWalletCardState(state.walletCardState), - depositButtonState = state.depositButtonState.copy(isEnabled = false), - balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, - ) - } - - state.copy( - walletCardState = getContentWalletCardState(state.walletCardState, visaCurrency), - depositButtonState = state.depositButtonState.copy(isEnabled = true), - balancesAndLimitBlockState = getContentBlockState(visaCurrency), + override fun transformTyped(prevState: WalletState.Visa.Content): WalletState { + val visaCurrency = maybeVisaCurrency.getOrElse { + return prevState.copy( + walletCardState = getErrorWalletCardState(prevState.walletCardState), + depositButtonState = prevState.depositButtonState.copy(isEnabled = false), + balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } + + return prevState.copy( + walletCardState = getContentWalletCardState(prevState.walletCardState, visaCurrency), + depositButtonState = prevState.depositButtonState.copy(isEnabled = true), + balancesAndLimitBlockState = getContentBlockState(visaCurrency), + ) } private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 0f6d520acf..58ceb2ac53 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -64,6 +64,7 @@ internal class SetRefreshStateTransformer( is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button + is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt new file mode 100644 index 0000000000..2c72901066 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt @@ -0,0 +1,22 @@ +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.WalletState +import kotlin.reflect.KClass + +internal abstract class TypedWalletStateTransformer( + userWalletId: UserWalletId, + protected val targetStateClass: KClass, +) : WalletStateTransformer(userWalletId) { + + abstract fun transformTyped(prevState: S): WalletState + + @Suppress("UNCHECKED_CAST") + final override fun transform(prevState: WalletState): WalletState { + return if (prevState::class == targetStateClass) { + transformTyped(prevState as S) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt index f02bd16531..bcbd84cf28 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt @@ -4,7 +4,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber internal abstract class WalletStateTransformer( protected val userWalletId: UserWalletId, @@ -12,7 +11,7 @@ internal abstract class WalletStateTransformer( abstract fun transform(prevState: WalletState): WalletState - override fun transform(prevState: WalletScreenState): WalletScreenState { + final override fun transform(prevState: WalletScreenState): WalletScreenState { return prevState.copy( wallets = prevState.wallets .map { state -> @@ -21,13 +20,4 @@ internal abstract class WalletStateTransformer( .toImmutableList(), ) } - - protected inline fun WalletState.transformWhenInState( - transform: (state: S) -> WalletState, - ): WalletState = if (this is S) { - transform(this) - } else { - Timber.w("Impossible to transform ${this::class.simpleName} because current is ${S::class.simpleName}") - this - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index fa7478a428..417ff93ede 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -59,6 +59,11 @@ internal class MultiWalletCurrencyActionsConverter( icon = R.drawable.ic_arrow_down_24 action = { clickIntents.onReceiveClick(cryptoCurrencyStatus) } } + is TokenActionsState.ActionState.Stake -> { + title = resourceReference(R.string.common_stake) + icon = R.drawable.ic_arrow_down_24 // TODO staking replace icon + action = { clickIntents.onStakeClick(cryptoCurrencyStatus) } + } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) icon = R.drawable.ic_currency_24 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt index f41d4d4ee7..c16dc3e022 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenItemStateConverter.kt @@ -1,15 +1,17 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter -import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.wallet.presentation.common.state.TokenItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero import java.math.BigDecimal internal class TokenItemStateConverter( @@ -61,13 +63,16 @@ internal class TokenItemStateConverter( } private fun CryptoCurrencyStatus.getFormattedAmount(): String { - val amount = value.amount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val amount = value.amount?.plus(yieldBalance) ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) } private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { - val fiatAmount = value.fiatAmount ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN + val yieldBalance = (value.yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero() + val fiatYieldBalance = value.fiatRate?.times(yieldBalance).orZero() + val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return TokenItemState.UNKNOWN_AMOUNT_SIGN val appCurrency = appCurrencyProvider() return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index 5c1b0a20c5..819d0d20d1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -9,6 +9,7 @@ import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.VisaWalletIntents +import com.tangem.utils.Strings import com.tangem.utils.converter.Converter import org.joda.time.DateTimeZone @@ -20,7 +21,7 @@ internal class VisaTxHistoryItemStateConverter( override fun convert(value: VisaTxHistoryItem): TransactionState { val localDate = value.date.withZone(DateTimeZone.getDefault()) val time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter) - val subtitle = "$time • ${value.status.capitalize()}" + val subtitle = "$time ${Strings.DOT} ${value.status.capitalize()}" return TransactionState.Content( txHash = value.id, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index c930c63241..0de613caa1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -2,11 +2,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler -import androidx.compose.animation.core.TweenSpec -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -15,34 +10,27 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.clearAndSetSemantics 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 androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems -import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.tangem.core.ui.components.Keyboard +import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SystemBarsEffect -import com.tangem.core.ui.components.atoms.Hand -import com.tangem.core.ui.components.atoms.handComposableComponentHeight +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheetConfig -import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -50,10 +38,12 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags +import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder +import com.tangem.feature.wallet.presentation.wallet.ui.components.PushNotificationsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* @@ -65,16 +55,10 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.VisaTxDe import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balancesAndLimitsBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator -import com.tangem.features.managetokens.navigation.ExpandableState import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @Composable -internal fun WalletScreen( - state: WalletScreenState, - bottomSheetHeaderHeightProvider: () -> Dp, - bottomSheetContent: @Composable () -> Unit, -) { +internal fun WalletScreen(state: WalletScreenState) { BackHandler(onBack = state.onBackClick) // It means that screen is still initializing @@ -97,9 +81,6 @@ internal fun WalletScreen( snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, - bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, - bottomSheetContent = bottomSheetContent, - alertConfig = alertConfig, ) WalletEventEffect( @@ -112,17 +93,14 @@ internal fun WalletScreen( ) } -@Suppress("LongMethod", "LongParameterList") +@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod") @Composable private fun WalletContent( state: WalletScreenState, walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - bottomSheetHeaderHeightProvider: () -> Dp, onAutoScrollReset: () -> Unit, - bottomSheetContent: @Composable () -> Unit, - alertConfig: WalletAlertState?, ) { var selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } val selectedWallet = state.wallets.getOrElse(selectedWalletIndex) { state.wallets[state.selectedWalletIndex] } @@ -144,17 +122,20 @@ private fun WalletContent( .padding(top = betweenItemsPadding) .padding(horizontal = horizontalPadding) + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + LazyColumn( modifier = Modifier .fillMaxSize() .testTag(TestTags.WALLET_SCREEN), contentPadding = PaddingValues( - bottom = TangemTheme.dimens.spacing92, + bottom = TangemTheme.dimens.spacing92 + bottomBarHeight, ), horizontalAlignment = Alignment.CenterHorizontally, ) { item( - key = state.wallets.map { it.walletCardState.id }, + // !!! Type of the key should be saveable via Bundle on Android !!! + key = state.wallets.map { it.walletCardState.id.stringValue }, contentType = state.wallets.map { it.walletCardState.id }, ) { WalletsList( @@ -202,17 +183,7 @@ private fun WalletContent( organizeTokens(state = selectedWallet, itemModifier = itemModifier) } - val bottomSheetConfig = selectedWallet.bottomSheetConfig - if (bottomSheetConfig != null) { - when (bottomSheetConfig.content) { - is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) - is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig) - is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) - is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) - is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig) - is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig) - } - } + ShowBottomSheet(bottomSheetConfig = selectedWallet.bottomSheetConfig) WalletsListEffects( lazyListState = walletsListState, @@ -224,274 +195,12 @@ private fun WalletContent( ) } - if (state.manageTokenRedesignToggle) { - BaseScaffoldManageTokenRedesign( - state = state, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, - bottomSheetContent = bottomSheetContent, - alertConfig = alertConfig, - ) { - scaffoldContent() - } - } else { - BaseScaffold( - state = state, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - ) { - scaffoldContent() - } - } -} - -@Suppress("LongParameterList", "LongMethod") -@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) -@Composable -private fun BaseScaffoldManageTokenRedesign( - state: WalletScreenState, - selectedWallet: WalletState, - snackbarHostState: SnackbarHostState, - bottomSheetHeaderHeightProvider: () -> Dp, - bottomSheetContent: @Composable () -> Unit, - alertConfig: WalletAlertState?, - content: @Composable () -> Unit, -) { - // show the bottom sheet if there is at least one multicurrency wallet - val showManageTokensBottomSheet = remember(state.wallets) { - state.wallets.any { it is WalletState.MultiCurrency } - } - val bottomSheetState = rememberSheetStateEnhanced( - initialValue = if (showManageTokensBottomSheet) SheetValue.PartiallyExpanded else SheetValue.Hidden, - confirmValueChange = { sheetValue -> - when { - sheetValue == SheetValue.Hidden && showManageTokensBottomSheet -> false - sheetValue != SheetValue.Hidden && !showManageTokensBottomSheet -> false - else -> true - } - }, - skipHiddenState = showManageTokensBottomSheet, - ) - - val keyboardShown = keyboardAsState() - - BottomSheetStateEffects( - bottomSheetState = bottomSheetState, + BaseScaffold( state = state, - showManageTokensBottomSheet = showManageTokensBottomSheet, - alertConfig = alertConfig, - keyboardShown = keyboardShown, - ) - - val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = bottomSheetState, + selectedWallet = selectedWallet, snackbarHostState = snackbarHostState, - ) - - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() } - val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight - - val coroutineScope = rememberCoroutineScope() - - BottomSheetScaffold( - snackbarHost = { - WalletSnackbarHost( - snackbarHostState = it, - event = state.event, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), - ) - }, - containerColor = TangemTheme.colors.background.secondary, - sheetContainerColor = TangemTheme.colors.background.primary, - scaffoldState = scaffoldState, - sheetPeekHeight = peekHeight, - sheetDragHandle = { - Hand(modifier = Modifier.background(color = TangemTheme.colors.background.primary)) - }, - sheetContent = { - BoxWithConstraints { - Box( - modifier = Modifier - .sizeIn(maxHeight = maxHeight - statusBarHeight) - .align(Alignment.BottomCenter), - ) { - bottomSheetContent() - } - } - - // hide bottom sheet when back pressed - BackHandler( - keyboardShown.value is Keyboard.Closed && - bottomSheetState.currentValue == SheetValue.Expanded, - ) { - coroutineScope.launch { bottomSheetState.partialExpand() } - } - }, - content = { paddingValues -> - val pullRefreshState = rememberPullRefreshState( - refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = { - selectedWallet.pullToRefreshConfig.onRefresh(WalletPullToRefreshConfig.ShowRefreshState(true)) - }, - ) - - Column( - modifier = Modifier.padding(paddingValues), - ) { - WalletTopBar(config = state.topBarConfig) - Box( - modifier = Modifier.pullRefresh(pullRefreshState), - ) { - content() - - WalletPullToRefreshIndicator( - isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) - } - } - - BottomSheetScrim( - color = BottomSheetDefaults.ScrimColor, - visible = bottomSheetState.targetValue == SheetValue.Expanded, - onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } }, - ) - }, - ) -} - -@Composable -private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { - val alpha by animateFloatAsState( - targetValue = if (visible) 1f else 0f, - animationSpec = TweenSpec(), - label = "scrim", - ) - val dismissSheet = if (visible) { - Modifier - .pointerInput(onDismissRequest) { - detectTapGestures { - onDismissRequest() - } - } - .clearAndSetSemantics {} - } else { - Modifier - } - Canvas( - Modifier - .fillMaxSize() - .then(dismissSheet), ) { - drawRect(color = color, alpha = alpha) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Suppress("CyclomaticComplexMethod") -@Composable -private fun BottomSheetStateEffects( - bottomSheetState: SheetState, - state: WalletScreenState, - showManageTokensBottomSheet: Boolean, - alertConfig: WalletAlertState?, - keyboardShown: State, -) { - // Bottom sheet during initialization internally expand partially after its content was remeasured, - // therefore initialValue = SheetValue.Hidden in rememberStandardBottomSheetState doesn't work as expected - // so we have to manually restrict expansion in this case - LaunchedEffect(bottomSheetState.targetValue, bottomSheetState.currentValue) { - if (!showManageTokensBottomSheet && - (bottomSheetState.targetValue != SheetValue.Hidden || bottomSheetState.currentValue != SheetValue.Hidden) - ) { - bottomSheetState.hide() - } - } - // react to changes in wallet list - LaunchedEffect(showManageTokensBottomSheet) { - when { - showManageTokensBottomSheet && bottomSheetState.currentValue != SheetValue.PartiallyExpanded -> { - bottomSheetState.partialExpand() - } - !showManageTokensBottomSheet && bottomSheetState.targetValue != SheetValue.Hidden -> { - bottomSheetState.hide() - } - } - } - - val systemUiController = rememberSystemUiController() - val navigationBarColor = TangemTheme.colors.background.primary - val navigationBarColorWithout = TangemTheme.colors.background.secondary - - SystemBarsEffect { - if (showManageTokensBottomSheet) { - setNavigationBarColor(navigationBarColor) - } - } - DisposableEffect( - showManageTokensBottomSheet, - ) { - onDispose { - if (showManageTokensBottomSheet) { - systemUiController.setNavigationBarColor(navigationBarColorWithout) - } - } - } - - // expand bottom sheet when keyboard appears - LaunchedEffect(keyboardShown.value is Keyboard.Opened) { - if (keyboardShown.value is Keyboard.Opened && alertConfig == null) { - bottomSheetState.expand() - } - } - - val keyboardController = LocalSoftwareKeyboardController.current - // hide keyboard when bottom sheet is about to be hidden - LaunchedEffect(Unit) { - snapshotFlow { - bottomSheetState.currentValue == SheetValue.Expanded && - bottomSheetState.targetValue == SheetValue.PartiallyExpanded - }.collect { sheetHasBeenHidden -> - if (sheetHasBeenHidden) { - keyboardController?.hide() - } - } - } - - val isSheetHidden = bottomSheetState.targetValue == SheetValue.PartiallyExpanded - LaunchedEffect(isSheetHidden) { - if (isSheetHidden) { - state.manageTokensExpandableState.value = ExpandableState.COLLAPSED - } else { - state.manageTokensExpandableState.value = ExpandableState.EXPANDED - } - } -} - -/** - * Use a standard method when this is fixed https://issuetracker.google.com/issues/314796718 - * Current material3 version: 1.2.0 - */ -@Composable -@ExperimentalMaterial3Api -private fun rememberSheetStateEnhanced( - skipPartiallyExpanded: Boolean = false, - confirmValueChange: (SheetValue) -> Boolean = { true }, - initialValue: SheetValue = SheetValue.Hidden, - skipHiddenState: Boolean = false, -): SheetState { - val density = LocalDensity.current - return remember(initialValue, skipPartiallyExpanded, confirmValueChange, skipHiddenState) { - SheetState( - skipPartiallyExpanded = skipPartiallyExpanded, - density = density, - initialValue = initialValue, - confirmValueChange = confirmValueChange, - skipHiddenState = skipHiddenState, - ) + scaffoldContent() } } @@ -505,6 +214,7 @@ private fun BaseScaffold( ) { Scaffold( topBar = { WalletTopBar(config = state.topBarConfig) }, + contentWindowInsets = WindowInsetsZero, snackbarHost = { WalletSnackbarHost( snackbarHostState = snackbarHostState, @@ -519,7 +229,12 @@ private fun BaseScaffold( ) } - manageTokensButtonConfig?.let { ManageTokensButton(onClick = it.onClick) } + manageTokensButtonConfig?.let { + ManageTokensButton( + modifier = Modifier.navigationBarsPadding(), + onClick = it.onClick, + ) + } }, floatingActionButtonPosition = FabPosition.Center, containerColor = TangemTheme.colors.background.secondary, @@ -543,6 +258,8 @@ private fun BaseScaffold( state = pullRefreshState, modifier = Modifier.align(Alignment.TopCenter), ) + + BottomFade(Modifier.align(Alignment.BottomCenter)) } }, ) @@ -564,11 +281,11 @@ private fun WalletSnackbarHost( } @Composable -private fun ManageTokensButton(onClick: () -> Unit) { +private fun ManageTokensButton(onClick: () -> Unit, modifier: Modifier = Modifier) { PrimaryButton( text = stringResource(id = R.string.main_manage_tokens), onClick = onClick, - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing16), ) @@ -588,6 +305,21 @@ internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modi } } +@Composable +private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { + if (bottomSheetConfig != null) { + when (bottomSheetConfig.content) { + is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) + is TokenReceiveBottomSheetConfig -> TokenReceiveBottomSheet(config = bottomSheetConfig) + is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) + is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) + is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig) + is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig) + is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig) + } + } +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -596,8 +328,6 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, - bottomSheetHeaderHeightProvider = { 0.dp }, - bottomSheetContent = {}, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt new file mode 100644 index 0000000000..15035df238 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/PushNotificationsBottomSheet.kt @@ -0,0 +1,119 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.showcase.ShowcaseContent +import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.requestPushPermission +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig +import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun PushNotificationsBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet(config = config) { + PushNotificationsSheetContent(content = it, onDismiss = config.onDismissRequest) + } +} + +@Composable +private fun PushNotificationsSheetContent(content: PushNotificationsBottomSheetConfig, onDismiss: () -> Unit) { + val isClicked = remember { mutableStateOf(false) } + val requestPushPermission = requestPushPermission( + pushPermission = getPushPermissionOrNull(), + isFirstTimeAsking = content.isFirstTimeAsking, + isClicked = isClicked, + onAllow = { + content.onAllow() + onDismiss() + }, + onDeny = { + content.onDeny() + onDismiss() + }, + onOpenSettings = content.openSettings, + ) + + Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { + ShowcaseContent( + headerIconRes = R.drawable.ic_notifications_unread_24, + headerText = resourceReference(R.string.user_push_notification_agreement_header), + showcaseItems = persistentListOf( + ShowcaseItemModel( + iconRes = R.drawable.ic_rocket_launch_24, + text = resourceReference(R.string.user_push_notification_agreement_argument_one), + ), + ShowcaseItemModel( + iconRes = R.drawable.ic_storefront_24, + text = resourceReference(R.string.user_push_notification_agreement_argument_two), + ), + ), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing40), + ) + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing40, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + SecondaryButton( + text = stringResource(R.string.common_later), + onClick = { + content.onDeny() + onDismiss() + }, + modifier = Modifier.weight(1f), + ) + PrimaryButton( + text = stringResource(R.string.common_allow), + onClick = { + isClicked.value = true + content.onRequest() + requestPushPermission() + }, + modifier = Modifier.weight(1f), + ) + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PushNotificationsSheetContent_Preview() { + TangemThemePreview { + PushNotificationsSheetContent( + PushNotificationsBottomSheetConfig( + isFirstTimeAsking = false, + onRequest = {}, + onAllow = {}, + onDeny = {}, + openSettings = {}, + ), + onDismiss = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 3962d07d47..7cc578f380 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -42,7 +42,6 @@ import androidx.compose.ui.unit.sp import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope import androidx.constraintlayout.compose.Dimension -import com.tangem.common.Strings import com.tangem.core.ui.components.FontSizeRange import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.ResizableText @@ -54,6 +53,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState +import com.tangem.utils.Strings private const val HALF_OF_ITEM_WIDTH = 0.5 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 713453127a..b88fbcf1f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -1,16 +1,14 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels -import androidx.compose.runtime.MutableState import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.navigation.settings.SettingsManager import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.settings.CanUseBiometryUseCase -import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled -import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase +import com.tangem.domain.settings.* import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -21,6 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWal import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.PushNotificationsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState @@ -28,7 +27,8 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents -import com.tangem.features.managetokens.navigation.ExpandableState +import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder @@ -56,13 +56,18 @@ internal class WalletViewModel @Inject constructor( private val canUseBiometryUseCase: CanUseBiometryUseCase, private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - analyticsEventsHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, private val screenLifecycleProvider: ScreenLifecycleProvider, private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, + private val shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase, + private val isFirstTimeAskingPermissionUseCase: IsFirstTimeAskingPermissionUseCase, + private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, + private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles, + private val settingsManager: SettingsManager, + analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState @@ -82,6 +87,7 @@ internal class WalletViewModel @Inject constructor( subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() subscribeToScreenBackgroundState() + subscribeOnPushNotificationsPermission() } private fun maybeMigrateNames() { @@ -95,12 +101,6 @@ internal class WalletViewModel @Inject constructor( clickIntents.initialize(router, viewModelScope) } - fun setExpandableState(state: MutableState) { - stateHolder.update { - it.copy(manageTokensExpandableState = state) - } - } - fun subscribeToLifecycle(lifecycleOwner: LifecycleOwner) { lifecycleOwner.lifecycle.addObserver(screenLifecycleProvider) } @@ -147,6 +147,33 @@ internal class WalletViewModel @Inject constructor( .launchIn(viewModelScope) } + private fun subscribeOnPushNotificationsPermission() { + viewModelScope.launch { + val isPushToggled = pushNotificationsFeatureToggles.isPushNotificationsEnabled + val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION) + if (!isPushToggled || !shouldRequestPush) return@launch + + delay(timeMillis = 1_800) + + val isFirstTimeAsking = isFirstTimeAskingPermissionUseCase(PUSH_PERMISSION).getOrElse { true } + val wasInitiallyAsk = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { true } + val onDenyClick: () -> Unit = if (wasInitiallyAsk) { + clickIntents::onDelayAskPushPermission + } else { + clickIntents::onNeverAskPushPermission + } + stateHolder.showBottomSheet( + PushNotificationsBottomSheetConfig( + isFirstTimeAsking = isFirstTimeAsking, + onRequest = clickIntents::onRequestPushPermission, + onAllow = clickIntents::onNeverAskPushPermission, + onDeny = onDenyClick, + openSettings = settingsManager::openSettings, + ), + ) + } + } + private fun subscribeOnSelectedWalletFlow() { getSelectedWalletUseCase().onRight { it diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 5b4bf78f5d..7c128ca00d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -1,21 +1,16 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.wallets.usecase.GetWalletNamesUseCase -import com.tangem.domain.wallets.usecase.RenameWalletUseCase -import com.tangem.feature.wallet.impl.R import com.tangem.domain.card.DeleteSavedAccessCodesUseCase -import com.tangem.core.navigation.AppScreen -import com.tangem.core.navigation.NavigationAction -import com.tangem.core.navigation.ReduxNavController import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.DeleteWalletUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.wallets.usecase.* +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -52,7 +47,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val reduxStateHolder: ReduxStateHolder, - private val reduxNavController: ReduxNavController, + private val appRouter: AppRouter, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents { @@ -123,7 +118,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( reduxStateHolder.onUserWalletSelected(selectedWallet) } else { stateHolder.clear() - reduxNavController.navigate(NavigationAction.PopBackTo(AppScreen.Home)) + appRouter.replaceAll(AppRoute.Home) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index eba25e8d34..ec3f2d992d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -32,6 +32,7 @@ internal class WalletClickIntents @Inject constructor( private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, private val visaWalletIntentsImplementor: VisaWalletIntentsImplementor, + private val pushPermissionClickIntentsImplementor: WalletPushPermissionClickIntentsImplementor, private val stateHolder: WalletStateController, private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, @@ -48,7 +49,8 @@ internal class WalletClickIntents @Inject constructor( WalletWarningsClickIntents by warningsClickIntentsImplementer, WalletCurrencyActionsClickIntents by currencyActionsClickIntentsImplementor, WalletContentClickIntents by contentClickIntentsImplementor, - VisaWalletIntents by visaWalletIntentsImplementor { + VisaWalletIntents by visaWalletIntentsImplementor, + WalletPushPermissionClickIntents by pushPermissionClickIntentsImplementor { override fun initialize(router: InnerWalletRouter, coroutineScope: CoroutineScope) { super.initialize(router, coroutineScope) @@ -58,6 +60,7 @@ internal class WalletClickIntents @Inject constructor( currencyActionsClickIntentsImplementor.initialize(router, coroutineScope) contentClickIntentsImplementor.initialize(router, coroutineScope) visaWalletIntentsImplementor.initialize(router, coroutineScope) + pushPermissionClickIntentsImplementor.initialize(router, coroutineScope) } fun onWalletChange(index: Int) { 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 bc843df1e7..55f1538573 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 @@ -44,6 +44,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject interface WalletCurrencyActionsClickIntents { @@ -58,6 +59,8 @@ interface WalletCurrencyActionsClickIntents { fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? fun onCopyAddressClick(cryptoCurrencyStatus: CryptoCurrencyStatus) @@ -364,6 +367,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse(action = ::openExplorer) } + override fun onStakeClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { + // TODO staking + Timber.e("Not implemented yet") + } + private fun openExplorer() { val userWalletId = stateHolder.getSelectedWalletId() @@ -466,6 +474,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun getUnavailabilityReasonText(unavailabilityReason: ScenarioUnavailabilityReason): TextReference { return when (unavailabilityReason) { + is ScenarioUnavailabilityReason.StakingUnavailable -> { + resourceReference( + id = R.string.token_button_unavailability_reason_staking_unavailable, + formatArgs = wrappedList(unavailabilityReason.cryptoCurrencyName), + ) + } is ScenarioUnavailabilityReason.PendingTransaction -> { when (unavailabilityReason.withdrawalScenario) { ScenarioUnavailabilityReason.WithdrawalScenario.SEND -> resourceReference( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt new file mode 100644 index 0000000000..d2f05b0f2a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletPushPermissionClickIntents.kt @@ -0,0 +1,44 @@ +package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents + +import com.tangem.domain.settings.DelayPermissionRequestUseCase +import com.tangem.domain.settings.NeverRequestPermissionUseCase +import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.launch +import javax.inject.Inject + +internal interface WalletPushPermissionClickIntents { + + fun onRequestPushPermission() + + fun onDelayAskPushPermission() + + fun onNeverAskPushPermission() +} + +@ViewModelScoped +internal class WalletPushPermissionClickIntentsImplementor @Inject constructor( + private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase, + private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, + private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase, +) : BaseWalletClickIntents(), WalletPushPermissionClickIntents { + + override fun onRequestPushPermission() { + viewModelScope.launch { + setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION) + } + } + + override fun onDelayAskPushPermission() { + viewModelScope.launch { + delayPermissionRequestUseCase(PUSH_PERMISSION) + } + } + + override fun onNeverAskPushPermission() { + viewModelScope.launch { + neverRequestPermissionUseCase(PUSH_PERMISSION) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index d2af11e81b..e90a55faac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -165,7 +165,10 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onScanToUnlockWalletClick() { analyticsEventHandler.send(MainScreen.UnlockWithCardScan) + openScanCardDialog() + } + private fun openScanCardDialog() { viewModelScope.launch(dispatchers.main) { scanCardToUnlockWalletClickHandler(walletId = stateHolder.getSelectedWalletId()) .onLeft { error -> @@ -175,7 +178,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( event = WalletEvent.ShowAlert(WalletAlertState.WrongCardIsScanned), ) } - ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog() + ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog(::openScanCardDialog) } } } @@ -201,7 +204,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( viewModelScope.launch(dispatchers.main) { neverToSuggestRateAppUseCase() - reduxStateHolder.dispatch(LegacyAction.SendEmailRateCanBeBetter) + reduxStateHolder.dispatch( + LegacyAction.SendEmailRateCanBeBetter( + scanResponse = getSelectedUserWallet()?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) } } @@ -254,7 +262,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - private suspend fun getSelectedUserWallet(): UserWallet? { + private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { Timber.e( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e73b861d62..4742ffb2c9 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -4,8 +4,8 @@ [versions] # region Classpath androidGradlePlugin = "8.2.2" -firebaseCrashlytics = "2.9.4" -googleServices = "4.3.10" +firebaseCrashlytics = "3.0.1" +googleServices = "4.4.1" kotlin = "1.9.22" # endregion Classpath @@ -21,6 +21,7 @@ androidxLifecycle = "2.5.1" androidx-paging = "3.1.1" androidx-palette = "1.0.0" androidx-datastore = "1.0.0" +androidxWindowManager = "1.3.0" # endregion AndroidX # region Compose @@ -44,7 +45,7 @@ coil = "2.1.0" compose-shimmer = "1.0.3" coroutine = "1.7.2" desugarJdkLibs = "1.1.5" -firebase = "26.0.0" +firebase = "33.1.0" googleMaterialComponent = "1.6.1" googlePlayCore = "1.10.3" googlePlayCoreKtx = "1.8.1" @@ -87,10 +88,12 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.12-698" +tangemBlockchainSdk = "develop-699" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.12-369" +tangemCardSdk = "develop-370" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ +tangemVico = "2.0.0-alpha.21-tangem14" +#tangemVico = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem # region Tools @@ -130,7 +133,6 @@ room = { id = "androidx.room", version.ref = "room" } gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } -gradle-firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-gradle", version.ref = "firebaseCrashlytics" } # end region Classpath # region AndroidX @@ -144,6 +146,7 @@ androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref androidx-paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "androidx-paging" } androidx-swipeRefreshLayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swipeRefreshLayout" } androidx-palette = { module = "androidx.palette:palette", version.ref = "androidx-palette" } +androidx-windowManager = { group = "androidx.window", name = "window", version.ref = "androidxWindowManager" } lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-common-java8", version.ref = "androidxLifecycle" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" } lifecycle-viewModel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } @@ -167,6 +170,7 @@ compose-navigation-hilt = { module = "androidx.hilt:hilt-navigation-compose", ve compose-accompanist-appCompatTheme = { module = "com.google.accompanist:accompanist-appcompat-theme", version.ref = "compose-accompanist" } compose-accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "compose-accompanist" } compose-accompanist-webView = { module = "com.google.accompanist:accompanist-webview", version.ref = "compose-accompanist" } +compose-accompanist-permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "compose-accompanist" } compose-paging = { module = "androidx.paging:paging-compose", version.ref = "compose-paging" } compose-reorderable = { module = "org.burnoutcrew.composereorderable:reorderable", version.ref = "compose-reorderable" } # endregion Compose @@ -175,12 +179,17 @@ compose-reorderable = { module = "org.burnoutcrew.composereorderable:reorderable firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase" } firebase-analytics = { module = "com.google.firebase:firebase-analytics-ktx" } firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx" } # endregion Firebase # region Tangem tangem-blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } tangem-card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } tangem-card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } + +tangem-vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } +tangem-vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } +tangem-vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } # endregion Tangem # region Detekt @@ -210,6 +219,7 @@ armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } kotlin-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "coroutine" } +kotlin-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" } kotlin-immutable-collections = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlin-immutable-collections" } desugar = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugarJdkLibs" } googlePlay-core = { module = "com.google.android.play:core", version.ref = "googlePlayCore" } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt index 5d713313ea..f8e769ebd1 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainSDKConfigConverter.kt @@ -41,6 +41,7 @@ internal object BlockchainSDKConfigConverter : Converter ProviderType.Tron.TronGrid "dwellirBittensor" -> ProviderType.Bittensor.Dwellir "onfinalityBittensor" -> ProviderType.Bittensor.Onfinality + "koinospro" -> ProviderType.Koinos.KoinosPro else -> { Timber.e("Private provider with name $name is not supported") null diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 6b69d96fb8..f114465769 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -1,35 +1,10 @@ package com.tangem.lib.crypto -import com.tangem.blockchain.common.TransactionExtras import com.tangem.lib.crypto.models.* -import com.tangem.lib.crypto.models.transactions.SendTxResult import java.math.BigDecimal interface TransactionManager { - @Throws(IllegalStateException::class) - suspend fun sendApproveTransaction( - txData: ApproveTxData, - derivationPath: String?, - analyticsData: AnalyticsData, - ): SendTxResult - - /** - * Send transaction - * - * @param txData data to build a tx - * @param derivationPath for select right walletManager - * @param analyticsData data for send analytics event - * @return result of transaction - */ - @Throws(IllegalStateException::class) - suspend fun sendTransaction( - txData: SwapTxData, - isSwap: Boolean, - derivationPath: String?, - analyticsData: AnalyticsData, - ): SendTxResult - /** * Get fee * @@ -57,8 +32,6 @@ interface TransactionManager { @Throws(IllegalStateException::class) suspend fun updateWalletManager(networkId: String, derivationPath: String?) - fun calculateFee(networkId: String, gasPrice: String, estimatedGas: Int): BigDecimal - /** * In app blockchain id, actual in blockchain sdk, not the same as networkId * @@ -69,7 +42,4 @@ interface TransactionManager { @Throws(IllegalStateException::class) fun getExplorerTransactionLink(networkId: String, txAddress: String): String - - // TODO: move to another place to use as in Send feature - fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? } \ No newline at end of file diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt index 7ae3b7e0dc..4696b698fb 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/VisaContractInfoProvider.kt @@ -3,8 +3,8 @@ package com.tangem.lib.visa import com.ihsanbal.logging.Level import com.ihsanbal.logging.LoggingInterceptor import com.tangem.lib.visa.model.VisaContractInfo -import com.tangem.lib.visa.utils.VisaConfig -import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG +import com.tangem.lib.visa.utils.Constants +import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG import com.tangem.lib.visa.utils.toHexString import com.tangem.utils.coroutines.CoroutineDispatcherProvider import okhttp3.OkHttpClient @@ -24,16 +24,16 @@ interface VisaContractInfoProvider { suspend fun getContractInfo(walletAddress: String): VisaContractInfo class Builder( + private val useTestnetRpc: Boolean, + private val bridgeProcessorAddress: String, + private val paymentAccountRegistryAddress: String, private val isNetworkLoggingEnabled: Boolean, private val dispatchers: CoroutineDispatcherProvider, - private val baseUrl: String = VisaConfig.BASE_RPC_URL, - private val bridgeProcessorAddress: String = VisaConfig.BRIDGE_PROCESSOR_CONTRACT_ADDRESS, - private val paymentAccountRegistryAddress: String = VisaConfig.PAYMENT_ACCOUNT_REGISTRY_ADDRESS, - private val chainId: Long = VisaConfig.CHAIN_ID, - private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS, - private val decimals: Int = VisaConfig.DECIMALS, - private val gasLimit: Long = VisaConfig.GAS_LIMIT, - private val privateKey: String = ByteArray(VisaConfig.PRIVATE_KEY_LENGTH).toHexString(), + private val chainId: Long = Constants.CHAIN_ID, + private val decimals: Int = Constants.DECIMALS, + private val gasLimit: Long = Constants.GAS_LIMIT, + private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS, + private val privateKey: String = ByteArray(Constants.PRIVATE_KEY_LENGTH).toHexString(), ) { fun build(): VisaContractInfoProvider { @@ -52,6 +52,8 @@ interface VisaContractInfoProvider { } private fun createWeb3J(): Web3j { + val baseUrl: String = if (useTestnetRpc) Constants.TESTNET_RPC_URL else Constants.MAINNET_RPC_URL + val httpClient = OkHttpClient.Builder().apply { connectTimeout(networkTimeoutSeconds, TimeUnit.SECONDS) readTimeout(networkTimeoutSeconds, TimeUnit.SECONDS) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt index 50f200249b..ef1a99dd5f 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/api/VisaApiBuilder.kt @@ -5,8 +5,8 @@ import com.ihsanbal.logging.Level import com.ihsanbal.logging.LoggingInterceptor import com.squareup.moshi.Moshi import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory -import com.tangem.lib.visa.utils.VisaConfig -import com.tangem.lib.visa.utils.VisaConfig.NETWORK_LOGS_TAG +import com.tangem.lib.visa.utils.Constants +import com.tangem.lib.visa.utils.Constants.NETWORK_LOGS_TAG import okhttp3.Interceptor import okhttp3.OkHttpClient import retrofit2.Retrofit @@ -17,7 +17,8 @@ class VisaApiBuilder( private val useDevApi: Boolean, private val isNetworkLoggingEnabled: Boolean, private val moshi: Moshi, - private val networkTimeoutSeconds: Long = VisaConfig.NETWORK_TIMEOUT_SECONDS, + private val headers: Map, + private val networkTimeoutSeconds: Long = Constants.NETWORK_TIMEOUT_SECONDS, ) { fun build(): VisaApi { @@ -35,13 +36,23 @@ class VisaApiBuilder( if (isNetworkLoggingEnabled) { addInterceptor(createNetworkLoggingInterceptor()) } + + if (headers.isNotEmpty()) { + addInterceptor { chain -> + val request = chain.request().newBuilder().apply { + headers.forEach { (key, value) -> addHeader(key, value) } + }.build() + + chain.proceed(request) + } + } } return builder.build() } private fun createRetrofit(okHttpClient: OkHttpClient): Retrofit { - val baseUrl = if (useDevApi) VisaConfig.VISA_API_DEV_URL else VisaConfig.VISA_API_PROD_URL + val baseUrl = if (useDevApi) Constants.VISA_API_DEV_URL else Constants.VISA_API_PROD_URL return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt similarity index 55% rename from libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt rename to libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt index e3d0a9dab5..06e365e4ec 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/VisaConfig.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt @@ -1,10 +1,10 @@ package com.tangem.lib.visa.utils -internal object VisaConfig { +internal object Constants { + + const val MAINNET_RPC_URL = "https://polygon-rpc.com/" + const val TESTNET_RPC_URL = "https://rpc-amoy.polygon.technology/" - const val BASE_RPC_URL = "https://polygon-mumbai.g.alchemy.com/v2/_1qqjXgBC_IikaXChnna8KTcV2eMMIQG/" - const val BRIDGE_PROCESSOR_CONTRACT_ADDRESS = "0xe32ecbbc1ec17fa9c160569cd613ad568ca50279" - const val PAYMENT_ACCOUNT_REGISTRY_ADDRESS = "0x3f4ae01073d1a9d5a92315fe118e57d1cdec7c44" const val CHAIN_ID = 80_001L const val DECIMALS = 9 const val GAS_LIMIT = 500_000_000L diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt index c47360c0fc..1612e170ed 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt @@ -20,10 +20,12 @@ internal fun BaseExtension.configureCompilerOptions() { internal fun BaseExtension.configureCompose(project: Project) { val useCompose = with(project.path) { contains(":ui") || + contains(":common:ui-charts") || contains(":features:onboarding") || // TODO: divide on api/impl after migrating all onboarding to module contains(Regex(pattern = ":presentation\$")) || contains(Regex(pattern = ":app\$")) || // TODO: [REDACTED_JIRA] contains(Regex(pattern = ":features:manage-tokens:api\$")) || // provides Composable function + contains(Regex(pattern = ":features:markets:api\$")) || // provides Composable function contains(Regex(pattern = ":impl\$")) } diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt index 053151f9f1..f82cfcf71e 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/LibraryExtensionConfigurations.kt @@ -39,7 +39,7 @@ private fun LibraryExtension.configureBuildTypes() { } private fun LibraryExtension.configurePackagingOptions() { - packagingOptions { + packaging { resources { excludes += "lib/x86_64/darwin/libscrypt.dylib" excludes += "lib/x86_64/freebsd/libscrypt.so" diff --git a/settings.gradle.kts b/settings.gradle.kts index 9f9643c29f..0d6be44673 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,6 +35,7 @@ dependencyResolutionManagement { mavenLocal { content { includeGroupAndSubgroups("com.tangem.tangem-sdk-kotlin") + includeGroupAndSubgroups("com.tangem.vico") includeModule("com.tangem", "blstlib") includeModule("com.tangem", "blockchain") includeModule("com.tangem", "wallet-core-proto") @@ -80,6 +81,17 @@ dependencyResolutionManagement { includeModule("com.tangem", "wallet-core") } } + maven { + // setting any repository from tangem project allows maven search all packages in the project + url = uri("https://maven.pkg.github.com/tangem/vico") + credentials { + username = properties.getProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR") + password = properties.getProperty("gpr.key") ?: System.getenv("GITHUB_TOKEN") + } + content { + includeGroupAndSubgroups("com.tangem.vico") + } + } jcenter { // unable to replace with mavenCentral() due to rekotlin content { includeModule("org.rekotlin", "rekotlin") @@ -100,6 +112,8 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") include(":app") include(":common") +include(":common:ui-charts") +include(":common:routing") // region Core modules include(":core:analytics") @@ -113,8 +127,13 @@ include(":core:utils") include(":core:deep-links") include(":core:deep-links:global") include(":core:decompose") +include(":core:pagination") // endregion Core modules +// region Common modules +include(":common:ui") +// endregion + // region Libs modules include(":libs:auth") include(":libs:blockchain-sdk") @@ -159,6 +178,18 @@ include(":features:staking:impl") include(":features:details:api") include(":features:details:impl") + +include(":features:disclaimer:api") +include(":features:disclaimer:impl") + +include(":features:push-notifications:api") +include(":features:push-notifications:impl") + +include(":features:wallet-settings:api") +include(":features:wallet-settings:impl") + +include(":features:markets:api") +include(":features:markets:impl") // endregion Feature modules // region Domain modules @@ -191,7 +222,10 @@ include(":domain:feedback") include(":domain:qr-scanning") include(":domain:qr-scanning:models") include(":domain:staking") +include(":domain:staking:models") include(":domain:wallet-connect") +include(":domain:markets") +include(":domain:markets:models") // endregion Domain modules // region Data modules @@ -213,4 +247,5 @@ include(":data:feedback") include(":data:qr-scanning") include(":data:staking") include(":data:wallet-connect") +include(":data:markets") // endregion Data modules \ No newline at end of file diff --git a/version.properties b/version.properties new file mode 100644 index 0000000000..7ef818eaa8 --- /dev/null +++ b/version.properties @@ -0,0 +1 @@ +versionName=5.13 \ No newline at end of file