diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 18fd29dc28..bbe71b61be 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 {
@@ -69,8 +74,10 @@ dependencies {
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.staking)
implementation(projects.domain.walletConnect)
+ implementation(projects.domain.markets)
implementation(projects.common)
+ implementation(projects.common.routing)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
@@ -103,6 +110,7 @@ dependencies {
implementation(projects.data.qrScanning)
implementation(projects.data.staking)
implementation(projects.data.walletConnect)
+ implementation(projects.data.markets)
/** Features */
implementation(projects.features.onboarding)
@@ -130,6 +138,14 @@ 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)
+ implementation(projects.features.markets.api)
+ implementation(projects.features.markets.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
@@ -166,6 +182,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 2754eb4547..aed18d1c66 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -41,11 +41,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">
@@ -147,5 +147,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 87b2bf414d..fd2967ed70 100644
--- a/app/src/main/java/com/tangem/tap/MainActivity.kt
+++ b/app/src/main/java/com/tangem/tap/MainActivity.kt
@@ -1,37 +1,38 @@
package com.tangem.tap
-import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.ActivityInfo
-import android.content.pm.PackageManager
import android.content.res.Configuration
-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
@@ -40,15 +41,18 @@ 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.staking.SendUnsubmittedHashesUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
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
@@ -59,6 +63,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
@@ -68,17 +75,18 @@ 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 timber.log.Timber
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -125,9 +133,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var tokenDetailsRouter: TokenDetailsRouter
- @Inject
- lateinit var manageTokensUi: ManageTokensUi
-
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@@ -143,6 +148,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var settingsRepository: SettingsRepository
+ @Inject
+ lateinit var sendUnsubmittedHashesUseCase: SendUnsubmittedHashesUseCase
+
@Inject
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
@@ -158,6 +166,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
@@ -177,6 +207,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 }
@@ -185,17 +222,43 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
observeAppThemeModeUpdates()
setContentView(R.layout.activity_main)
+ installRouting()
initContent()
- checkForNotificationPermission()
observeStateUpdates()
observePolkadotAccountHealthCheck()
+ sendStakingUnsubmittedHashes()
if (intent != null) {
deepLinksRegistry.launch(intent)
}
}
+ 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)
@@ -217,8 +280,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun installActivityDependencies() {
- store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
-
cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager
appStateHolder.tangemSdkManager = tangemSdkManager
@@ -238,11 +299,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,
),
)
}
@@ -263,8 +325,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@SuppressLint("SourceLockedOrientationActivity")
private fun initContent() {
- WindowCompat.setDecorFitsSystemWindows(window, false)
-
supportFragmentManager.registerFragmentLifecycleCallbacks(
NavBarInsetsFragmentLifecycleCallback(),
true,
@@ -308,7 +368,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
override fun onDestroy() {
- store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this)))
intentProcessor.removeAll()
super.onDestroy()
}
@@ -448,8 +507,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
@@ -466,17 +525,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)
}
}
@@ -484,16 +542,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()
@@ -514,4 +562,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
}
+
+ private fun sendStakingUnsubmittedHashes() {
+ lifecycleScope.launch {
+ sendUnsubmittedHashesUseCase.invoke()
+ .onRight { Timber.d("Submitting hashes succeeded") }
+ .onLeft { Timber.e(it.toString()) }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt
index 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 01625dc4f2..829fdd5601 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..5d7a14709f 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,60 @@
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) {
+ if (backStackEntryCount > 0) {
+ val currentFragmentName = getBackStackEntryAt(backStackEntryCount - 1).name
- val fragment = fragmentFactory(screen).apply {
- arguments = bundle
-
- if (fgShareTransition != null) {
- sharedElementEnterTransition = fgShareTransition.enterTransitionSet
- sharedElementReturnTransition = fgShareTransition.exitTransitionSet
- fgShareTransition.shareElements.forEach { shareElement ->
- shareElement.wView.get()?.let { view ->
- transaction.addSharedElement(view, shareElement.elementName)
- }
- }
+ if (name == currentFragmentName) {
+ Timber.d("Fragment $name is already at the top of the stack")
+ return
}
}
- if (screen.isDialogFragment && fragment is DialogFragment) {
- fragment.showAllowingStateLoss(
- fragmentManager = supportFragmentManager,
- baseTransaction = transaction,
- tag = screen.name,
- addToBackstack = addToBackstack,
- )
+ Timber.d("Showing $name route")
+
+ val isPoppedBack = popBackStackImmediate(name, 0)
+
+ if (!isPoppedBack) {
+ val fragment = fragmentProvider()
+
+ if (fragment is DialogFragment) {
+ fragment.showDialog(fragmentManager = this, name)
+ } else {
+ fragment.showFragment(fragmentManager = this, name)
+ }
+
+ 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..10299c8916
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/finisher/AndroidAppFinisher.kt
@@ -0,0 +1,27 @@
+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)
+ Runtime.getRuntime().exit(0)
+ }
+}
\ 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 a65014a580..04a76036c3 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 1935d1f5d6..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() -> NavigationState()
- 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..145073e75c
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/settings/IntentSettingsManager.kt
@@ -0,0 +1,20 @@
+package com.tangem.tap.common.settings
+
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
+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),
+ ).apply {
+ flags = FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ }
+ 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/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt
index 3d1b711ccf..0f3fab437c 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt
@@ -84,4 +84,10 @@ internal object CardDomainModule {
fun provideResetCardUseCase(tangemSdkManager: TangemSdkManager): ResetCardUseCase {
return DefaultResetCardUseCase(tangemSdkManager)
}
+
+ @Provides
+ @Singleton
+ fun provideNetworkHasDerivationUseCase(): NetworkHasDerivationUseCase {
+ return NetworkHasDerivationUseCase()
+ }
}
\ 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/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt
new file mode 100644
index 0000000000..765e3bcb7f
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt
@@ -0,0 +1,22 @@
+package com.tangem.tap.di.domain
+
+import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
+import com.tangem.domain.markets.repositories.MarketsTokenRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+object MarketsDomainModule {
+
+ @Provides
+ @Singleton
+ fun provideGetMarketsTokenListFlowUseCase(
+ marketsTokenRepository: MarketsTokenRepository,
+ ): GetMarketsTokenListFlowUseCase {
+ return GetMarketsTokenListFlowUseCase(marketsTokenRepository = marketsTokenRepository)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt
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..c280208083 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,7 @@
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.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import dagger.Module
import dagger.Provides
@@ -16,17 +15,145 @@ internal object StakingDomainModule {
@Provides
@Singleton
- fun provideGetStakingEntryInfoUseCase(stakingRepository: StakingRepository): GetStakingEntryInfoUseCase {
- return GetStakingEntryInfoUseCase(
+ fun provideGetStakingAvailabilityUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): GetStakingAvailabilityUseCase {
+ return GetStakingAvailabilityUseCase(
stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
- fun provideGetStakingAvailabilityUseCase(stakingRepository: StakingRepository): GetStakingAvailabilityUseCase {
- return GetStakingAvailabilityUseCase(
+ fun provideGetStakingEntryInfoUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): GetStakingEntryInfoUseCase {
+ return GetStakingEntryInfoUseCase(
stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideGetYieldUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): GetYieldUseCase {
+ return GetYieldUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideGetStakingTokensUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): FetchStakingTokensUseCase {
+ return FetchStakingTokensUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideFetchStakingYieldBalanceUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): FetchStakingYieldBalanceUseCase {
+ return FetchStakingYieldBalanceUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideGetStakingYieldBalanceUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): GetStakingYieldBalanceUseCase {
+ return GetStakingYieldBalanceUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideInitializeStakingProcessUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): GetStakingTransactionUseCase {
+ return GetStakingTransactionUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideGasEstimateUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): EstimateGasUseCase {
+ return EstimateGasUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideSubmitHashUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): SubmitHashUseCase {
+ return SubmitHashUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideSaveUnsubmittedHashUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): SaveUnsubmittedHashUseCase {
+ return SaveUnsubmittedHashUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideSendUnsubmittedHashesUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): SendUnsubmittedHashesUseCase {
+ return SendUnsubmittedHashesUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
+ )
+ }
+
+ @Provides
+ @Singleton
+ fun provideIsStakeMoreAvailableUseCase(
+ stakingRepository: StakingRepository,
+ stakingErrorResolver: StakingErrorResolver,
+ ): IsStakeMoreAvailableUseCase {
+ return IsStakeMoreAvailableUseCase(
+ stakingRepository = stakingRepository,
+ stakingErrorResolver = stakingErrorResolver,
)
}
}
\ 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/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt
index 922581137c..04a4940928 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt
@@ -494,6 +494,6 @@ internal class DefaultLegacyWalletConnectRepository(
private companion object {
- val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro")
+ val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox")
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/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 ba234b662c..c9dc2ceead 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
@@ -74,7 +74,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)
@@ -94,7 +95,7 @@ class DetailsMiddleware {
store.dispatch(DetailsAction.ReCreateTwinsWallet)
return
} else {
- store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
+ store.dispatchNavigationAction { push(AppRoute.ResetToFactory) }
}
}
is DetailsAction.ResetToFactory.Proceed -> {
@@ -135,15 +136,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) }
}
}
}
@@ -167,7 +168,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
@@ -186,7 +187,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 -> {
@@ -246,6 +247,7 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
+ is DetailsAction.AppSettings.Prepare,
-> Unit
}
}
@@ -274,7 +276,7 @@ class DetailsMiddleware {
private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication)
- store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
+ activityResultCaller.openSystemBiometrySettings()
}
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
@@ -395,7 +397,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
- store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
+ store.dispatchNavigationAction { popTo() }
return CompletionResult.Success(Unit)
}
@@ -434,7 +436,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 {
@@ -446,7 +448,7 @@ class DetailsMiddleware {
AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled),
),
)
- store.dispatchOnMain(NavigationAction.PopBackTo())
+ store.dispatchNavigationAction(AppRouter::pop)
store.dispatchOnMain(
DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled),
)
@@ -502,7 +504,7 @@ class DetailsMiddleware {
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
},
disclaimerWillShow = {
- store.dispatchOnMain(NavigationAction.PopBackTo())
+ store.dispatchNavigationAction(AppRouter::pop)
},
onSuccess = { scanResponse ->
createUserWallet(scanResponse)
@@ -546,7 +548,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) {
@@ -554,7 +556,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 68b87d064f..7a87450f97 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
@@ -281,6 +283,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..fd2b740b37 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.toString(), "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 3aca958d05..d6c4adff57 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 4069551f22..f1b43ae123 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) = scope.launch {
@@ -131,7 +124,7 @@ internal class SaveWalletMiddleware {
)
store.dispatchWithMain(SaveWalletAction.AllowToUseBiometrics.Success)
- store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
+ store.dispatchNavigationAction { popTo() }
}
private fun dismiss(state: SaveWalletState) {
@@ -160,13 +153,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..645fc6ec32 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.StringsSigns.STARS
import java.math.BigDecimal
/**
diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt
index dbf1db4f40..8d1d46d4cd 100644
--- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt
@@ -10,6 +10,7 @@ import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.store
+import com.tangem.utils.StringsSigns.LOWER_SIGN
import java.math.BigDecimal
/**
@@ -401,11 +402,7 @@ class ReceiptReducer : SendInternalReducer {
}
private fun String.addPrecisionSign(): String {
- val result = if (feeState.feeIsApproximate) "$CAN_BE_LOWER_SIGN $this" else this
+ val result = if (feeState.feeIsApproximate) "$LOWER_SIGN $this" else this
return result.trim()
}
-
- private companion object {
- const val CAN_BE_LOWER_SIGN = "<"
- }
}
\ No newline at end of file
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 f401253e1e..987034d99b 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() {
@@ -58,7 +60,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 4874bc303c..a716c916bb 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..146c491074 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.StringsSigns.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/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt
new file mode 100644
index 0000000000..8689591d9b
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/SendDoneButtons.kt
@@ -0,0 +1,73 @@
+package com.tangem.common.ui.amountScreen.ui
+
+import android.content.res.Configuration
+import androidx.compose.animation.*
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.tangem.core.ui.components.SecondaryButtonIconStart
+import com.tangem.core.ui.components.SpacerW12
+import com.tangem.core.ui.extensions.shareText
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.common.ui.R
+import com.tangem.core.ui.res.TangemThemePreview
+
+@Composable
+fun SendDoneButtons(
+ txUrl: String,
+ onExploreClick: () -> Unit,
+ onShareClick: () -> Unit,
+ isVisible: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ val hapticFeedback = LocalHapticFeedback.current
+ val context = LocalContext.current
+
+ AnimatedVisibility(
+ visible = isVisible && txUrl.isNotBlank(),
+ modifier = modifier,
+ enter = slideInVertically().plus(fadeIn()),
+ exit = slideOutVertically().plus(fadeOut()),
+ label = "Animate show sent state buttons",
+ ) {
+ Row(modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12)) {
+ SecondaryButtonIconStart(
+ text = stringResource(id = R.string.common_explore),
+ iconResId = R.drawable.ic_web_24,
+ onClick = onExploreClick,
+ modifier = Modifier.weight(1f),
+ )
+ SpacerW12()
+ SecondaryButtonIconStart(
+ text = stringResource(id = R.string.common_share),
+ iconResId = R.drawable.ic_share_24,
+ onClick = {
+ hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
+ context.shareText(txUrl)
+ onShareClick()
+ },
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+}
+
+@Preview(showBackground = true, widthDp = 328)
+@Preview(showBackground = true, widthDp = 328, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun SendDoneButtons_Preview() {
+ TangemThemePreview {
+ SendDoneButtons(
+ txUrl = "txUrl",
+ onShareClick = {},
+ onExploreClick = {},
+ isVisible = true,
+ )
+ }
+}
\ No newline at end of file
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/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt
new file mode 100644
index 0000000000..f59c494895
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt
@@ -0,0 +1,333 @@
+package com.tangem.common.ui.bottomsheet.permission
+
+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.bottomsheet.permission.state.*
+import com.tangem.core.ui.components.*
+import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
+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) {
+ var isPermissionAlertShow by remember { mutableStateOf(false) }
+
+ TangemBottomSheet(
+ config = config,
+ containerColor = TangemTheme.colors.background.secondary,
+ titleText = resourceReference(R.string.give_permission_title),
+ titleAction = TopAppBarButtonUM(
+ iconRes = R.drawable.ic_information_24,
+ onIconClicked = { isPermissionAlertShow = true },
+ ),
+ content = { content: GiveTxPermissionBottomSheetConfig ->
+ GiveTxPermissionBottomSheetContent(content = content)
+
+ if (isPermissionAlertShow) {
+ BasicDialog(
+ message = content.data.dialogText.resolveReference(),
+ title = stringResource(id = R.string.common_approve),
+ confirmButton = DialogButton { isPermissionAlertShow = false },
+ onDismissDialog = {},
+ )
+ }
+ },
+ )
+}
+
+@Composable
+private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSheetConfig) {
+ val data = content.data
+ Column(
+ modifier = Modifier
+ .background(color = TangemTheme.colors.background.secondary)
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ text = content.data.subtitle.resolveReference(),
+ 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()
+ }
+}
+
+@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,
+ )
+ Text(
+ text = stringResource(
+ when (approveType) {
+ ApproveType.LIMITED -> R.string.give_permission_current_transaction
+ ApproveType.UNLIMITED -> R.string.give_permission_unlimited
+ },
+ ),
+ color = TangemTheme.colors.text.tertiary,
+ style = TangemTheme.typography.body1,
+ maxLines = 1,
+ )
+ }
+ 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
+@Composable
+@Preview(showBackground = true)
+@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
+private fun Preview_GiveTxPermissionBottomSheet() {
+ TangemThemePreview {
+ GiveTxPermissionBottomSheet(
+ config = TangemBottomSheetConfig(
+ isShow = true,
+ onDismissRequest = {},
+ 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 },
+ subtitle = resourceReference(R.string.give_permission_staking_subtitle),
+ dialogText = resourceReference(R.string.give_permission_staking_footer),
+ ),
+ onCancel = {},
+)
\ No newline at end of file
diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionBottomSheetConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionBottomSheetConfig.kt
new file mode 100644
index 0000000000..08ffdcc086
--- /dev/null
+++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionBottomSheetConfig.kt
@@ -0,0 +1,8 @@
+package com.tangem.common.ui.bottomsheet.permission.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/bottomsheet/permission/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/bottomsheet/permission/state/GiveTxPermissionState.kt
index 4192106336..dbdce22ba9 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/bottomsheet/permission/state/GiveTxPermissionState.kt
@@ -1,41 +1,35 @@
-package com.tangem.feature.swap.models
+package com.tangem.common.ui.bottomsheet.permission.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 subtitle: TextReference,
+ val dialogText: TextReference,
val currency: String,
val amount: String,
val walletAddress: String,
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..014f9adf75
--- /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: String,
+ @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..b2f534b038
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketChartListResponse.kt
@@ -0,0 +1,3 @@
+package com.tangem.datasource.api.markets.models.response
+
+typealias TokenMarketChartListResponse = 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..4d6ff9ca76 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,71 @@
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.TokenWithYieldDTO
+import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
+import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO
+import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO
+import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO
+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: ActionRequestBody): ApiResponse
+
+ @POST("actions/exit")
+ suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse
+
+ @POST("actions/enter/estimate-gas")
+ suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse
+
+ @POST("actions/exit/estimate-gas")
+ suspend fun estimateGasOnExit(@Body body: ActionRequestBody): 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/ActionRequestBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt
new file mode 100644
index 0000000000..e044b1f9a9
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/request/ActionRequestBody.kt
@@ -0,0 +1,50 @@
+package com.tangem.datasource.api.stakekit.models.request
+
+import com.squareup.moshi.Json
+import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs
+import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
+import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
+
+data class ActionRequestBody(
+ @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,
+ @Json(name = "gasArgs")
+ val gasArgs: GasArgs? = null, // used only in estimate_gas request
+) {
+
+ 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/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/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/error/StakeKitErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt
new file mode 100644
index 0000000000..cf62a7d43b
--- /dev/null
+++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/error/StakeKitErrorResponse.kt
@@ -0,0 +1,143 @@
+package com.tangem.datasource.api.stakekit.models.response.model.error
+
+import com.squareup.moshi.Json
+
+class StakeKitErrorResponse(
+ @Json(name = "details")
+ val details: StakeKitErrorDetailsDTO? = null,
+ @Json(name = "message")
+ val message: StakeKitErrorMessageDTO? = null,
+ @Json(name = "level")
+ val level: String? = null, // unused
+)
+
+data class StakeKitErrorDetailsDTO(
+ @Json(name = "arguments")
+ val arguments: String? = null,
+ @Json(name = "amount")
+ val amount: String? = null,
+ @Json(name = "yieldId")
+ val yieldId: String? = null,
+)
+
+enum class StakeKitErrorMessageDTO {
+ @Json(name = "MissingArgumentsError")
+ MISSING_ARGUMENTS_ERROR,
+
+ @Json(name = "MinimumAmountNotReached")
+ MINIMUM_AMOUNT_NOT_REACHED,
+
+ @Json(name = "YieldUnderMaintenanceError")
+ YIELD_UNDER_MAINTENANCE_ERROR,
+
+ @Json(name = "InsufficientFundsError")
+ INSUFFICIENT_FUNDS_ERROR,
+
+ @Json(name = "StakedPositionNotFoundError")
+ STAKED_POSITION_NOT_FOUND_ERROR,
+
+ @Json(name = "InvalidAmountSubmittedError")
+ INVALID_AMOUNT_SUBMITTED_ERROR,
+
+ @Json(name = "BalanceUnavailableError")
+ BALANCE_UNAVAILABLE_ERROR,
+
+ @Json(name = "GasPriceUnavailableError")
+ GAS_PRICE_UNAVAILABLE_ERROR,
+
+ @Json(name = "NotImplementedError")
+ NOT_IMPLEMENTED_ERROR,
+
+ @Json(name = "TokenNotFoundError")
+ TOKEN_NOT_FOUND_ERROR,
+
+ @Json(name = "BroadcastTransactionError")
+ BROADCAST_TRANSACTION_ERROR,
+
+ @Json(name = "MissingGasPriceStrategyError")
+ MISSING_GAS_PRICE_STRATEGY_ERROR,
+
+ @Json(name = "SubstrateMalformedTransactionHashError")
+ SUBSTRATE_MALFORMED_TRANSACTION_HASH_ERROR,
+
+ @Json(name = "TronMaximumAmountOfValidatorsExceededError")
+ TRON_MAXIMUM_AMOUNT_OF_VALIDATORS_EXCEEDED_ERROR,
+
+ @Json(name = "SubstratePoolNotFoundError")
+ SUBSTRATE_POOL_NOT_FOUND_ERROR,
+
+ @Json(name = "SubstrateBondedAmountTooLowError")
+ SUBSTRATE_BONDED_AMOUNT_TOO_LOW_ERROR,
+
+ @Json(name = "TronMissingResourceTypeArgumentError")
+ TRON_MISSING_RESOURCE_TYPE_ARGUMENT_ERROR,
+
+ @Json(name = "AaveV3PoolFrozenError")
+ AAVE_V3_POOL_FROZEN_ERROR,
+
+ @Json(name = "AaveV3TokenPairNotFoundError")
+ AAVE_V3_TOKEN_PAIR_NOT_FOUND_ERROR,
+
+ @Json(name = "YearnVaultAtMaxCapacityError")
+ YEARN_VAULT_AT_MAX_CAPACITY_ERROR,
+
+ @Json(name = "StETHNoWithdrawalRequestsFoundError")
+ STETH_NO_WITHDRAWAL_REQUESTS_FOUND_ERROR,
+
+ @Json(name = "MorphoLendingPoolPausedError")
+ MORPHO_LENDING_POOL_PAUSED_ERROR,
+
+ @Json(name = "NonceUnavailableError")
+ NONCE_UNAVAILABLE_ERROR,
+
+ @Json(name = "CosmosAcccountNotFoundError")
+ COSMOS_ACCOUNT_NOT_FOUND_ERROR,
+
+ @Json(name = "AvalancheMissingAdditionalAddressesArgumentError")
+ AVALANCHE_MISSING_ADDITIONAL_ADDRESSES_ARGUMENT_ERROR,
+
+ @Json(name = "AvalancheValidatorInfoNotFoundError")
+ AVALANCHE_VALIDATOR_INFO_NOT_FOUND_ERROR,
+
+ @Json(name = "SolanaTransactionSignatureVerificationFailureError")
+ SOLANA_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE_ERROR,
+
+ @Json(name = "SolanaUnableTocreateStakeAccountError")
+ SOLANA_UNABLE_TO_CREATE_STAKE_ACCOUNT_ERROR,
+
+ @Json(name = "SolanaStakeAmountTooLowError")
+ SOLANA_STAKE_AMOUNT_TOO_LOW_ERROR,
+
+ @Json(name = "SolanaUnstakeAmountTooLowError")
+ SOLANA_UNSTAKE_AMOUNT_TOO_LOW_ERROR,
+
+ @Json(name = "SolanaStakeAccountsNotFoundError")
+ SOLANA_STAKE_ACCOUNTS_NOT_FOUND_ERROR,
+
+ @Json(name = "SolanaEligibleStakeAccountsNotFoundError")
+ SOLANA_ELIGIBLE_STAKE_ACCOUNTS_NOT_FOUND_ERROR,
+
+ @Json(name = "TezosNoBalanceDelegatedError")
+ TEZOS_NO_BALANCE_DELEGATED_ERROR,
+
+ @Json(name = "TezosMissingPubkeyArgumentError")
+ TEZOS_MISSING_PUBKEY_ARGUMENT_ERROR,
+
+ @Json(name = "TezosEstimateRevealGasLimitError")
+ TEZOS_ESTIMATE_REVEAL_GAS_LIMIT_ERROR,
+
+ @Json(name = "TezosBalanceAlreadyDelegatedError")
+ TEZOS_BALANCE_ALREADY_DELEGATED_ERROR,
+
+ @Json(name = "BinanceAccountNotFoundError")
+ BINANCE_ACCOUNT_NOT_FOUND_ERROR,
+
+ @Json(name = "BinanceMissingAccountNumberOrSequenceError")
+ BINANCE_MISSING_ACCOUNT_NUMBER_OR_SEQUENCE_ERROR,
+
+ @Json(name = "GRTStakingDisabledError")
+ GRT_STAKING_DISABLED_ERROR,
+
+ @Json(name = "GRTStakingDisabledLedgerLiveError")
+ GRT_STAKING_DISABLED_LEDGER_LIVE_ERROR,
+}
\ 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..6951732450 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
@@ -123,7 +124,31 @@ class NetworkModule {
context = context,
appVersionProvider = appVersionProvider,
baseUrl = PROD_V1_TANGEM_TECH_BASE_URL,
- timeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS,
+ timeouts = Timeouts(
+ callTimeoutSeconds = TANGEM_TECH_SERVICE_TIMEOUT_SECONDS,
+ ),
+ requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)),
+ )
+ }
+
+ @Provides
+ @DevTangemApi
+ @Singleton
+ fun provideTangemTechMarketsApi(
+ @NetworkMoshi moshi: Moshi,
+ @ApplicationContext context: Context,
+ appVersionProvider: AppVersionProvider,
+ ): TangemTechMarketsApi {
+ return provideTangemTechApiInternal(
+ moshi = moshi,
+ context = context,
+ appVersionProvider = appVersionProvider,
+ baseUrl = DEV_V1_TANGEM_TECH_BASE_URL,
+ timeouts = Timeouts(
+ callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
+ connectTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
+ readTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
+ ),
requestHeaders = listOf(AppVersionPlatformHeaders(appVersionProvider)),
)
}
@@ -133,16 +158,25 @@ class NetworkModule {
context: Context,
appVersionProvider: AppVersionProvider,
baseUrl: String,
- timeoutSeconds: Long? = null,
+ timeouts: Timeouts = Timeouts(),
requestHeaders: List = listOf(CacheControlHeader, AppVersionPlatformHeaders(appVersionProvider)),
): T {
val client = OkHttpClient.Builder()
.let { builder ->
- if (timeoutSeconds != null) {
- builder.callTimeout(timeoutSeconds, TimeUnit.SECONDS)
- } else {
- builder
+ var b = builder
+ if (timeouts.callTimeoutSeconds != null) {
+ b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS)
}
+ if (timeouts.connectTimeoutSeconds != null) {
+ b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS)
+ }
+ if (timeouts.readTimeoutSeconds != null) {
+ b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS)
+ }
+ if (timeouts.writeTimeoutSeconds != null) {
+ b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS)
+ }
+ b
}
.addHeaders(
*requestHeaders.toTypedArray(),
@@ -161,6 +195,13 @@ class NetworkModule {
.create(T::class.java)
}
+ private data class Timeouts(
+ val callTimeoutSeconds: Long? = null,
+ val connectTimeoutSeconds: Long? = null,
+ val readTimeoutSeconds: Long? = null,
+ val writeTimeoutSeconds: Long? = null,
+ )
+
private companion object {
const val STAKEKIT_BASE_URL = "https://api.stakek.it/v1/"
const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/"
@@ -173,5 +214,6 @@ class NetworkModule {
const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/"
const val TANGEM_TECH_SERVICE_TIMEOUT_SECONDS = 5L
+ const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
}
}
\ No newline at end of file
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..c517d9fdec 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
@@ -85,6 +85,8 @@ object PreferencesKeys {
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
+ val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") }
+
val IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY by lazy {
booleanPreferencesKey(name = "isWalletSwapPromoOkxShown")
}
@@ -94,6 +96,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/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt
index aa3171803c..26be12c002 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt
@@ -6,6 +6,7 @@ import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Types
import com.tangem.datasource.local.preferences.AppPreferencesStore
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
@@ -20,7 +21,7 @@ inline fun AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.storeObjectList(key: Preferen
/** Get flow of list of data [T] by string [key]. If data is not found, it returns `null` */
inline fun AppPreferencesStore.getObjectList(key: Preferences.Key): Flow?> {
val adapter = moshi.adapter>(Types.newParameterizedType(List::class.java, T::class.java))
- return data.map { it[key]?.let(adapter::fromJson) }
+ return data.map { it[key]?.let(adapter::fromJson) }.distinctUntilChanged()
}
/** Get list of data [T] by string [key], or empty if data is not found */
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/DefaultUserTokensStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt
deleted file mode 100644
index 23cea085d6..0000000000
--- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultUserTokensStore.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.tangem.datasource.local.token
-
-import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
-import com.tangem.datasource.local.datastore.core.StringKeyDataStore
-import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
-import com.tangem.domain.wallets.models.UserWalletId
-
-internal class DefaultUserTokensStore(
- dataStore: StringKeyDataStore,
-) : UserTokensStore, StringKeyDataStoreDecorator(dataStore) {
-
- override fun provideStringKey(key: UserWalletId): String {
- return "user_tokens_${key.stringValue}"
- }
-}
\ 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