diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 18fd29dc28..a003607888 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 {
@@ -22,20 +27,12 @@ configurations.all {
resolutionStrategy {
dependencySubstitution {
- substitute(module("com.facebook.react:react-native"))
- .using(module("com.facebook.react:react-android:0.72.4"))
-
- substitute(module("com.facebook.react:hermes-engine"))
- .using(module("com.facebook.react:hermes-android:0.72.4"))
-
substitute(module("org.bouncycastle:bcprov-jdk15on"))
.using(module("org.bouncycastle:bcprov-jdk18on:1.73"))
}
force(
"org.bouncycastle:bcpkix-jdk15on:1.70",
- "com.facebook.react:react-android:0.72.4",
- "com.facebook.react:hermes-android:0.72.4",
)
}
}
@@ -69,8 +66,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 +102,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 +130,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 +174,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) {
@@ -207,10 +216,6 @@ dependencies {
implementation(deps.walletConnectCore)
implementation(deps.walletConnectWeb3)
implementation(deps.prettyLogger)
- implementation("com.facebook.react:react-android:0.72.4")
- implementation(deps.sprClient) {
- exclude(group = "com.github.stephenc.jcip")
- }
/** Testing libraries */
testImplementation(deps.test.coroutine)
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..0740725de0 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,19 @@
android:resource="@xml/provider_paths" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config
index 35af912b96..095b8f4ea0 160000
--- a/app/src/main/assets/tangem-app-config
+++ b/app/src/main/assets/tangem-app-config
@@ -1 +1 @@
-Subproject commit 35af912b96ce0f871906061bd483d9cf3af7451d
+Subproject commit 095b8f4ea0fa02e7ccea93cf0f437346345297ef
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..8758b045e3 100644
--- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
+++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt
@@ -3,13 +3,11 @@ 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.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 +48,7 @@ internal class LockUserWalletsTimer(
start()
if (shouldOpenWelcomeScreenOnResume) {
- store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
+ store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
}
}
@@ -128,7 +126,7 @@ internal class LockUserWalletsTimer(
if (wasApplicationStopped) {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
} else {
- store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Welcome))
+ store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}
}
diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt
index 87b2bf414d..20808b8396 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,20 @@ 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.ShouldInitiallyAskPermissionUseCase
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.pushnotifications.api.utils.PUSH_PERMISSION
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 +65,9 @@ import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
+import com.tangem.tap.common.extensions.dispatchNavigationAction
+import com.tangem.tap.common.extensions.inject
+import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
@@ -68,17 +77,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 +135,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var tokenDetailsRouter: TokenDetailsRouter
- @Inject
- lateinit var manageTokensUi: ManageTokensUi
-
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
@@ -143,6 +150,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var settingsRepository: SettingsRepository
+ @Inject
+ lateinit var sendUnsubmittedHashesUseCase: SendUnsubmittedHashesUseCase
+
@Inject
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
@@ -158,6 +168,31 @@ 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
+
+ @Inject
+ lateinit var shouldInitiallyAskPermissionUseCase: ShouldInitiallyAskPermissionUseCase
+
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow
@@ -177,6 +212,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 +227,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 +285,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 +304,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 +330,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@SuppressLint("SourceLockedOrientationActivity")
private fun initContent() {
- WindowCompat.setDecorFitsSystemWindows(window, false)
-
supportFragmentManager.registerFragmentLifecycleCallbacks(
NavBarInsetsFragmentLifecycleCallback(),
true,
@@ -308,7 +373,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
override fun onDestroy() {
- store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this)))
intentProcessor.removeAll()
super.onDestroy()
}
@@ -448,8 +512,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 +530,24 @@ 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 isPushPermissionEnabled = toggles.isPushNotificationsEnabled
+ val shouldShowTos = !cardRepository.isTangemTOSAccepted() && isPushPermissionEnabled
+ val wasPushInitiallyAsked = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrElse { false }
+ val shouldShowInitialPush = wasPushInitiallyAsked && isPushPermissionEnabled
+
+ val route = when {
+ shouldShowTos -> AppRoute.Disclaimer(isTosAccepted = false)
+ shouldShowInitialPush -> AppRoute.PushNotification
+ else -> AppRoute.Home
+ }
+
+ store.dispatchNavigationAction { replaceAll(route) }
intentProcessor.handleIntent(intentWhichStartedActivity, false)
}
}
@@ -484,16 +555,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 +575,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..eaca995355 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,17 +45,15 @@ 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
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
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 +110,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 +178,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 +262,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
customTokenFeatureToggles = customTokenFeatureToggles,
walletConnectRepository = walletConnect2Repository,
walletConnectSessionsRepository = walletConnectSessionsRepository,
- manageTokensFeatureToggles = manageTokensFeatureToggles,
scanCardProcessor = scanCardProcessor,
appCurrencyRepository = appCurrencyRepository,
walletManagersFacade = walletManagersFacade,
@@ -274,9 +282,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,
),
),
)
@@ -302,7 +315,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private fun initWithConfigDependency(config: Config) {
initAnalytics(this, config)
- initFeedbackManager(this, foregroundActivityObserver, store)
+ initFeedbackManager(this, store)
}
private fun initAnalytics(application: Application, config: Config) {
@@ -323,11 +336,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
// ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA]
}
- private fun initFeedbackManager(
- context: Context,
- foregroundActivityObserver: ForegroundActivityObserver,
- store: Store,
- ) {
+ private fun initFeedbackManager(context: Context, store: Store) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo {
return AdditionalFeedbackInfo().apply {
appVersion = try {
@@ -365,10 +374,9 @@ 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),
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
)
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 4352a65eb9..a13d000c44 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/DefaultAnalyticsContextProxy.kt b/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt
new file mode 100644
index 0000000000..d1aecc29d0
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/common/analytics/DefaultAnalyticsContextProxy.kt
@@ -0,0 +1,31 @@
+package com.tangem.tap.common.analytics
+
+import com.tangem.core.analytics.Analytics
+import com.tangem.core.analytics.utils.AnalyticsContextProxy
+import com.tangem.domain.models.scan.ScanResponse
+import com.tangem.tap.common.extensions.addContext
+import com.tangem.tap.common.extensions.eraseContext
+import com.tangem.tap.common.extensions.removeContext
+import com.tangem.tap.common.extensions.setContext
+
+/**
+[REDACTED_AUTHOR]
+ */
+internal class DefaultAnalyticsContextProxy : AnalyticsContextProxy {
+
+ override fun setContext(scanResponse: ScanResponse) {
+ Analytics.setContext(scanResponse)
+ }
+
+ override fun eraseContext() {
+ Analytics.eraseContext()
+ }
+
+ override fun addContext(scanResponse: ScanResponse) {
+ Analytics.addContext(scanResponse)
+ }
+
+ override fun removeContext() {
+ Analytics.removeContext()
+ }
+}
\ No newline at end of file
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/chat/ChatManager.kt b/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt
deleted file mode 100644
index 5bbe3fc679..0000000000
--- a/app/src/main/java/com/tangem/tap/common/chat/ChatManager.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.tangem.tap.common.chat
-
-import android.content.Context
-import com.tangem.datasource.config.models.ChatConfig
-import com.tangem.datasource.config.models.SprinklrConfig
-import com.tangem.tap.ForegroundActivityObserver
-import com.tangem.tap.common.chat.opener.ChatOpener
-import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
-import java.io.File
-
-class ChatManager(private val foregroundActivityObserver: ForegroundActivityObserver) {
- private val openers = mutableMapOf()
-
- fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) {
- val opener = openers.getOrPut(config) {
- when (config) {
- is SprinklrConfig -> SprinklrChatOpener(config, foregroundActivityObserver)
- }
- }
-
- opener.open(createFeedbackFile, createLogsFile)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt
deleted file mode 100644
index aff21cad05..0000000000
--- a/app/src/main/java/com/tangem/tap/common/chat/opener/ChatOpener.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package com.tangem.tap.common.chat.opener
-
-import android.content.Context
-import java.io.File
-
-internal interface ChatOpener {
- fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?)
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt b/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt
deleted file mode 100644
index 0fcb2a4cf2..0000000000
--- a/app/src/main/java/com/tangem/tap/common/chat/opener/implementation/SprinklrChatOpener.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.tangem.tap.common.chat.opener.implementation
-
-import android.annotation.SuppressLint
-import android.app.Application
-import android.content.Context
-import android.provider.Settings
-import com.spr.messengerclient.config.SPRMessenger
-import com.spr.messengerclient.config.bean.SPRMessengerConfig
-import com.tangem.common.extensions.guard
-import com.tangem.datasource.config.models.SprinklrConfig
-import com.tangem.tap.ForegroundActivityObserver
-import com.tangem.tap.common.chat.opener.ChatOpener
-import timber.log.Timber
-import java.io.File
-import java.util.Locale
-
-internal class SprinklrChatOpener(
- private val config: SprinklrConfig,
- private val foregroundActivityObserver: ForegroundActivityObserver,
-) : ChatOpener {
-
- override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
- val messenger = SPRMessenger.shared()
-
- if (messenger.config == null) {
- initSprConfig(messenger)
- }
-
- messenger.startApplication()
- }
-
- private fun initSprConfig(messenger: SPRMessenger) {
- val application = foregroundActivityObserver.foregroundActivity?.application.guard {
- Timber.e("The SPR chat cannot be opened because there are no activities in foreground")
- return
- }
-
- messenger.takeOff(application, createSprConfig(application, config))
- }
-
- @SuppressLint("HardwareIds")
- private fun createSprConfig(application: Application, config: SprinklrConfig): SPRMessengerConfig {
- return SPRMessengerConfig().apply {
- appId = config.appId
- appKey = CHAT_APP_KEY
- deviceId = Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID)
- environment = config.environment
- skin = CHAT_SKIN
- locale = Locale.getDefault().language
- }
- }
-
- private companion object {
- const val CHAT_APP_KEY = "com.sprinklr.messenger.release"
- const val CHAT_SKIN = "MODERN"
- }
-}
\ 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 67%
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..e7713995ac 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
@@ -2,18 +2,17 @@ package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.core.navigation.email.EmailSender
-import com.tangem.datasource.config.models.ChatConfig
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.tap.common.chat.ChatManager
+import com.tangem.domain.models.scan.ScanResponse
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,27 +24,33 @@ import java.io.StringWriter
/**
[REDACTED_AUTHOR]
*/
-class FeedbackManager(
+class LegacyFeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
- private val chatManager: ChatManager,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase,
) {
- 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,46 +71,25 @@ class FeedbackManager(
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
file = getLogFile(activity),
- onFail = onFail,
)
}
}
}
- fun openChat(config: ChatConfig, feedbackData: FeedbackData) {
- chatManager.open(
- config = config,
- createLogsFile = ::getLogFile,
- createFeedbackFile = { context -> getFeedbackFile(context, feedbackData) },
- )
- }
+ fun sendEmail(type: FeedbackEmailType) {
+ if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) error("LOCAL_LOGS feature toggle must be enabled")
- private fun getFeedbackFile(context: Context, feedbackData: FeedbackData): File? {
- return try {
- if (sessionFeedbackFile != null) {
- return sessionFeedbackFile
- }
- val file = File(context.filesDir, FEEDBACK_FILE)
- file.delete()
- file.createNewFile()
+ scope.launch {
+ val email = getFeedbackEmailUseCase(type = type)
- val feedback = feedbackData.run {
- prepare(infoHolder)
- joinTogether(context, infoHolder)
- }
- val fileWriter = FileWriter(file)
- fileWriter.write(feedback)
- fileWriter.close()
-
- if (file.exists()) {
- sessionFeedbackFile = file
- sessionFeedbackFile
- } else {
- null
- }
- } catch (ex: Exception) {
- Timber.e(ex, "Can't create the logs file")
- null
+ store.inject(DaggerGraphState::emailSender).send(
+ email = EmailSender.Email(
+ address = email.address,
+ subject = email.subject,
+ message = email.message,
+ attachment = email.file,
+ ),
+ )
}
}
@@ -144,10 +128,9 @@ 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"
const val LOGS_FILE = "logs.txt"
}
}
\ No newline at end of file
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..ade5d123b2 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,21 +2,19 @@ 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
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
-import com.tangem.tap.features.details.redux.SecurityOption
import org.rekotlin.Action
sealed class GlobalAction : Action {
@@ -75,14 +73,12 @@ sealed class GlobalAction : Action {
) : GlobalAction()
data class HideWarningMessage(val warning: WarningMessage) : GlobalAction()
- data class UpdateSecurityOptions(val securityOption: SecurityOption) : GlobalAction()
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 OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()
+ data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction()
object ExchangeManager : GlobalAction() {
object Init : 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..9c6882deda 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,27 +69,10 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
}
is GlobalAction.SendEmail -> {
- store.state.globalState.feedbackManager?.sendEmail(action.feedbackData)
- }
- is GlobalAction.OpenChat -> {
- val globalState = store.state.globalState
- val feedbackManager = globalState.feedbackManager.guard {
- store.dispatchDebugErrorNotification("FeedbackManager not initialized")
- return
- }
- val config = globalState.configManager?.config.guard {
- store.dispatchDebugErrorNotification("Config not initialized")
- return
- }
-
- // if config not set -> try to get it based on a scanResponse.productType
- val unsafeChatConfig = action.chatConfig ?: config.sprinklr
-
- val chatConfig = unsafeChatConfig.guard {
- store.dispatchDebugErrorNotification("The chat config is not initialized")
- return
- }
- feedbackManager.openChat(chatConfig, action.feedbackData)
+ store.state.globalState.feedbackManager?.sendEmail(
+ feedbackData = action.feedbackData,
+ scanResponse = action.scanResponse,
+ )
}
is GlobalAction.ExchangeManager.Init -> {
val appStateSafe = appState() ?: return
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 4b7bdcdf15..80d9f694c8 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.FeedbackEmail
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
@@ -20,10 +23,16 @@ 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.SendEmailSupport -> {
- store.state.globalState.feedbackManager?.sendEmail(FeedbackEmail())
+ store.state.globalState.feedbackManager?.sendEmail(
+ feedbackData = FeedbackEmail(),
+ scanResponse = action.scanResponse,
+ )
}
is LegacyAction.StartOnboardingProcess -> {
store.dispatch(
@@ -32,8 +41,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 {
@@ -50,7 +79,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 69%
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 6f1c8151d6..7fe3ef0081 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.net.Uri
@@ -6,12 +6,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..1661cedb1d 100644
--- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt
+++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt
@@ -1,9 +1,11 @@
package com.tangem.tap.data
+import com.tangem.common.CompletionResult
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 +17,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
@@ -26,7 +31,10 @@ internal class RuntimeUserWalletsStore(
return userWalletsListManager.userWallets.firstOrNull()
}
- override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) {
- userWalletsListManager.update(userWalletId, update)
+ override suspend fun update(
+ userWalletId: UserWalletId,
+ update: suspend (UserWallet) -> UserWallet,
+ ): CompletionResult {
+ return userWalletsListManager.update(userWalletId, update)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt
index 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/analytics/AnalyticsModule.kt b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt
index 34dbbd2e11..c687d63c10 100644
--- a/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/analytics/AnalyticsModule.kt
@@ -1,6 +1,8 @@
package com.tangem.tap.di.analytics
+import com.tangem.core.analytics.utils.AnalyticsContextProxy
import com.tangem.domain.analytics.ChangeCardAnalyticsContextUseCase
+import com.tangem.tap.common.analytics.DefaultAnalyticsContextProxy
import com.tangem.tap.common.analytics.DefaultChangeCardAnalyticsContextUseCase
import dagger.Module
import dagger.Provides
@@ -17,4 +19,8 @@ internal object AnalyticsModule {
fun provideChangeCardAnalyticsContextUseCase(): ChangeCardAnalyticsContextUseCase {
return DefaultChangeCardAnalyticsContextUseCase()
}
+
+ @Provides
+ @Singleton
+ fun provideAnalyticsContextProxy(): AnalyticsContextProxy = DefaultAnalyticsContextProxy()
}
\ 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..c05129c777 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,36 @@ internal object SettingsDomainModule {
): IncrementAppLaunchCounterUseCase {
return IncrementAppLaunchCounterUseCase(settingsRepository = settingsRepository)
}
+
+ // region PushPermissionRepository
+ @Provides
+ @Singleton
+ fun provideShouldInitiallyAskPermissionUseCase(
+ permissionRepository: PermissionRepository,
+ ): ShouldInitiallyAskPermissionUseCase {
+ return ShouldInitiallyAskPermissionUseCase(repository = permissionRepository)
+ }
+
+ @Provides
+ @Singleton
+ fun provideNeverToInitiallyAskPermissionUseCase(
+ permissionRepository: PermissionRepository,
+ ): NeverToInitiallyAskPermissionUseCase {
+ return NeverToInitiallyAskPermissionUseCase(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..09792b495e 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,10 +2,12 @@ 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
import com.tangem.feature.swap.domain.api.SwapRepository
+import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@@ -54,8 +56,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 +72,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 +92,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 +116,7 @@ internal object TokensDomainModule {
currencyChecksRepository: CurrencyChecksRepository,
showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
promoRepository: PromoRepository,
+ stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
@@ -112,6 +129,7 @@ internal object TokensDomainModule {
swapRepository = swapRepository,
showSwapPromoTokenUseCase = showSwapPromoTokenUseCase,
promoRepository = promoRepository,
+ stakingRepository = stakingRepository,
dispatchers = dispatchers,
)
}
@@ -122,12 +140,14 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
+ stakingRepository: StakingRepository,
dispatchers: CoroutineDispatcherProvider,
): GetPrimaryCurrencyStatusUpdatesUseCase {
return GetPrimaryCurrencyStatusUpdatesUseCase(
currenciesRepository,
quotesRepository,
networksRepository,
+ stakingRepository,
dispatchers,
)
}
@@ -148,8 +168,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 +211,8 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
+ stakingRepository: StakingRepository,
+ stakingFeatureToggles: StakingFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
@@ -199,6 +222,8 @@ internal object TokensDomainModule {
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
+ stakingRepository = stakingRepository,
+ stakingFeatureToggles = stakingFeatureToggles,
dispatchers = dispatchers,
)
}
@@ -209,12 +234,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 +252,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 +392,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..8af69450c8 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,23 @@ 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 = walletManagersFacade,
+ demoConfig = DemoConfig(),
+ )
+ }
+
@Provides
@Singleton
fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase {
diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
index 3dfc085c10..cc9b89e775 100644
--- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
+++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt
@@ -1,12 +1,12 @@
package com.tangem.tap.di.domain
import com.tangem.domain.redux.ReduxStateHolder
-import com.tangem.domain.walletmanager.WalletManagersFacade
-import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase
+import com.tangem.domain.walletmanager.WalletManagersFacade
+import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
@@ -95,8 +95,11 @@ internal object WalletsDomainModule {
@Provides
@Singleton
- fun providesRenameWalletUseCase(userWalletsListManager: UserWalletsListManager): RenameWalletUseCase {
- return RenameWalletUseCase(userWalletsListManager = userWalletsListManager)
+ fun providesRenameWalletUseCase(
+ userWalletsListManager: UserWalletsListManager,
+ dispatchers: CoroutineDispatcherProvider,
+ ): RenameWalletUseCase {
+ return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers)
}
@Provides
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..64e1b31472 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
@@ -15,7 +19,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import com.tangem.utils.coroutines.runCatching
+import kotlinx.coroutines.withContext
import timber.log.Timber
internal typealias Derivations = Map>
@@ -44,10 +48,12 @@ internal class DefaultDerivationsRepository(
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
.doOnSuccess { response ->
- updatePublicKeys(userWalletId = userWalletId, keys = response.entries).fold(
- onSuccess = { return },
- onFailure = { throw it },
- )
+ updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
+ .doOnSuccess {
+ validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
+ return
+ }
+ .doOnFailure { throw it }
}
.doOnFailure { throw it }
@@ -76,8 +82,24 @@ internal class DefaultDerivationsRepository(
}
}
- private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result {
- return runCatching(dispatchers.io) {
+ /**
+ * 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): CompletionResult {
+ return withContext(dispatchers.io) {
userWalletsStore.update(
userWalletId = userWalletId,
update = { userWallet -> userWallet.updateDerivedKeys(keys) },
diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt
index 966f9613e9..1e322aeaba 100644
--- a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt
+++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt
@@ -11,8 +11,8 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.domain.card.ResetCardUseCase
+import com.tangem.domain.card.ResetCardUserCodeParams
import com.tangem.domain.card.models.ResetCardError
-import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.sdk.TangemSdkManager
@@ -20,24 +20,25 @@ internal class DefaultResetCardUseCase(
private val tangemSdkManager: TangemSdkManager,
) : ResetCardUseCase {
- override suspend fun invoke(card: CardDTO): Either = resourceScope {
- either {
- withUserCodeRequestPolicy(card)
+ override suspend fun invoke(cardId: String, params: ResetCardUserCodeParams): Either =
+ resourceScope {
+ either {
+ withUserCodeRequestPolicy(params)
- tangemSdkManager.resetToFactorySettings(
- cardId = card.cardId,
- allowsRequestAccessCodeFromRepository = true,
- ).bind(raise = this)
+ tangemSdkManager.resetToFactorySettings(
+ cardId = cardId,
+ allowsRequestAccessCodeFromRepository = true,
+ ).bind(raise = this)
+ }
}
- }
override suspend fun invoke(
cardNumber: Int,
- card: CardDTO,
+ params: ResetCardUserCodeParams,
userWalletId: UserWalletId,
- ): Either = resourceScope {
+ ): Either = resourceScope {
either {
- withUserCodeRequestPolicy(card)
+ withUserCodeRequestPolicy(params)
tangemSdkManager.resetBackupCard(
cardNumber = cardNumber,
@@ -46,11 +47,11 @@ internal class DefaultResetCardUseCase(
}
}
- private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) {
+ private suspend fun ResourceScope.withUserCodeRequestPolicy(params: ResetCardUserCodeParams) {
install(
acquire = {
val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy
- requestMandatoryAccessCodeEntry(card)
+ requestMandatoryAccessCodeEntry(params)
policyBeforeReset
},
@@ -60,10 +61,10 @@ internal class DefaultResetCardUseCase(
)
}
- private fun requestMandatoryAccessCodeEntry(card: CardDTO) {
- val type = if (card.isAccessCodeSet) {
+ private fun requestMandatoryAccessCodeEntry(params: ResetCardUserCodeParams) {
+ val type = if (params.isAccessCodeSet) {
UserCodeType.AccessCode
- } else if (card.isPasscodeSet == true) {
+ } else if (params.isPasscodeSet == true) {
UserCodeType.Passcode
} else {
null
@@ -74,15 +75,14 @@ internal class DefaultResetCardUseCase(
}
}
- private fun CompletionResult<*>.bind(raise: Raise) {
+ private fun CompletionResult.bind(raise: Raise): Boolean {
return when (this) {
is CompletionResult.Failure -> {
val domainError = error.mapToDomainError()
raise.raise(domainError)
}
- is CompletionResult.Success -> { /* no-op */
- }
+ is CompletionResult.Success -> data
}
}
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..ea7c4ac652 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
@@ -32,6 +32,7 @@ import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
internal object LegacyScanProcessor {
@@ -115,24 +116,28 @@ internal object LegacyScanProcessor {
} else {
scope.launch {
delay(DELAY_SDK_DIALOG_CLOSE)
- disclaimerWillShow()
- store.dispatchWithMain(
- DisclaimerAction.Show(
- fromScreen = AppScreen.Home,
- callback = DisclaimerCallback(
- onAccept = {
- scope.launch(Dispatchers.Main) {
- nextHandler(scanResponse)
- }
- },
- onDismiss = {
- scope.launch(Dispatchers.Main) {
- onFailure(TangemSdkError.UserCancelled())
- }
- },
+
+ withContext(Dispatchers.Main.immediate) {
+ disclaimerWillShow()
+
+ store.dispatch(
+ DisclaimerAction.Show(
+ from = DisclaimerSource.Home,
+ callback = DisclaimerCallback(
+ onAccept = {
+ scope.launch(Dispatchers.Main.immediate) {
+ nextHandler(scanResponse)
+ }
+ },
+ onDismiss = {
+ scope.launch(Dispatchers.Main.immediate) {
+ onFailure(TangemSdkError.UserCancelled())
+ }
+ },
+ ),
),
- ),
- )
+ )
+ }
}
}
}
@@ -170,7 +175,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 +183,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/sdk/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt
index 0f5e408f5e..2dbfaec5b7 100644
--- a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt
@@ -70,9 +70,9 @@ interface TangemSdkManager {
suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
- ): CompletionResult
+ ): CompletionResult
- suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult
+ suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult
suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult
diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt
index 00bacdbe26..835a2fee69 100644
--- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt
@@ -202,7 +202,7 @@ class DefaultTangemSdkManager(
override suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
- ): CompletionResult {
+ ): CompletionResult {
return runTaskAsyncReturnOnMain(
runnable = ResetToFactorySettingsTask(
allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository,
@@ -210,10 +210,9 @@ class DefaultTangemSdkManager(
cardId = cardId,
initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)),
)
- .map { CardDTO(it) }
}
- override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult {
+ override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult {
return runTaskAsyncReturnOnMain(
runnable = ResetBackupCardTask(userWalletId),
initialMessage = Message(
diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt
index dcc850f798..0dffc198a7 100644
--- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt
@@ -87,12 +87,12 @@ class MockTangemSdkManager(
override suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
- ): CompletionResult {
- return MockProvider.getCardDto()
+ ): CompletionResult {
+ return CompletionResult.Success(true)
}
- override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult {
- return CompletionResult.Success(Unit)
+ override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult {
+ return CompletionResult.Success(true)
}
override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult {
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt
index 7de7ceb374..288ca22a9d 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt
@@ -19,11 +19,11 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
*/
internal class ResetBackupCardTask(
private val userWalletId: UserWalletId,
-) : CardSessionRunnable {
+) : CardSessionRunnable {
override val allowsRequestAccessCodeFromRepository: Boolean = false
- override fun run(session: CardSession, callback: CompletionCallback) {
+ override fun run(session: CardSession, callback: CompletionCallback) {
PreflightReadTask(
readMode = PreflightReadMode.FullCardRead,
filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId),
@@ -35,10 +35,10 @@ internal class ResetBackupCardTask(
}
}
- private fun resetCard(session: CardSession, callback: CompletionCallback) {
+ private fun resetCard(session: CardSession, callback: CompletionCallback) {
ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository).run(session) { result ->
when (result) {
- is CompletionResult.Success -> callback(CompletionResult.Success(Unit))
+ is CompletionResult.Success -> callback(CompletionResult.Success(result.data))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt
index f06f2f801a..51ff0b2d80 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt
@@ -10,13 +10,15 @@ import com.tangem.operations.wallet.PurgeWalletCommand
class ResetToFactorySettingsTask(
override val allowsRequestAccessCodeFromRepository: Boolean,
-) : CardSessionRunnable {
+) : CardSessionRunnable {
- override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) {
+ private var isResetCompleted = false
+
+ override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) {
deleteWallets(session, callback)
}
- private fun deleteWallets(session: CardSession, callback: (result: CompletionResult) -> Unit) {
+ private fun deleteWallets(session: CardSession, callback: (result: CompletionResult) -> Unit) {
val wallet = session.environment.card?.wallets?.lastOrNull().guard {
resetBackup(session, callback)
return
@@ -25,6 +27,7 @@ class ResetToFactorySettingsTask(
PurgeWalletCommand(wallet.publicKey).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
+ isResetCompleted = true
deleteWallets(session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
@@ -32,18 +35,18 @@ class ResetToFactorySettingsTask(
}
}
- private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) {
+ private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) {
if (session.environment.card?.backupStatus == null ||
session.environment.card?.backupStatus == Card.BackupStatus.NoBackup
) {
- callback(CompletionResult.Success(session.environment.card!!))
+ callback(CompletionResult.Success(isResetCompleted))
return
}
ResetBackupCommand().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
- callback(CompletionResult.Success(session.environment.card!!))
+ callback(CompletionResult.Success(true))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
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 8fae8e9abb..3e8529f150 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 4f9055bfb4..cea126bf75 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
@@ -508,6 +508,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/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt
index 7dc282cacd..c8576de418 100644
--- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt
+++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt
@@ -4,7 +4,6 @@ import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
-import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
@@ -21,7 +20,6 @@ object DemoHelper {
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
BackupAction.StartBackup::class.java,
- DetailsAction.ResetToFactory.Start::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
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..10bb1b383d 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
@@ -14,26 +14,6 @@ sealed class DetailsAction : Action {
val shouldSaveUserWallets: Boolean,
) : DetailsAction()
- data object ReCreateTwinsWallet : DetailsAction()
-
- sealed class ResetToFactory : DetailsAction() {
- data object Start : ResetToFactory()
- data object Proceed : ResetToFactory()
- data class AcceptCondition1(val accepted: Boolean) : ResetToFactory()
- data class AcceptCondition2(val accepted: Boolean) : ResetToFactory()
- data object Failure : ResetToFactory()
- data object Success : ResetToFactory()
-
- data class ShowDialog(val dialog: CardSettingsState.Dialog) : ResetToFactory()
-
- data object DismissDialog : ResetToFactory()
- }
-
- data object ScanCard : DetailsAction()
-
- data class PrepareCardSettingsData(val scanResponse: ScanResponse) : DetailsAction()
-
- data object ResetCardSettingsData : DetailsAction()
data object ScanAndSaveUserWallet : DetailsAction() {
data object Success : DetailsAction()
@@ -43,26 +23,6 @@ sealed class DetailsAction : Action {
data object DismissError : DetailsAction()
- sealed class AccessCodeRecovery : DetailsAction() {
- object Open : AccessCodeRecovery()
- data class SaveChanges(val enabled: Boolean) : AccessCodeRecovery() {
- data class Success(val enabled: Boolean) : AccessCodeRecovery()
- }
-
- data class SelectOption(val enabled: Boolean) : AccessCodeRecovery()
- }
-
- sealed class ManageSecurity : DetailsAction() {
- data object OpenSecurity : ManageSecurity()
- data class SelectOption(val option: SecurityOption) : ManageSecurity()
- data object SaveChanges : ManageSecurity() {
- data object Success : ManageSecurity()
- data object Failure : ManageSecurity()
- }
-
- data object ChangeAccessCode : ManageSecurity()
- }
-
sealed class AppSettings : DetailsAction() {
data class SwitchPrivacySetting(
val enable: Boolean,
@@ -96,6 +56,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..14a1147b89 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
@@ -1,40 +1,36 @@
package com.tangem.tap.features.details.redux
import androidx.lifecycle.LifecycleCoroutineScope
-import com.tangem.common.*
+import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
-import com.tangem.common.core.UserCodeRequestPolicy
+import com.tangem.common.doOnFailure
+import com.tangem.common.doOnSuccess
+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
import com.tangem.domain.apptheme.model.AppThemeMode
-import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletBuilder
-import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.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.*
-import com.tangem.tap.common.redux.AppDialog
+import com.tangem.tap.common.extensions.dispatchNavigationAction
+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.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
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
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -42,17 +38,13 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
class DetailsMiddleware {
- private val eraseWalletMiddleware = EraseWalletMiddleware()
- private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val appSettingsMiddleware = AppSettingsMiddleware()
- private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware()
val detailsMiddleware: Middleware = { _, stateProvider ->
{ next ->
{ action ->
@@ -69,152 +61,11 @@ class DetailsMiddleware {
private fun handleAction(state: DetailsState, action: Action) {
when (action) {
- is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
- is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state)
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
- is DetailsAction.ReCreateTwinsWallet -> {
- store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
- store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
- }
- is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action)
- is DetailsAction.ScanCard -> scanCard(state)
is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet()
}
}
- class EraseWalletMiddleware {
- @Suppress("CyclomaticComplexMethod")
- fun handle(action: DetailsAction.ResetToFactory) {
- val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
-
- when (action) {
- is DetailsAction.ResetToFactory.Start -> {
- val card = store.state.detailsState.cardSettingsState?.card ?: return
- if (card.isTangemTwins) {
- store.dispatch(DetailsAction.ReCreateTwinsWallet)
- return
- } else {
- store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
- }
- }
- is DetailsAction.ResetToFactory.Proceed -> {
- val card = store.state.detailsState.cardSettingsState?.card ?: return
- scope.launch {
- val userWalletId = UserWalletIdBuilder.card(card).build()
-
- // we must require a password regardless of biometric settings
- val policy = tangemSdkManager.userCodeRequestPolicy
- val doBeforeErase = {
- val type = if (card.isAccessCodeSet) {
- UserCodeType.AccessCode
- } else if (card.isPasscodeSet == true) {
- UserCodeType.Passcode
- } else {
- null
- }
-
- type?.let {
- tangemSdkManager.setUserCodeRequestPolicy(UserCodeRequestPolicy.Always(type))
- }
- }
-
- val doAfterErase = {
- tangemSdkManager.setUserCodeRequestPolicy(policy)
- }
-
- doBeforeErase()
- tangemSdkManager.resetToFactorySettings(card.cardId, true)
- .flatMap {
- userWalletsListManager.delete(listOfNotNull(userWalletId))
- }
- .flatMap {
- tangemSdkManager.deleteSavedUserCodes(setOf(card.cardId))
- }
- .doOnSuccess {
- Analytics.send(Settings.CardSettings.FactoryResetFinished())
-
- val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
- if (selectedUserWallet != null) {
- store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
- 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))
- } else {
- store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
- }
- }
- }
- .doOnFailure { error ->
- if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
- Analytics.send(Settings.CardSettings.FactoryResetFinished(error = error))
- }
- }
- .doOnResult {
- doAfterErase()
- }
- }
- }
- else -> Unit
- }
- }
- }
-
- class ManageSecurityMiddleware {
- @Suppress("ComplexMethod")
- fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) {
- when (action) {
- is DetailsAction.ManageSecurity.OpenSecurity -> {
- store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity))
- }
- is DetailsAction.ManageSecurity.SaveChanges -> {
- val cardSettingsState = detailsState.cardSettingsState
- val cardId = cardSettingsState?.card?.cardId
- val selectedOption = cardSettingsState?.manageSecurityState?.selectedOption
- scope.launch {
- val result = when (selectedOption) {
- SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
- SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)
- SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
- else -> return@launch
- }
- withContext(Dispatchers.Main) {
- val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
- when (result) {
- is CompletionResult.Success -> {
- Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
- store.dispatch(GlobalAction.UpdateSecurityOptions(selectedOption))
- store.dispatch(NavigationAction.PopBackTo())
- store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
- }
- is CompletionResult.Failure -> {
- val error = result.error
- if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
- Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
- }
- store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)
- }
- else -> Unit
- }
- }
- }
- }
- is DetailsAction.ManageSecurity.ChangeAccessCode -> {
- val card = store.state.detailsState.cardSettingsState?.card ?: return
- scope.launch {
- when (tangemSdkManager.setAccessCode(card.cardId)) {
- is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
- is CompletionResult.Failure -> {}
- }
- }
- }
- else -> Unit
- }
- }
- }
-
class AppSettingsMiddleware {
private val checkBiometricsStatusJobHolder = JobHolder()
@@ -246,6 +97,7 @@ class DetailsMiddleware {
is DetailsAction.AppSettings.SwitchPrivacySetting.Success,
is DetailsAction.AppSettings.SwitchPrivacySetting.Failure,
is DetailsAction.AppSettings.BiometricsStatusChanged,
+ is DetailsAction.AppSettings.Prepare,
-> Unit
}
}
@@ -274,7 +126,7 @@ class DetailsMiddleware {
private fun enrollBiometrics() {
Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication)
- store.dispatchOnMain(NavigationAction.OpenBiometricsSettings)
+ activityResultCaller.openSystemBiometrySettings()
}
private fun changeAppThemeMode(appThemeMode: AppThemeMode) {
@@ -395,7 +247,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
- store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Home))
+ store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
return CompletionResult.Success(Unit)
}
@@ -429,63 +281,6 @@ class DetailsMiddleware {
}
}
- class AccessCodeRecoveryMiddleware {
- fun handle(state: DetailsState, action: DetailsAction.AccessCodeRecovery) {
- when (action) {
- is DetailsAction.AccessCodeRecovery.Open -> {
- Analytics.send(Settings.CardSettings.AccessCodeRecoveryButton())
- store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery))
- }
- is DetailsAction.AccessCodeRecovery.SaveChanges -> {
- scope.launch {
- tangemSdkManager
- .setAccessCodeRecoveryEnabled(state.cardSettingsState?.card?.cardId, action.enabled)
- .doOnSuccess {
- Analytics.send(
- Settings.CardSettings.AccessCodeRecoveryChanged(
- AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled),
- ),
- )
- store.dispatchOnMain(NavigationAction.PopBackTo())
- store.dispatchOnMain(
- DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled),
- )
- }
- }
- }
- is DetailsAction.AccessCodeRecovery.SelectOption -> Unit
- is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> Unit
- }
- }
- }
-
- private fun scanCard(state: DetailsState) = scope.launch {
- store.inject(DaggerGraphState::scanCardProcessor)
- .scan(allowsRequestAccessCodeFromRepository = true)
- .doOnSuccess { scanResponse ->
- // if we use biometric, scanResponse in GlobalState is null, and crashes NPE on twin cards
- store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
- val currentUserWalletId = state.scanResponse
- ?.let { UserWalletIdBuilder.scanResponse(it).build() }
- val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse)
- .build()
- val isSameWallet = currentUserWalletId == scannedUserWalletId
-
- if (isSameWallet) {
- store.dispatchOnMain(
- DetailsAction.PrepareCardSettingsData(scanResponse = scanResponse),
- )
- } else {
- store.dispatchDialogShow(
- AppDialog.SimpleOkDialogRes(
- headerId = R.string.common_warning,
- messageId = R.string.error_wrong_wallet_tapped,
- ),
- )
- }
- }
- }
-
private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) {
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)
@@ -502,7 +297,7 @@ class DetailsMiddleware {
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
},
disclaimerWillShow = {
- store.dispatchOnMain(NavigationAction.PopBackTo())
+ store.dispatchNavigationAction(AppRouter::pop)
},
onSuccess = { scanResponse ->
createUserWallet(scanResponse)
@@ -546,7 +341,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 +349,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..dbb450b426 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,21 +1,15 @@
package com.tangem.tap.features.details.redux
-import com.tangem.core.navigation.AppScreen
import com.tangem.domain.apptheme.model.AppThemeMode
-import com.tangem.domain.common.CardTypesResolver
-import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
-import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
-import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.runBlocking
import org.rekotlin.Action
-import java.util.EnumSet
object DetailsReducer {
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
@@ -29,16 +23,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.PrepareScreen -> {
handlePrepareScreen(action, state)
}
- is DetailsAction.PrepareCardSettingsData -> {
- handlePrepareCardSettingsScreen(scanResponse = action.scanResponse, state = detailsState)
- }
- is DetailsAction.ResetCardSettingsData -> detailsState.copy(cardSettingsState = null)
- is DetailsAction.ResetToFactory -> {
- handleEraseWallet(action, detailsState)
- }
- is DetailsAction.ManageSecurity -> {
- handleSecurityAction(action, detailsState)
- }
is DetailsAction.AppSettings -> {
handlePrivacyAction(action, detailsState)
}
@@ -47,7 +31,6 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
selectedAppCurrency = action.currency,
),
)
- is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy(
isScanningInProgress = true,
)
@@ -61,19 +44,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.DismissError -> detailsState.copy(
error = null,
)
- else -> detailsState
}
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState {
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) {
- state.detailsState.cardSettingsState
- } else {
- null
- },
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
appSettingsState = AppSettingsState(
isBiometricsAvailable = runBlocking {
@@ -96,139 +72,6 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppS
)
}
-private fun handlePrepareCardSettingsScreen(scanResponse: ScanResponse, state: DetailsState): DetailsState {
- val cardTypesResolver = scanResponse.cardTypesResolver
- val card = scanResponse.card
- val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
- val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
-
- val cardSettingsState = CardSettingsState(
- cardInfo = card.toCardInfo(cardTypesResolver),
- scanResponse = scanResponse,
- manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
- card = scanResponse.card,
- resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
- resetButtonEnabled = false,
- condition1Checked = false,
- condition2Checked = false,
- accessCodeRecovery = if (cardTypesResolver.isWallet2()) {
- val enabled = card.userSettings?.isUserCodeRecoveryAllowed ?: false
- AccessCodeRecoveryState(
- enabledOnCard = enabled,
- enabledSelection = enabled,
- )
- } else {
- null
- },
- isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
- dialog = null,
- )
-
- return state.copy(cardSettingsState = cardSettingsState)
-}
-
-private fun prepareSecurityOptions(card: CardDTO, cardTypesResolver: CardTypesResolver): ManageSecurityState {
- val securityOption = when {
- card.isAccessCodeSet -> {
- SecurityOption.AccessCode
- }
-
- card.isPasscodeSet == true -> {
- SecurityOption.PassCode
- }
-
- else -> {
- SecurityOption.LongTap
- }
- }
- val allowedSecurityOptions = when {
- cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> {
- EnumSet.of(SecurityOption.LongTap)
- }
- card.settings.isBackupAllowed -> {
- EnumSet.of(securityOption)
- }
- else ->
- prepareAllowedSecurityOptions(cardTypesResolver = cardTypesResolver, currentSecurityOption = securityOption)
- }
- return ManageSecurityState(
- currentOption = securityOption,
- allowedOptions = allowedSecurityOptions,
- selectedOption = securityOption,
- )
-}
-
-private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
- val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
- val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
- return !isNotAllowed
-}
-
-private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState {
- val cardSettingsState = state.cardSettingsState
- return when (action) {
- is DetailsAction.ResetToFactory.AcceptCondition1 -> {
- val warning1Checked = action.accepted
- val resetButtonEnabled = if (cardSettingsState?.isShowPasswordResetRadioButton == true) {
- warning1Checked && cardSettingsState.condition2Checked
- } else {
- warning1Checked
- }
- state.copy(
- cardSettingsState = cardSettingsState?.copy(
- condition1Checked = action.accepted,
- resetButtonEnabled = resetButtonEnabled,
- ),
- )
- }
- is DetailsAction.ResetToFactory.AcceptCondition2 -> {
- val warning2Checked = action.accepted
- state.copy(
- cardSettingsState = cardSettingsState?.copy(
- condition2Checked = action.accepted,
- resetButtonEnabled = warning2Checked && cardSettingsState.condition1Checked,
- ),
- )
- }
- is DetailsAction.ResetToFactory.ShowDialog -> {
- state.copy(cardSettingsState = cardSettingsState?.copy(dialog = action.dialog))
- }
- is DetailsAction.ResetToFactory.DismissDialog -> {
- state.copy(cardSettingsState = cardSettingsState?.copy(dialog = null))
- }
- else -> state
- }
-}
-
-private fun handleSecurityAction(action: DetailsAction.ManageSecurity, state: DetailsState): DetailsState {
- return when (action) {
- is DetailsAction.ManageSecurity.SelectOption -> {
- val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
- selectedOption = action.option,
- )
- state.copy(cardSettingsState = state.cardSettingsState?.copy(manageSecurityState = manageSecurityState))
- }
- is DetailsAction.ManageSecurity.SaveChanges.Success -> {
- // Setting options to show only LongTap from now on for non-twins
- val manageSecurityState = state.cardSettingsState?.manageSecurityState?.copy(
- currentOption = state.cardSettingsState.manageSecurityState.selectedOption,
- allowedOptions = state.scanResponse?.cardTypesResolver?.let {
- prepareAllowedSecurityOptions(
- cardTypesResolver = it,
- currentSecurityOption = state.cardSettingsState.manageSecurityState.selectedOption,
- )
- } ?: EnumSet.of(SecurityOption.LongTap),
- )
- state.copy(
- cardSettingsState = state.cardSettingsState?.copy(
- manageSecurityState = manageSecurityState,
- ),
- )
- }
- else -> state
- }
-}
-
private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState {
return when (action) {
is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy(
@@ -281,69 +124,11 @@ 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
}
-}
-
-private fun handleAccessCodeRecoveryAction(
- action: DetailsAction.AccessCodeRecovery,
- state: DetailsState,
-): DetailsState {
- return when (action) {
- DetailsAction.AccessCodeRecovery.Open -> {
- val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
- enabledSelection = state.cardSettingsState.accessCodeRecovery.enabledOnCard,
- )
- state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
- }
- is DetailsAction.AccessCodeRecovery.SaveChanges -> state
- is DetailsAction.AccessCodeRecovery.SelectOption -> {
- val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
- enabledSelection = action.enabled,
- )
- state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
- }
- is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> {
- val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
- enabledOnCard = action.enabled,
- enabledSelection = action.enabled,
- )
- state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
- }
- }
-}
-
-private fun prepareAllowedSecurityOptions(
- cardTypesResolver: CardTypesResolver,
- currentSecurityOption: SecurityOption?,
-): EnumSet {
- val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
-
- if (cardTypesResolver.isTangemTwins()) {
- allowedSecurityOptions.add(SecurityOption.PassCode)
- }
- if (currentSecurityOption == SecurityOption.AccessCode) {
- allowedSecurityOptions.add(SecurityOption.AccessCode)
- }
- if (currentSecurityOption == SecurityOption.PassCode) {
- allowedSecurityOptions.add(SecurityOption.PassCode)
- }
- return allowedSecurityOptions
-}
-
-@Suppress("MagicNumber")
-private fun CardDTO.toCardInfo(cardTypesResolver: CardTypesResolver): CardInfo {
- val cardId = this.cardId.chunked(4).joinToString(separator = " ")
- val issuer = this.issuer.name
- val signedHashes = this.signedHashesCount()
-
- return CardInfo(
- cardId = cardId,
- issuer = issuer,
- signedHashes = signedHashes,
- isTwin = cardTypesResolver.isTangemTwins(),
- hasBackup = backupStatus?.isActive == true,
- )
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
index 8e7e583ecf..c35d0c8d83 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
@@ -3,15 +3,11 @@ package com.tangem.tap.features.details.redux
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
-import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
-import com.tangem.tap.common.entities.Button
import org.rekotlin.StateType
-import java.util.EnumSet
data class DetailsState(
val scanResponse: ScanResponse? = null,
- val cardSettingsState: CardSettingsState? = null,
val privacyPolicyUrl: String? = null,
val createBackupAllowed: Boolean = false,
val isScanningInProgress: Boolean = false,
@@ -19,52 +15,13 @@ data class DetailsState(
val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType
-data class CardInfo(
- val cardId: String,
- val issuer: String,
- val signedHashes: Int,
- val isTwin: Boolean,
- val hasBackup: Boolean,
-)
-
-/**
- * @property enabledOnCard whether access code recovery is enabled on card
- * @property enabledSelection current selected option in app (not saved on card yet)
- */
-data class AccessCodeRecoveryState(
- val enabledOnCard: Boolean,
- val enabledSelection: Boolean,
-)
-
-data class CardSettingsState(
- val cardInfo: CardInfo,
- val card: CardDTO,
- val scanResponse: ScanResponse,
- val manageSecurityState: ManageSecurityState?,
- val resetCardAllowed: Boolean,
- val resetButtonEnabled: Boolean,
- val condition1Checked: Boolean,
- val condition2Checked: Boolean,
- val accessCodeRecovery: AccessCodeRecoveryState? = null,
- val isShowPasswordResetRadioButton: Boolean,
- val dialog: Dialog?,
-) {
-
- sealed interface Dialog {
- data object StartResetDialog : Dialog
- data object ContinueResetDialog : Dialog
- data object InterruptedResetDialog : Dialog
- data object CompletedResetDialog : Dialog
- }
+sealed class ResetCardDialog {
+ data object StartResetDialog : ResetCardDialog()
+ data object ContinueResetDialog : ResetCardDialog()
+ data object InterruptedResetDialog : ResetCardDialog()
+ data object CompletedResetDialog : ResetCardDialog()
}
-data class ManageSecurityState(
- val currentOption: SecurityOption = SecurityOption.LongTap,
- val selectedOption: SecurityOption = currentOption,
- val allowedOptions: EnumSet = EnumSet.allOf(SecurityOption::class.java),
- val buttonProceed: Button = Button(true),
-)
-
data class AppSettingsState(
val saveWallets: Boolean = false,
val saveAccessCodes: Boolean = false,
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 3afa643211..6b0e0898ec 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
@@ -65,14 +63,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 f407924cca..d1a9368288 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..9036d7d1fd 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,11 +6,11 @@ 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.features.details.redux.DetailsAction
+import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -35,8 +35,7 @@ internal class AppSettingsFragment : ComposeFragment() {
modifier = modifier,
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..328815f377 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
@@ -1,14 +1,12 @@
package com.tangem.tap.features.details.ui.cardsettings
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.fragment.app.viewModels
-import com.tangem.core.navigation.NavigationAction
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
-import com.tangem.tap.features.details.redux.DetailsAction
-import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@@ -22,15 +20,8 @@ internal class CardSettingsFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
- LocalLifecycleOwner.current.lifecycle.addObserver(viewModel)
+ val state by viewModel.screenState.collectAsStateWithLifecycle()
- CardSettingsScreen(
- modifier = modifier,
- state = viewModel.screenState.value,
- onBackClick = {
- store.dispatch(DetailsAction.ResetCardSettingsData)
- store.dispatch(NavigationAction.PopBackTo())
- },
- )
+ CardSettingsScreen(modifier = modifier, state = state)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt
index b0e2785b32..57f1103703 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt
@@ -1,10 +1,13 @@
package com.tangem.tap.features.details.ui.cardsettings
import android.content.res.Configuration
-import androidx.compose.foundation.*
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -13,18 +16,14 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
-import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@Composable
-internal fun CardSettingsScreen(
- state: CardSettingsScreenState,
- onBackClick: () -> Unit,
- modifier: Modifier = Modifier,
-) {
+internal fun CardSettingsScreen(state: CardSettingsScreenState, modifier: Modifier = Modifier) {
val needReadCard = state.cardDetails == null
SettingsScreensScaffold(
@@ -37,7 +36,7 @@ internal fun CardSettingsScreen(
}
},
titleRes = R.string.card_settings_title,
- onBackClick = onBackClick,
+ onBackClick = state.onBackClick,
)
}
@@ -180,7 +179,7 @@ private fun CardSettings(state: CardSettingsScreenState) {
// region Preview
@Composable
private fun CardSettingsScreenStateSample() {
- CardSettingsScreen(state = CardSettingsScreenState(onScanCardClick = {}, onElementClick = {}), {})
+ CardSettingsScreen(state = CardSettingsScreenState(onBackClick = {}, onScanCardClick = {}, onElementClick = {}))
}
@Preview(showBackground = true, widthDp = 360)
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt
index 4cf5e68186..bbde2c8579 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt
@@ -4,18 +4,15 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
-import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
-import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
import com.tangem.wallet.R
-import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
internal data class CardSettingsScreenState(
val cardDetails: List? = null,
- val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
+ val onBackClick: () -> Unit,
)
internal sealed class CardInfo(
@@ -44,7 +41,7 @@ internal sealed class CardInfo(
clickable = clickable,
)
- object ChangeAccessCode : CardInfo(
+ data object ChangeAccessCode : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_change_access_code),
subtitle = TextReference.Res(R.string.card_settings_change_access_code_footer),
clickable = true,
@@ -60,9 +57,9 @@ internal sealed class CardInfo(
clickable = true,
)
- class ResetToFactorySettings(cardInfo: ReduxCardInfo) : CardInfo(
+ class ResetToFactorySettings(description: TextReference) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
- subtitle = cardInfo.toResetCardDescriptionText(),
+ subtitle = description,
clickable = true,
)
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt
index 64c00b50f4..7f972b948d 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt
@@ -1,105 +1,132 @@
package com.tangem.tap.features.details.ui.cardsettings
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
-import androidx.lifecycle.DefaultLifecycleObserver
-import androidx.lifecycle.LifecycleOwner
+import android.os.Bundle
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
-import arrow.core.Either
+import androidx.lifecycle.viewModelScope
+import com.tangem.common.CompletionResult
+import com.tangem.common.doOnSuccess
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
+import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.TapWorkarounds.isTangemTwins
-import com.tangem.domain.common.getTwinCardIdForUser
-import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
+import com.tangem.domain.card.ScanCardProcessor
+import com.tangem.domain.common.CardTypesResolver
+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.builder.UserWalletIdBuilder
+import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
-import com.tangem.tap.features.details.redux.CardSettingsState
-import com.tangem.tap.features.details.redux.DetailsAction
-import com.tangem.tap.features.details.redux.DetailsState
+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.domain.extensions.signedHashesCount
+import com.tangem.tap.domain.sdk.TangemSdkManager
+import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
+import com.tangem.tap.features.details.ui.common.utils.*
+import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
+import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.store
+import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
-import org.rekotlin.StoreSubscriber
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@HiltViewModel
internal class CardSettingsViewModel @Inject constructor(
- private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
-) :
- ViewModel(), DefaultLifecycleObserver, StoreSubscriber {
+ private val scanCardProcessor: ScanCardProcessor,
+ private val tangemSdkManager: TangemSdkManager,
+ private val cardSettingsInteractor: CardSettingsInteractor,
+ savedStateHandle: SavedStateHandle,
+) : ViewModel() {
- var screenState: MutableState =
- mutableStateOf(updateState(store.state.detailsState.cardSettingsState))
+ private val userWalletId = savedStateHandle.get(AppRoute.CardSettings.USER_WALLET_ID_KEY)
+ ?.unbundle(UserWalletId.serializer())
+ ?: error("User wallet ID is required for CardSettingsViewModel")
- override fun onStart(owner: LifecycleOwner) {
- when (val selectedWalletEither = getSelectedWalletSyncUseCase()) {
- is Either.Left -> {
- Timber.e(selectedWalletEither.value.toString())
+ val screenState: MutableStateFlow = MutableStateFlow(getInitialState())
+
+ init {
+ cardSettingsInteractor.scannedScanResponse
+ .filterNotNull()
+ .onEach(::updateCardDetails)
+ .launchIn(viewModelScope)
+ }
+
+ private fun getInitialState() = CardSettingsScreenState(
+ cardDetails = null,
+ onElementClick = ::handleClickingItem,
+ onScanCardClick = ::scanCard,
+ onBackClick = ::onBackClick,
+ )
+
+ private fun scanCard() = viewModelScope.launch {
+ scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true)
+ .doOnSuccess { scanResponse ->
+ val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
+ if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) {
+ cardSettingsInteractor.initialize(scanResponse)
+ } else {
+ store.dispatchDialogShow(
+ AppDialog.SimpleOkDialogRes(
+ headerId = R.string.common_warning,
+ messageId = R.string.error_wrong_wallet_tapped,
+ ),
+ )
+ }
+ }
+ }
+
+ private fun updateCardDetails(scanResponse: ScanResponse) {
+ val card = scanResponse.card
+ val cardTypesResolver = scanResponse.cardTypesResolver
+ val cardId = cardTypesResolver.getCardId()
+
+ val currentSecurityOption = getCurrentSecurityOption(card)
+ val allowedSecurityOptions = getAllowedSecurityOptions(
+ card = card,
+ cardTypesResolver = cardTypesResolver,
+ currentSecurityOption = currentSecurityOption,
+ )
+ val isResetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver)
+
+ val cardDetails = buildList {
+ CardInfo.CardId(cardId).let(::add)
+ CardInfo.Issuer(card.issuer.name).let(::add)
+
+ if (!cardTypesResolver.isTangemTwins()) {
+ CardInfo.SignedHashes(card.signedHashesCount().toString()).let(::add)
+ }
+
+ CardInfo.SecurityMode(
+ currentSecurityOption,
+ clickable = allowedSecurityOptions.size > 1,
+ ).let(::add)
+
+ if (card.backupStatus?.isActive == true && card.isAccessCodeSet) {
+ CardInfo.ChangeAccessCode.let(::add)
+ }
+
+ if (isAccessCodeRecoveryAllowed(cardTypesResolver)) {
+ CardInfo.AccessCodeRecovery(isAccessCodeRecoveryEnabled(cardTypesResolver, card)).let(::add)
+ }
+
+ if (isResetCardAllowed) {
+ CardInfo.ResetToFactorySettings(
+ description = getResetToFactoryDescription(
+ isActiveBackupStatus = card.backupStatus?.isActive == true,
+ typesResolver = cardTypesResolver,
+ ),
+ ).let(::add)
}
- is Either.Right -> Unit
}
- store.subscribe(this) { state ->
- state.skipRepeats { oldState, newState ->
- oldState.detailsState == newState.detailsState
- }.select { it.detailsState }
- }
- }
-
- override fun onStop(owner: LifecycleOwner) {
- store.unsubscribe(this)
- }
-
- override fun newState(state: DetailsState) {
- screenState.value = updateState(state.cardSettingsState)
- }
-
- private fun updateState(state: CardSettingsState?): CardSettingsScreenState {
- return if (state?.manageSecurityState == null) {
- CardSettingsScreenState(
- cardDetails = null,
- accessCodeRecoveryState = null,
- onElementClick = {},
- onScanCardClick = {
- store.dispatch(DetailsAction.ScanCard)
- },
- )
- } else {
- val cardId = if (state.card.isTangemTwins) {
- state.card.getTwinCardIdForUser()
- } else {
- state.cardInfo.cardId
- }
- val cardDetails: MutableList = mutableListOf(
- CardInfo.CardId(cardId),
- CardInfo.Issuer(state.cardInfo.issuer),
- )
-
- if (!state.card.isTangemTwins) {
- cardDetails.add(CardInfo.SignedHashes(state.cardInfo.signedHashes.toString()))
- }
- cardDetails.add(
- CardInfo.SecurityMode(
- securityOption = state.manageSecurityState.currentOption,
- clickable = state.manageSecurityState.allowedOptions.size > 1,
- ),
- )
- if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) {
- cardDetails.add(CardInfo.ChangeAccessCode)
- }
- if (state.accessCodeRecovery != null) {
- cardDetails.add(CardInfo.AccessCodeRecovery(state.accessCodeRecovery.enabledOnCard))
- }
- if (state.resetCardAllowed) {
- cardDetails.add(CardInfo.ResetToFactorySettings(state.cardInfo))
- }
- CardSettingsScreenState(
- cardDetails = cardDetails,
- accessCodeRecoveryState = state.accessCodeRecovery,
- onScanCardClick = { },
- onElementClick = {
- handleClickingItem(it)
- },
- )
+ screenState.update { state ->
+ state.copy(cardDetails = cardDetails)
}
}
@@ -107,20 +134,77 @@ internal class CardSettingsViewModel @Inject constructor(
when (item) {
is CardInfo.ChangeAccessCode -> {
Analytics.send(Settings.CardSettings.ButtonChangeUserCode(AnalyticsParam.UserCode.AccessCode))
- store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode)
+ changeAccessCode()
}
is CardInfo.ResetToFactorySettings -> {
Analytics.send(Settings.CardSettings.ButtonFactoryReset())
- store.dispatch(DetailsAction.ResetToFactory.Start)
+ resetWalletToFactorySettings()
}
is CardInfo.SecurityMode -> {
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
- store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
+ store.dispatchNavigationAction {
+ push(route = AppRoute.DetailsSecurity(userWalletId))
+ }
}
is CardInfo.AccessCodeRecovery -> {
- store.dispatch(DetailsAction.AccessCodeRecovery.Open)
+ store.dispatchNavigationAction { push(AppRoute.AccessCodeRecovery) }
}
else -> {}
}
}
+
+ private fun resetWalletToFactorySettings() {
+ val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) {
+ "Impossible to reset card if ScanResponse is null"
+ }
+
+ if (scanResponse.cardTypesResolver.isTangemTwins()) {
+ store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse)))
+
+ cardSettingsInteractor.clear()
+
+ store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) }
+ } else {
+ val card = scanResponse.card
+
+ store.dispatchNavigationAction {
+ push(
+ route = AppRoute.ResetToFactory(
+ userWalletId = userWalletId,
+ cardId = card.cardId,
+ isActiveBackupStatus = card.backupStatus?.isActive == true,
+ backupCardsCount = when (val status = card.backupStatus) {
+ is CardDTO.BackupStatus.Active -> status.cardCount
+ is CardDTO.BackupStatus.CardLinked,
+ CardDTO.BackupStatus.NoBackup,
+ null,
+ -> 0
+ },
+ ),
+ )
+ }
+ }
+ }
+
+ private fun changeAccessCode() = viewModelScope.launch {
+ val scanResponse = requireNotNull(cardSettingsInteractor.scannedScanResponse.value) { "Scan response is null" }
+
+ when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) {
+ is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged())
+ is CompletionResult.Failure -> {
+ Timber.e("Failed to change access code: ${result.error}")
+ }
+ }
+ }
+
+ private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean {
+ val hasPermanentWallet = card.wallets.any { it.settings.isPermanent }
+ val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin()
+ return !isNotAllowed
+ }
+
+ private fun onBackClick() {
+ cardSettingsInteractor.clear()
+ store.dispatchNavigationAction(AppRouter::pop)
+ }
}
\ No newline at end of file
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..bcfe7cd28d 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
@@ -1,54 +1,33 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import com.tangem.core.navigation.NavigationAction
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
-import com.tangem.tap.features.details.redux.DetailsState
+import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
-import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
-class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber {
+class AccessCodeRecoveryFragment : ComposeFragment() {
- private val viewModel = AccessCodeRecoveryViewModel(store)
-
- private var screenState: MutableState =
- mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
+ private val viewModel: AccessCodeRecoveryViewModel by viewModels()
@Inject
override lateinit var uiDependencies: UiDependencies
@Composable
override fun ScreenContent(modifier: Modifier) {
+ val state by viewModel.screenState.collectAsStateWithLifecycle()
+
AccessCodeRecoveryScreen(
- state = screenState.value,
- onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
+ state = state,
+ onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
-
- override fun onStart() {
- super.onStart()
- store.subscribe(this) { state ->
- state.skipRepeats { oldState, newState ->
- oldState.detailsState == newState.detailsState
- }.select { it.detailsState }
- }
- }
-
- override fun onStop() {
- super.onStop()
- store.unsubscribe(this)
- }
-
- override fun newState(state: DetailsState) {
- if (activity == null || view == null) return
- screenState.value =
- viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery)
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt
index 12edf5a47e..d8107e0c4d 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt
@@ -1,10 +1,6 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
@@ -57,7 +53,7 @@ fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
DetailsMainButton(
title = stringResource(id = R.string.common_save_changes),
enabled = state.isSaveChangesEnabled,
- onClick = { state.onSaveChangesClick(state.enabledSelection) },
+ onClick = { state.onSaveChangesClick() },
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
)
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt
index f1a825067a..b4b6c68535 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt
@@ -11,6 +11,6 @@ data class AccessCodeRecoveryScreenState(
val enabledOnCard: Boolean,
val enabledSelection: Boolean,
val isSaveChangesEnabled: Boolean,
- val onSaveChangesClick: (Boolean) -> Unit,
+ val onSaveChangesClick: () -> Unit,
val onOptionClick: (Boolean) -> Unit,
)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt
index a2948075f8..d65fb74a54 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt
@@ -1,29 +1,83 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
-import com.tangem.tap.common.redux.AppState
-import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
-import com.tangem.tap.features.details.redux.DetailsAction
-import org.rekotlin.Store
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.tangem.common.doOnSuccess
+import com.tangem.common.routing.AppRouter
+import com.tangem.core.analytics.Analytics
+import com.tangem.domain.common.util.cardTypesResolver
+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.domain.sdk.TangemSdkManager
+import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
+import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled
+import com.tangem.tap.store
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
-class AccessCodeRecoveryViewModel(val store: Store) {
+@HiltViewModel
+internal class AccessCodeRecoveryViewModel @Inject constructor(
+ private val tangemSdkManager: TangemSdkManager,
+ private val cardSettingsInteractor: CardSettingsInteractor,
+) : ViewModel() {
- fun updateState(state: AccessCodeRecoveryState?): AccessCodeRecoveryScreenState {
- // We shouldn't get to this screen here when this state is null
- return if (state == null) {
- AccessCodeRecoveryScreenState(
- enabledOnCard = false,
- enabledSelection = false,
- isSaveChangesEnabled = false,
- onSaveChangesClick = {},
- onOptionClick = {},
- )
- } else {
- AccessCodeRecoveryScreenState(
- enabledOnCard = state.enabledOnCard,
- enabledSelection = state.enabledSelection,
- isSaveChangesEnabled = state.enabledOnCard != state.enabledSelection,
- onSaveChangesClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SaveChanges(it)) },
- onOptionClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SelectOption(it)) },
+ private val scannedScanResponse = cardSettingsInteractor.scannedScanResponse.value
+ ?: error("Scan response is null")
+
+ val screenState = MutableStateFlow(
+ value = getInitialState(),
+ )
+
+ private fun getInitialState(): AccessCodeRecoveryScreenState {
+ val isEnabled = isAccessCodeRecoveryEnabled(
+ typeResolver = scannedScanResponse.cardTypesResolver,
+ card = scannedScanResponse.card,
+ )
+
+ return AccessCodeRecoveryScreenState(
+ enabledOnCard = isEnabled,
+ enabledSelection = isEnabled,
+ isSaveChangesEnabled = false,
+ onSaveChangesClick = ::saveChanges,
+ onOptionClick = ::selectOption,
+ )
+ }
+
+ private fun saveChanges() = viewModelScope.launch {
+ val isEnabled = screenState.value.enabledSelection
+
+ tangemSdkManager
+ .setAccessCodeRecoveryEnabled(scannedScanResponse.card.cardId, isEnabled)
+ .doOnSuccess {
+ Analytics.send(
+ Settings.CardSettings.AccessCodeRecoveryChanged(
+ AnalyticsParam.AccessCodeRecoveryStatus.from(isEnabled),
+ ),
+ )
+
+ cardSettingsInteractor.update { scanResponse ->
+ scanResponse.copy(
+ card = scanResponse.card.copy(
+ userSettings = scanResponse.card.userSettings?.copy(
+ isUserCodeRecoveryAllowed = isEnabled,
+ ),
+ ),
+ )
+ }
+
+ store.dispatchNavigationAction(AppRouter::pop)
+ }
+ }
+
+ private fun selectOption(isEnabled: Boolean) {
+ screenState.update {
+ it.copy(
+ enabledSelection = isEnabled,
+ isSaveChangesEnabled = isEnabled != it.enabledOnCard,
)
}
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt
new file mode 100644
index 0000000000..73bbf10991
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/domain/CardSettingsInteractor.kt
@@ -0,0 +1,35 @@
+package com.tangem.tap.features.details.ui.cardsettings.domain
+
+import com.tangem.domain.models.scan.ScanResponse
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Interactor for sharing logic and data between all card settings screens
+ *
+[REDACTED_AUTHOR]
+ */
+@Singleton
+internal class CardSettingsInteractor @Inject constructor() {
+
+ private val _scannedScanResponse = MutableStateFlow(value = null)
+ val scannedScanResponse: StateFlow = _scannedScanResponse
+
+ fun initialize(scanResponse: ScanResponse) {
+ _scannedScanResponse.value = scanResponse
+ }
+
+ fun update(transform: (ScanResponse) -> ScanResponse) {
+ _scannedScanResponse.update {
+ requireNotNull(it)
+ transform(it)
+ }
+ }
+
+ fun clear() {
+ _scannedScanResponse.value = null
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt
index 35b36c0f00..8cdc6777a9 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt
@@ -13,8 +13,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconEnd
-import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.wallet.R
@Composable
@@ -24,31 +24,42 @@ internal fun SettingsScreensScaffold(
modifier: Modifier = Modifier,
@StringRes titleRes: Int? = null,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
+ addBottomInsets: Boolean = true,
fab: @Composable () -> 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/common/utils/AccessCodeRecovery.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/AccessCodeRecovery.kt
new file mode 100644
index 0000000000..c08b50c4e5
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/AccessCodeRecovery.kt
@@ -0,0 +1,13 @@
+package com.tangem.tap.features.details.ui.common.utils
+
+import com.tangem.domain.common.CardTypesResolver
+import com.tangem.domain.models.scan.CardDTO
+
+internal fun isAccessCodeRecoveryAllowed(typeResolver: CardTypesResolver): Boolean = typeResolver.isWallet2()
+
+internal fun isAccessCodeRecoveryEnabled(typeResolver: CardTypesResolver, card: CardDTO): Boolean =
+ if (typeResolver.isWallet2()) {
+ card.userSettings?.isUserCodeRecoveryAllowed ?: false
+ } else {
+ false
+ }
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt
new file mode 100644
index 0000000000..b269ee6352
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt
@@ -0,0 +1,16 @@
+package com.tangem.tap.features.details.ui.common.utils
+
+import com.tangem.domain.common.CardTypesResolver
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+import com.tangem.wallet.R
+
+internal fun getResetToFactoryDescription(
+ isActiveBackupStatus: Boolean,
+ typesResolver: CardTypesResolver,
+): TextReference {
+ return if (!isActiveBackupStatus || typesResolver.isTangemTwins()) {
+ TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
+ } else {
+ TextReference.Res(R.string.reset_card_with_backup_to_factory_message)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/SecurityOptionsUtils.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/SecurityOptionsUtils.kt
new file mode 100644
index 0000000000..0a62abe82e
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/SecurityOptionsUtils.kt
@@ -0,0 +1,44 @@
+package com.tangem.tap.features.details.ui.common.utils
+
+import com.tangem.domain.common.CardTypesResolver
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.tap.features.details.redux.SecurityOption
+import java.util.EnumSet
+
+internal fun getCurrentSecurityOption(card: CardDTO): SecurityOption = when {
+ card.isAccessCodeSet -> SecurityOption.AccessCode
+ card.isPasscodeSet == true -> SecurityOption.PassCode
+ else -> SecurityOption.LongTap
+}
+
+internal fun getAllowedSecurityOptions(
+ card: CardDTO,
+ cardTypesResolver: CardTypesResolver,
+ currentSecurityOption: SecurityOption,
+): EnumSet = when {
+ cardTypesResolver.isStart2Coin() || cardTypesResolver.isTangemNote() -> EnumSet.of(SecurityOption.LongTap)
+ card.settings.isBackupAllowed -> EnumSet.of(currentSecurityOption)
+ else -> prepareAllowedSecurityOptions(
+ cardTypesResolver = cardTypesResolver,
+ currentSecurityOption = currentSecurityOption,
+ )
+}
+
+private fun prepareAllowedSecurityOptions(
+ cardTypesResolver: CardTypesResolver,
+ currentSecurityOption: SecurityOption?,
+): EnumSet {
+ val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap)
+
+ if (cardTypesResolver.isTangemTwins()) {
+ allowedSecurityOptions.add(SecurityOption.PassCode)
+ }
+ if (currentSecurityOption == SecurityOption.AccessCode) {
+ allowedSecurityOptions.add(SecurityOption.AccessCode)
+ }
+ if (currentSecurityOption == SecurityOption.PassCode) {
+ allowedSecurityOptions.add(SecurityOption.PassCode)
+ }
+
+ return allowedSecurityOptions
+}
\ No newline at end of file
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_more_cards,
+ iconResId = R.drawable.ic_more_cards_24,
title = resourceReference(R.string.details_row_title_create_backup),
)
data class CardSettings(
override val onClick: () -> 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..d2c84a43db 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
@@ -40,7 +39,6 @@ import kotlinx.coroutines.flow.onEach
import org.rekotlin.Store
import timber.log.Timber
-// TODO: change to Android ViewModel [REDACTED_JIRA]
internal class DetailsViewModel(
private val store: Store,
private val walletsRepository: WalletsRepository,
@@ -91,10 +89,6 @@ internal class DetailsViewModel(
SettingsItem.AppSettings(::navigateToAppSettings)
.let(::add)
- // removed chat in task [REDACTED_TASK_KEY]
- // SettingsItem.Chat(::navigateToChat)
- // .let(::add)
-
SettingsItem.SendFeedback(::sendFeedback)
.let(::add)
@@ -128,30 +122,43 @@ 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))
+ val userWallet = userWalletsListManager.selectedUserWalletSync
+ ?: error("Selected wallet must be not null")
+
+ store.dispatchNavigationAction { push(AppRoute.ReferralProgram(userWallet.walletId)) }
}
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() {
+ val userWalletId = userWalletsListManager.selectedUserWalletSync?.walletId
+ ?: error("UserWalletId must be not null")
Analytics.send(Settings.ButtonCardSettings())
- store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.CardSettings))
+ store.dispatchNavigationAction { push(AppRoute.CardSettings(userWalletId)) }
}
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..041a91ee08 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
@@ -1,56 +1,34 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
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.features.details.redux.DetailsState
+import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.flow.MutableStateFlow
-import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
-internal class ResetCardFragment : ComposeFragment(), StoreSubscriber {
+internal class ResetCardFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel: ResetCardViewModel by viewModels()
- private var screenState = MutableStateFlow(ResetCardScreenState.InitialState)
-
@Composable
override fun ScreenContent(modifier: Modifier) {
- val state = screenState.collectAsStateWithLifecycle().value
+ val state by viewModel.screenState.collectAsStateWithLifecycle()
ResetCardScreen(
state = state,
- onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
+ onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
modifier = modifier,
)
}
-
- override fun onStart() {
- super.onStart()
- store.subscribe(this) { state ->
- state.skipRepeats { oldState, newState ->
- oldState.detailsState == newState.detailsState
- }.select { it.detailsState }
- }
- }
-
- override fun onStop() {
- super.onStop()
- store.unsubscribe(this)
- }
-
- override fun newState(state: DetailsState) {
- if (activity == null || view == null) return
- screenState.value = viewModel.updateState(state.cardSettingsState)
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt
index 85bec44728..1b52a50fd1 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreen.kt
@@ -20,24 +20,19 @@ import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
-import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.ResetCardScreenContent.Dialog as ResetCardDialog
+import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog as ResetCardDialog
@Composable
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
modifier = modifier,
content = {
- when (state) {
- is ResetCardScreenState.ResetCardScreenContent -> ResetCardView(state = state)
- ResetCardScreenState.InitialState -> {
- // do nothing for now, just white screen
- }
- }
+ ResetCardView(state = state)
},
onBackClick = onBackClick,
)
- when (val dialog = (state as? ResetCardScreenState.ResetCardScreenContent)?.dialog) {
+ when (val dialog = state.dialog) {
is ResetCardDialog.StartReset,
is ResetCardDialog.ContinueReset,
is ResetCardDialog.InterruptedReset,
@@ -48,7 +43,7 @@ internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Uni
}
@Composable
-private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) {
+private fun ResetCardView(state: ResetCardScreenState) {
val scrollState = rememberScrollState()
Column(
@@ -110,7 +105,7 @@ private fun Description(text: TextReference) {
}
@Composable
-private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) {
+private fun Conditions(state: ResetCardScreenState) {
state.warningsToShow.forEach {
when (it) {
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
@@ -120,7 +115,6 @@ private fun Conditions(state: ResetCardScreenState.ResetCardScreenContent) {
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
}
-
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
@@ -192,7 +186,7 @@ private fun ResetButton(enabled: Boolean, onResetButtonClick: () -> Unit) {
}
@Composable
-private fun CommonResetDialog(dialog: ResetCardScreenState.ResetCardScreenContent.Dialog) {
+private fun CommonResetDialog(dialog: ResetCardScreenState.Dialog) {
BasicDialog(
title = stringResource(dialog.titleResId),
message = stringResource(dialog.messageResId),
@@ -231,10 +225,13 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
.background(TangemTheme.colors.background.secondary),
) {
ResetCardScreen(
- state = ResetCardScreenState.ResetCardScreenContent(
- accepted = true,
+ state = ResetCardScreenState(
+ resetButtonEnabled = true,
+ showResetPasswordButton = false,
warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS),
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
+ acceptCondition1Checked = false,
+ acceptCondition2Checked = false,
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},
onResetButtonClick = {},
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt
index 0bcff503bc..27dacf387f 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardScreenState.kt
@@ -4,63 +4,57 @@ import androidx.annotation.StringRes
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
-internal sealed class ResetCardScreenState {
+internal data class ResetCardScreenState(
+ val resetButtonEnabled: Boolean,
+ val descriptionText: TextReference,
+ val warningsToShow: List,
+ val showResetPasswordButton: Boolean,
+ val acceptCondition1Checked: Boolean,
+ val acceptCondition2Checked: Boolean,
+ val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
+ val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
+ val onResetButtonClick: () -> Unit,
+ val dialog: Dialog?,
+) {
- object InitialState : ResetCardScreenState()
+ sealed class Dialog(
+ @StringRes val titleResId: Int,
+ @StringRes val messageResId: Int,
+ ) {
- data class ResetCardScreenContent(
- val accepted: Boolean = false,
- val descriptionText: TextReference,
- val warningsToShow: List,
- val acceptCondition1Checked: Boolean = false,
- val acceptCondition2Checked: Boolean = false,
- val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
- val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
- val onResetButtonClick: () -> Unit,
- val dialog: Dialog? = null,
- ) : ResetCardScreenState() {
- val resetButtonEnabled: Boolean
- get() = accepted
+ abstract val onConfirmClick: () -> Unit
+ abstract val onDismiss: () -> Unit
- sealed class Dialog(
- @StringRes val titleResId: Int,
- @StringRes val messageResId: Int,
+ data class StartReset(
+ override val onConfirmClick: () -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : Dialog(
+ titleResId = R.string.common_attention,
+ messageResId = R.string.card_settings_action_sheet_title,
+ )
+
+ data class ContinueReset(
+ override val onConfirmClick: () -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : Dialog(
+ titleResId = R.string.card_settings_continue_reset_alert_title,
+ messageResId = R.string.card_settings_continue_reset_alert_message,
+ )
+
+ data class InterruptedReset(
+ override val onConfirmClick: () -> Unit,
+ override val onDismiss: () -> Unit,
+ ) : Dialog(
+ titleResId = R.string.card_settings_interrupted_reset_alert_title,
+ messageResId = R.string.card_settings_interrupted_reset_alert_message,
+ )
+
+ data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog(
+ titleResId = R.string.card_settings_completed_reset_alert_title,
+ messageResId = R.string.card_settings_completed_reset_alert_message,
) {
- abstract val onConfirmClick: () -> Unit
- abstract val onDismiss: () -> Unit
-
- data class StartReset(
- override val onConfirmClick: () -> Unit,
- override val onDismiss: () -> Unit,
- ) : Dialog(
- titleResId = R.string.common_attention,
- messageResId = R.string.card_settings_action_sheet_title,
- )
-
- data class ContinueReset(
- override val onConfirmClick: () -> Unit,
- override val onDismiss: () -> Unit,
- ) : Dialog(
- titleResId = R.string.card_settings_continue_reset_alert_title,
- messageResId = R.string.card_settings_continue_reset_alert_message,
- )
-
- data class InterruptedReset(
- override val onConfirmClick: () -> Unit,
- override val onDismiss: () -> Unit,
- ) : Dialog(
- titleResId = R.string.card_settings_interrupted_reset_alert_title,
- messageResId = R.string.card_settings_interrupted_reset_alert_message,
- )
-
- data class CompletedReset(override val onConfirmClick: () -> Unit) : Dialog(
- titleResId = R.string.card_settings_completed_reset_alert_title,
- messageResId = R.string.card_settings_completed_reset_alert_message,
- ) {
-
- override val onDismiss: () -> Unit = {}
- }
+ override val onDismiss: () -> Unit = {}
}
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt
index 07f7857126..fefbdf122e 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt
@@ -1,104 +1,170 @@
package com.tangem.tap.features.details.ui.resetcard
+import android.os.Bundle
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import arrow.core.getOrElse
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.bundle.unbundle
+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.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.domain.card.ResetCardUseCase
+import com.tangem.domain.card.ResetCardUserCodeParams
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.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
+import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.tap.common.analytics.events.Settings
+import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.onUserWalletSelected
-import com.tangem.tap.features.details.redux.CardSettingsState
-import com.tangem.tap.features.details.redux.DetailsAction.ResetToFactory
-import com.tangem.tap.features.details.ui.cardsettings.TextReference
-import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles
-import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
+import com.tangem.tap.features.details.redux.ResetCardDialog
+import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
+import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription
import com.tangem.tap.store
import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import timber.log.Timber
import javax.inject.Inject
-import com.tangem.tap.features.details.redux.CardSettingsState.Dialog as CardSettingsDialog
@Suppress("LongParameterList")
@HiltViewModel
internal class ResetCardViewModel @Inject constructor(
- private val resetCardFeatureToggles: ResetCardFeatureToggles,
+ getUserWalletUseCase: GetUserWalletUseCase,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val resetCardUseCase: ResetCardUseCase,
private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase,
private val deleteWalletUseCase: DeleteWalletUseCase,
private val userWalletsListManager: UserWalletsListManager,
private val analyticsEventHandler: AnalyticsEventHandler,
+ private val cardSettingsInteractor: CardSettingsInteractor,
+ savedStateHandle: SavedStateHandle,
) : ViewModel() {
- private val firstCardScanResponse = store.state.detailsState.cardSettingsState?.scanResponse
- ?: error("ScanResponse can't be null")
+ // region Card-set specific data. All cards from single set have the same userWalletId and cardTypesResolver
+ private val currentUserWalletId = savedStateHandle.get(AppRoute.ResetToFactory.USER_WALLET_ID)
+ ?.unbundle(UserWalletId.serializer())
+ ?: error("UserWalletId must be provided for ResetCardViewModel")
- private val currentUserWalletId = createUserWalletId(firstCardScanResponse)
+ // Use only for card-specific data
+ private val userWallet = getUserWalletUseCase(userWalletId = currentUserWalletId)
+ .getOrElse { error("Failed to get user wallet: $it") }
+
+ private val currentCardTypesResolver = userWallet.cardTypesResolver
+ private val currentUserCodeParams = ResetCardUserCodeParams(
+ isAccessCodeSet = userWallet.scanResponse.card.isAccessCodeSet,
+ isPasscodeSet = userWallet.scanResponse.card.isPasscodeSet,
+ )
+ // endregion
+
+ // region Data of card that was scanned on CardSettings
+ private val primaryCardId: String = savedStateHandle.get(AppRoute.ResetToFactory.CARD_ID)
+ ?: error("CardId must be provided for ResetCardViewModel")
+
+ private val isActiveBackupPrimaryCard =
+ savedStateHandle.get(AppRoute.ResetToFactory.IS_ACTIVE_BACKUP_STATUS)
+ ?: error("IsActiveBackupCard must be provided for ResetCardViewModel")
+
+ private val primaryBackupCardsCount = savedStateHandle.get(AppRoute.ResetToFactory.BACKUP_CARDS_COUNT)
+ ?: error("CardCount must be provided for ResetCardViewModel")
+ // endregion
// TODO: move logic to separate domain entity
private var resetBackupCardCount = 0
- fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent {
- val descriptionText = state?.cardInfo
- ?.toResetCardDescriptionText()
- ?: TextReference.Str(value = "")
+ val screenState: MutableStateFlow = MutableStateFlow(
+ value = getInitialState(),
+ )
+ private fun getInitialState(): ResetCardScreenState {
+ val shouldShowResetPasswordButton = shouldShowResetPasswordButton()
val warningsToShow = buildList {
add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS)
- if (state?.isShowPasswordResetRadioButton == true) {
+ if (shouldShowResetPasswordButton) {
add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE)
}
}
- return ResetCardScreenState.ResetCardScreenContent(
- accepted = state?.resetButtonEnabled ?: false,
- descriptionText = descriptionText,
+ return ResetCardScreenState(
+ resetButtonEnabled = false,
+ descriptionText = getResetToFactoryDescription(
+ isActiveBackupStatus = isActiveBackupPrimaryCard,
+ typesResolver = currentCardTypesResolver,
+ ),
warningsToShow = warningsToShow,
- acceptCondition1Checked = state?.condition1Checked ?: false,
- acceptCondition2Checked = state?.condition2Checked ?: false,
- onAcceptCondition1ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition1(it)) },
- onAcceptCondition2ToggleClick = { store.dispatch(ResetToFactory.AcceptCondition2(it)) },
- onResetButtonClick = { showDialog(CardSettingsDialog.StartResetDialog) },
- dialog = state?.dialog?.let(::createDialog),
+ showResetPasswordButton = shouldShowResetPasswordButton,
+ acceptCondition1Checked = false,
+ acceptCondition2Checked = false,
+ onAcceptCondition1ToggleClick = ::toggleFirstCondition,
+ onAcceptCondition2ToggleClick = ::toggleSecondCondition,
+ onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) },
+ dialog = null,
)
}
- private fun createDialog(dialog: CardSettingsDialog): ResetCardScreenState.ResetCardScreenContent.Dialog {
+ private fun shouldShowResetPasswordButton(): Boolean {
+ val isTangemWallet = currentCardTypesResolver.isTangemWallet() || currentCardTypesResolver.isWallet2()
+
+ return isTangemWallet && isActiveBackupPrimaryCard
+ }
+
+ private fun toggleFirstCondition(isAccepted: Boolean) {
+ screenState.update { prevState ->
+ val resetButtonEnabled = if (prevState.showResetPasswordButton) {
+ isAccepted && prevState.acceptCondition2Checked
+ } else {
+ isAccepted
+ }
+
+ prevState.copy(
+ acceptCondition1Checked = isAccepted,
+ resetButtonEnabled = resetButtonEnabled,
+ )
+ }
+ }
+
+ private fun toggleSecondCondition(isAccepted: Boolean) {
+ screenState.update { prevState ->
+ val resetButtonEnabled = prevState.acceptCondition1Checked && isAccepted
+
+ prevState.copy(
+ acceptCondition2Checked = isAccepted,
+ resetButtonEnabled = resetButtonEnabled,
+ )
+ }
+ }
+
+ private fun createDialog(dialog: ResetCardDialog): ResetCardScreenState.Dialog {
return when (dialog) {
- CardSettingsDialog.StartResetDialog -> {
- ResetCardScreenState.ResetCardScreenContent.Dialog.StartReset(
+ ResetCardDialog.StartResetDialog -> {
+ ResetCardScreenState.Dialog.StartReset(
onConfirmClick = ::onStartResetClick,
onDismiss = ::dismissDialog,
)
}
- CardSettingsDialog.ContinueResetDialog -> {
- ResetCardScreenState.ResetCardScreenContent.Dialog.ContinueReset(
+ ResetCardDialog.ContinueResetDialog -> {
+ ResetCardScreenState.Dialog.ContinueReset(
onConfirmClick = ::onContinueResetClick,
onDismiss = ::onContinueResetDialogDismiss,
)
}
- CardSettingsDialog.InterruptedResetDialog -> {
- ResetCardScreenState.ResetCardScreenContent.Dialog.InterruptedReset(
+ ResetCardDialog.InterruptedResetDialog -> {
+ ResetCardScreenState.Dialog.InterruptedReset(
onConfirmClick = ::onContinueResetClick,
onDismiss = ::onInterruptedResetDialogDismiss,
)
}
- CardSettingsDialog.CompletedResetDialog -> {
- ResetCardScreenState.ResetCardScreenContent.Dialog.CompletedReset(
+ ResetCardDialog.CompletedResetDialog -> {
+ ResetCardScreenState.Dialog.CompletedReset(
onConfirmClick = ::dismissAndFinishFullReset,
)
}
@@ -108,21 +174,23 @@ internal class ResetCardViewModel @Inject constructor(
private fun onStartResetClick() {
dismissDialog()
- if (resetCardFeatureToggles.isFullResetEnabled) {
- makeFullReset()
- } else {
- store.dispatch(ResetToFactory.Proceed)
- }
+ makeFullReset()
}
private fun makeFullReset() {
viewModelScope.launch {
- resetCardUseCase(card = firstCardScanResponse.card).onRight {
- deleteSavedAccessCodesUseCase(firstCardScanResponse.card.cardId)
- deleteWalletUseCase(currentUserWalletId)
+ resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight {
+ deleteSavedAccessCodesUseCase(cardId = primaryCardId)
+ val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse {
+ Timber.e("Unable to delete user wallet: $it")
+ return@launch
+ }
+
+ if (hasUserWallets) {
+ val newSelectedWallet = getSelectedWalletSyncUseCase().getOrElse {
+ error("Failed to get selected wallet: $it")
+ }
- val newSelectedWallet = getSelectedWalletSyncUseCase().getOrNull()
- if (newSelectedWallet != null) {
store.onUserWalletSelected(newSelectedWallet)
}
@@ -139,24 +207,26 @@ internal class ResetCardViewModel @Inject constructor(
viewModelScope.launch {
resetCardUseCase(
cardNumber = resetBackupCardCount + 1,
- card = firstCardScanResponse.card,
+ params = currentUserCodeParams,
userWalletId = currentUserWalletId,
)
- .onRight {
- resetBackupCardCount++
+ .onRight { isResetCompleted ->
+ if (isResetCompleted) {
+ resetBackupCardCount++
+ }
delay(DELAY_SDK_DIALOG_CLOSE)
checkRemainingBackupCards()
}
- .onLeft { showDialog(CardSettingsDialog.InterruptedResetDialog) }
+ .onLeft { showDialog(ResetCardDialog.InterruptedResetDialog) }
}
}
private fun onContinueResetDialogDismiss() {
dismissDialog()
- showDialog(CardSettingsDialog.InterruptedResetDialog)
+ showDialog(ResetCardDialog.InterruptedResetDialog)
}
private fun onInterruptedResetDialogDismiss() {
@@ -166,15 +236,15 @@ internal class ResetCardViewModel @Inject constructor(
}
private fun checkRemainingBackupCards() {
- val backupCardsCount = firstCardScanResponse.getBackupCardsCount()
+ val backupCardsCount = getBackupCardsCount()
when {
- backupCardsCount > resetBackupCardCount -> showDialog(CardSettingsDialog.ContinueResetDialog)
+ backupCardsCount > resetBackupCardCount -> showDialog(ResetCardDialog.ContinueResetDialog)
backupCardsCount == resetBackupCardCount -> {
analyticsEventHandler.send(
event = Settings.CardSettings.FactoryResetFinished(cardsCount = resetBackupCardCount + 1),
)
- showDialog(CardSettingsDialog.CompletedResetDialog)
+ showDialog(ResetCardDialog.CompletedResetDialog)
}
else -> finishFullReset()
}
@@ -187,42 +257,33 @@ internal class ResetCardViewModel @Inject constructor(
}
private fun finishFullReset() {
+ cardSettingsInteractor.clear()
+
val newSelectedWallet = userWalletsListManager.selectedUserWalletSync
if (newSelectedWallet != null) {
- store.dispatch(NavigationAction.PopBackTo(AppScreen.Wallet))
+ store.dispatchNavigationAction { popTo() }
} 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 { replaceAll(AppRoute.Home) }
}
}
}
- private fun showDialog(dialog: CardSettingsDialog) {
- store.dispatch(ResetToFactory.ShowDialog(dialog))
+ private fun showDialog(dialog: ResetCardDialog) {
+ screenState.update { it.copy(dialog = createDialog(dialog)) }
}
private fun dismissDialog() {
- store.dispatch(ResetToFactory.DismissDialog)
+ screenState.update { it.copy(dialog = null) }
}
- private fun ScanResponse.getBackupCardsCount(): Int {
- if (!cardTypesResolver.isMultiwalletAllowed()) return 0
+ private fun getBackupCardsCount(): Int {
+ if (!currentCardTypesResolver.isMultiwalletAllowed()) return 0
- return when (val status = card.backupStatus) {
- is CardDTO.BackupStatus.Active -> status.cardCount
- is CardDTO.BackupStatus.CardLinked,
- is CardDTO.BackupStatus.NoBackup,
- null,
- -> 0
- }
- }
-
- private fun createUserWalletId(scanResponse: ScanResponse): UserWalletId {
- return UserWalletIdBuilder.scanResponse(scanResponse).build()
- ?: error("UserWalletId can't be null")
+ return primaryBackupCardsCount
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt
deleted file mode 100644
index 2de1e70012..0000000000
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/di/ResetCardModule.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.tangem.tap.features.details.ui.resetcard.di
-
-import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
-import com.tangem.tap.features.details.ui.resetcard.featuretoggles.DefaultResetCardFeatureToggles
-import com.tangem.tap.features.details.ui.resetcard.featuretoggles.ResetCardFeatureToggles
-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 ResetCardModule {
-
- @Provides
- @Singleton
- fun provideResetCardFeatureToggles(featureTogglesManager: FeatureTogglesManager): ResetCardFeatureToggles {
- return DefaultResetCardFeatureToggles(featureTogglesManager)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt
deleted file mode 100644
index a5b76a410d..0000000000
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/DefaultResetCardFeatureToggles.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.tangem.tap.features.details.ui.resetcard.featuretoggles
-
-import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
-
-internal class DefaultResetCardFeatureToggles(
- private val featureTogglesManager: FeatureTogglesManager,
-) : ResetCardFeatureToggles {
-
- override val isFullResetEnabled: Boolean
- get() = featureTogglesManager.isFeatureEnabled(name = "FULL_RESET_ENABLED")
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt
deleted file mode 100644
index d290d4d1d9..0000000000
--- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/featuretoggles/ResetCardFeatureToggles.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package com.tangem.tap.features.details.ui.resetcard.featuretoggles
-
-interface ResetCardFeatureToggles {
-
- val isFullResetEnabled: Boolean
-}
\ No newline at end of file
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..1c5fe7f90b 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
@@ -1,54 +1,34 @@
package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
-import com.tangem.core.navigation.NavigationAction
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
-import com.tangem.tap.features.details.redux.DetailsState
+import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
-import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
-internal class SecurityModeFragment : ComposeFragment(), StoreSubscriber {
+internal class SecurityModeFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
- private val viewModel = SecurityModeViewModel(store)
-
- private var screenState: MutableState =
- mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.manageSecurityState))
+ private val viewModel: SecurityModeViewModel by viewModels()
@Composable
override fun ScreenContent(modifier: Modifier) {
+ val state by viewModel.screenState.collectAsStateWithLifecycle()
+
SecurityModeScreen(
modifier = modifier,
- state = screenState.value,
- onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
+ state = state,
+ onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
)
}
-
- override fun onStart() {
- super.onStart()
- store.subscribe(this) { state ->
- state.skipRepeats { oldState, newState ->
- oldState.detailsState == newState.detailsState
- }.select { it.detailsState }
- }
- }
-
- override fun onStop() {
- super.onStop()
- store.unsubscribe(this)
- }
-
- override fun newState(state: DetailsState) {
- if (activity == null || view == null) return
- screenState.value = viewModel.updateState(state.cardSettingsState?.manageSecurityState)
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt
index 7aa863a7df..5f0d54717e 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt
@@ -1,29 +1,109 @@
package com.tangem.tap.features.details.ui.securitymode
-import com.tangem.tap.common.redux.AppState
-import com.tangem.tap.features.details.redux.DetailsAction
-import com.tangem.tap.features.details.redux.ManageSecurityState
+import android.os.Bundle
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import arrow.core.getOrElse
+import com.tangem.common.CompletionResult
+import com.tangem.common.core.TangemSdkError
+import com.tangem.common.routing.AppRoute
+import com.tangem.common.routing.AppRouter
+import com.tangem.common.routing.bundle.unbundle
+import com.tangem.core.analytics.Analytics
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.wallets.models.UserWallet
+import com.tangem.domain.wallets.models.UserWalletId
+import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
+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.domain.sdk.TangemSdkManager
import com.tangem.tap.features.details.redux.SecurityOption
-import org.rekotlin.Store
+import com.tangem.tap.features.details.ui.common.utils.getAllowedSecurityOptions
+import com.tangem.tap.features.details.ui.common.utils.getCurrentSecurityOption
+import com.tangem.tap.store
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import javax.inject.Inject
-internal class SecurityModeViewModel(val store: Store) {
+@HiltViewModel
+internal class SecurityModeViewModel @Inject constructor(
+ private val getUserWalletUseCase: GetUserWalletUseCase,
+ private val tangemSdkManager: TangemSdkManager,
+ savedStateHandle: SavedStateHandle,
+) : ViewModel() {
+
+ private val userWalletId = savedStateHandle.get(AppRoute.DetailsSecurity.USER_WALLET_ID_KEY)
+ ?.unbundle(UserWalletId.serializer())
+ ?: error("UserWalletId is required for SecurityModeViewModel")
+
+ val screenState = MutableStateFlow(
+ value = getInitialState(),
+ )
+
+ private fun getInitialState(): SecurityModeScreenState {
+ val userWallet = getUserWallet()
+ val scanResponse = userWallet.scanResponse
+ val card = scanResponse.card
+ val cardTypesResolver = scanResponse.cardTypesResolver
+
+ val currentSecurityOption = getCurrentSecurityOption(card)
+ val allowedSecurityOptions = getAllowedSecurityOptions(card, cardTypesResolver, currentSecurityOption)
- fun updateState(state: ManageSecurityState?): SecurityModeScreenState {
- if (state == null) {
- return SecurityModeScreenState(
- availableOptions = emptyList(),
- selectedSecurityMode = SecurityOption.LongTap,
- isSaveChangesEnabled = false,
- onNewModeSelected = {},
- onSaveChangesClicked = {},
- )
- }
return SecurityModeScreenState(
- availableOptions = state.allowedOptions.toList(),
- selectedSecurityMode = state.selectedOption,
- isSaveChangesEnabled = state.selectedOption != state.currentOption,
- onNewModeSelected = { store.dispatch(DetailsAction.ManageSecurity.SelectOption(it)) },
- onSaveChangesClicked = { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) },
+ availableOptions = allowedSecurityOptions.toList(),
+ selectedSecurityMode = currentSecurityOption,
+ isSaveChangesEnabled = false,
+ onNewModeSelected = ::selectOption,
+ onSaveChangesClicked = ::saveChanges,
)
}
+
+ private fun selectOption(securityOption: SecurityOption) {
+ screenState.update { state ->
+ state.copy(
+ selectedSecurityMode = securityOption,
+ isSaveChangesEnabled = securityOption != getCurrentSecurityOption(getUserWallet().scanResponse.card),
+ )
+ }
+ }
+
+ private fun saveChanges() {
+ val userWallet = getUserWallet()
+ val cardId = userWallet.cardId
+ val selectedOption = screenState.value.selectedSecurityMode
+
+ viewModelScope.launch {
+ val result = when (selectedOption) {
+ SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)
+ SecurityOption.PassCode -> tangemSdkManager.setPasscode(cardId)
+ SecurityOption.AccessCode -> tangemSdkManager.setAccessCode(cardId)
+ }
+
+ val paramValue = AnalyticsParam.SecurityMode.from(selectedOption)
+ when (result) {
+ is CompletionResult.Success -> {
+ Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue))
+
+ store.dispatchNavigationAction(AppRouter::pop)
+ }
+ is CompletionResult.Failure -> {
+ val error = result.error
+ if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
+ Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
+ }
+ }
+ else -> Unit
+ }
+ }
+ }
+
+ private fun getUserWallet(): UserWallet {
+ return getUserWalletUseCase(userWalletId).getOrElse {
+ error("Unable to get user wallet $userWalletId: $it")
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/utils/Mappers.kt b/app/src/main/java/com/tangem/tap/features/details/ui/utils/Mappers.kt
deleted file mode 100644
index 459e5ba49a..0000000000
--- a/app/src/main/java/com/tangem/tap/features/details/ui/utils/Mappers.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.tangem.tap.features.details.ui.utils
-
-import com.tangem.tap.features.details.redux.CardInfo
-import com.tangem.tap.features.details.ui.cardsettings.TextReference
-import com.tangem.wallet.R
-
-internal fun CardInfo.toResetCardDescriptionText(): TextReference {
- return if (!this.hasBackup || this.isTwin) {
- TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
- } else {
- TextReference.Res(R.string.reset_card_with_backup_to_factory_message)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt
index 19dd0771bc..d720c10ec7 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt
@@ -6,11 +6,12 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
+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.tap.common.analytics.events.WalletConnect
+import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
@@ -41,7 +42,7 @@ internal class WalletConnectFragment : 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/Disclaimer.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt
index f92bc4be2f..9cda07b754 100644
--- a/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt
+++ b/app/src/main/java/com/tangem/tap/features/disclaimer/Disclaimer.kt
@@ -6,14 +6,13 @@ import android.net.Uri
[REDACTED_AUTHOR]
*/
interface Disclaimer {
- fun type(): DisclaimerType
fun getUri(): Uri
suspend fun accept()
suspend fun isAccepted(): Boolean
}
abstract class BaseDisclaimer(
- protected val dataProvider: DisclaimerDataProvider,
+ private val dataProvider: DisclaimerDataProvider,
) : Disclaimer {
val baseUrl = "https://tangem.com"
@@ -26,46 +25,11 @@ abstract class BaseDisclaimer(
}
class DummyDisclaimer : Disclaimer {
- override fun type(): DisclaimerType = DisclaimerType.Tangem
override fun getUri(): Uri = Uri.parse("https://tangem.com/tangem_tos.html")
override suspend fun accept() {}
override suspend fun isAccepted(): Boolean = false
}
class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
- override fun type(): DisclaimerType = DisclaimerType.Tangem
override fun getUri(): Uri = Uri.parse("$baseUrl/tangem_tos.html")
-}
-
-class Start2CoinDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
- override fun type(): DisclaimerType = DisclaimerType.Start2Coin
- override fun getUri(): Uri = Uri.parse("$baseUrl/" + filename(dataProvider.getLanguage(), getRegion()))
-
- @Suppress("ComplexMethod")
- private fun filename(languageCode: String, regionCode: String?): String {
- return when {
- languageCode == "fr" && regionCode == "ch" -> "start2coin-fr-ch-tangem.html"
- languageCode == "de" && regionCode == "ch" -> "start2coin-de-ch-tangem.html"
- languageCode == "en" && regionCode == "ch" -> "start2coin-en-ch-tangem.html"
- languageCode == "it" && regionCode == "ch" -> "start2coin-it-ch-tangem.html"
- languageCode == "fr" && regionCode == "fr" -> "start2coin-fr-fr-tangem.html"
- languageCode == "de" && regionCode == "at" -> "start2coin-de-at-tangem.html"
- regionCode == "fr" -> "start2coin-fr-fr-tangem.html"
- regionCode == "ch" -> "start2coin-en-ch-tangem.html"
- regionCode == "at" -> "start2coin-de-at-tangem.html"
- else -> "start2coin-fr-fr-tangem.html"
- }
- }
-
- private fun getRegion(): String? {
- val cardId = dataProvider.getCardId()
- if (cardId.isEmpty()) return null
-
- return when (cardId[1]) {
- '0' -> "fr"
- '1' -> "ch"
- '2' -> "at"
- else -> null
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt
index 796452dcab..90f73cb4ad 100644
--- a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt
+++ b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt
@@ -1,58 +1,28 @@
package com.tangem.tap.features.disclaimer
-import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import java.util.Locale
-/**
-[REDACTED_AUTHOR]
- */
-enum class DisclaimerType {
- Tangem,
- Start2Coin,
- ;
-
- companion object {
- fun get(cardDTO: CardDTO): DisclaimerType {
- return when {
- cardDTO.isStart2Coin -> Start2Coin
- else -> Tangem
- }
- }
- }
+fun CardDTO.createDisclaimer(): Disclaimer {
+ val dataProvider = provideDisclaimerDataProvider(cardId)
+ return TangemDisclaimer(dataProvider)
}
-fun DisclaimerType.createDisclaimer(cardDTO: CardDTO): Disclaimer {
- val dataProvider = provideDisclaimerDataProvider(cardDTO.cardId, this)
- return when (this) {
- DisclaimerType.Tangem -> TangemDisclaimer(dataProvider)
- DisclaimerType.Start2Coin -> Start2CoinDisclaimer(dataProvider)
- }
-}
-
-fun CardDTO.createDisclaimer(): Disclaimer = DisclaimerType.get(this).createDisclaimer(this)
-
-private fun provideDisclaimerDataProvider(cardId: String, disclaimerType: DisclaimerType): DisclaimerDataProvider {
+private fun provideDisclaimerDataProvider(cardId: String): DisclaimerDataProvider {
val cardRepository = store.inject(DaggerGraphState::cardRepository)
return object : DisclaimerDataProvider {
override fun getLanguage(): String = Locale.getDefault().language
override fun getCardId(): String = cardId
override suspend fun accept() {
- when (disclaimerType) {
- DisclaimerType.Tangem -> cardRepository.acceptTangemTOS()
- DisclaimerType.Start2Coin -> cardRepository.acceptStart2CoinTOS(cardId)
- }
+ cardRepository.acceptTangemTOS()
}
override suspend fun isAccepted(): Boolean {
- return when (disclaimerType) {
- DisclaimerType.Tangem -> cardRepository.isTangemTOSAccepted()
- DisclaimerType.Start2Coin -> cardRepository.isStart2CoinTOSAccepted(cardId)
- }
+ return cardRepository.isTangemTOSAccepted()
}
}
}
\ No newline at end of file
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 42f21e542d..38c890dc29 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
@@ -6,11 +6,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
@@ -126,7 +125,7 @@ private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
scope.launch { store.onUserWalletSelected(userWallet = userWallet) }
}
.doOnResult {
- navigateTo(AppScreen.Wallet)
+ navigateTo(AppRoute.Wallet)
}
}
@@ -150,8 +149,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..404a469233 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
@@ -32,42 +32,43 @@ import timber.log.Timber
*/
object OnboardingHelper {
suspend fun isOnboardingCase(response: ScanResponse): Boolean {
- val onboardingManager = store.state.globalState.onboardingState.onboardingManager
+ val onboardingManager =
+ store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response)
val cardId = response.card.cardId
return when {
response.cardTypesResolver.isTangemTwins() -> {
if (!response.twinsIsTwinned()) {
true
} else {
- onboardingManager?.isActivationInProgress(cardId) ?: false
+ onboardingManager.isActivationInProgress(cardId) ?: false
}
}
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
- val activationInProgress = onboardingManager?.isActivationInProgress(cardId)
+ val activationInProgress = onboardingManager.isActivationInProgress(cardId)
val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup &&
!DemoHelper.isDemoCard(response)
- emptyWallets || activationInProgress == true || isNoBackup
+ emptyWallets || activationInProgress || isNoBackup
}
- response.card.wallets.isNotEmpty() -> onboardingManager?.isActivationInProgress(cardId) ?: false
+ response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId) ?: false
else -> true
}
}
- 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 +112,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/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt
index 7ac3806126..757ec91454 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt
@@ -11,12 +11,14 @@ import coil.load
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.ShareElement
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.addBackPressHandler
+import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
@@ -134,7 +136,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment() {
}
private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) {
- if (state.isBuyAllowed) {
+ if (availableForBuy(state.scanResponse, state.walletBalance)) {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.icon = null
btnMainAction.setOnClickListener {
@@ -217,6 +219,11 @@ class OnboardingNoteFragment : BaseOnboardingFragment() {
}
}
+ private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean {
+ scanResponse ?: return false
+ return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency)
+ }
+
override fun handleOnBackPressed() {
store.dispatch(OnboardingNoteAction.OnBackPressed)
}
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/note/redux/OnboardingNoteReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt
index 5552fc8b50..14f7989391 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt
@@ -13,7 +13,7 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingNoteSt
when (action) {
is GlobalAction.Onboarding.Start -> {
- state = OnboardingNoteState()
+ state = OnboardingNoteState(scanResponse = action.scanResponse)
}
is OnboardingNoteAction.SetArtworkUrl -> {
state = state.copy(cardArtworkUrl = action.artworkUrl)
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt
index ae7a2db594..3d659e39da 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt
@@ -1,11 +1,10 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
-import com.tangem.tap.store
import org.rekotlin.StateType
-import kotlin.properties.ReadOnlyProperty
/**
[REDACTED_AUTHOR]
@@ -20,14 +19,11 @@ data class OnboardingNoteState(
val currentStep: OnboardingNoteStep = OnboardingNoteStep.None,
val steps: List = OnboardingNoteStep.values().toList(),
val showConfetti: Boolean = false,
+ val scanResponse: ScanResponse? = null,
) : StateType {
val progress: Int
get() = steps.indexOf(currentStep)
-
- val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ ->
- store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency)
- }
}
enum class OnboardingNoteStep {
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..19d77c509e 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,12 +40,13 @@ 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
}
-private val twinsWalletMiddleware: Middleware = { dispatch, state ->
+private val twinsWalletMiddleware: Middleware = { dispatch, _ ->
{ next ->
{ action ->
handle(action, dispatch)
@@ -64,15 +66,15 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
fun getScanResponse(): ScanResponse {
return when (twinCardsState.mode) {
- CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse
- CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse
+ is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse
+ is CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse
} ?: throw NullPointerException("ScanResponse can't be NULL")
}
fun updateScanResponse(response: ScanResponse) {
when (twinCardsState.mode) {
- CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response
- CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response))
+ is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response
+ is CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response))
}
}
@@ -103,6 +105,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
mainScope.launch {
if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch
+ if (twinCardsState.mode is CreateTwinWalletMode.RecreateWallet) {
+ store.dispatch(GlobalAction.SaveScanResponse(twinCardsState.mode.scanResponse))
+ }
+
val scanResponse = getScanResponse()
onboardingManager?.apply {
if (!isActivationStarted(scanResponse.card.cardId)) {
@@ -111,7 +117,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
when (twinCardsState.mode) {
- CreateTwinWalletMode.CreateWallet -> {
+ is CreateTwinWalletMode.CreateWallet -> {
mainScope.launch {
val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase)
.invokeSync()
@@ -130,7 +136,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatch(dispatchAction)
}
}
- CreateTwinWalletMode.RecreateWallet -> {
+ is CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
}
}
@@ -224,10 +230,10 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
when (twinCardsState.mode) {
- CreateTwinWalletMode.CreateWallet -> {
+ is CreateTwinWalletMode.CreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet))
}
- CreateTwinWalletMode.RecreateWallet -> {
+ is CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
}
}
@@ -317,18 +323,18 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
TwinCardsAction.Done -> {
val scanResponse = getScanResponse()
when (twinCardsState.mode) {
- CreateTwinWalletMode.CreateWallet -> {
+ is CreateTwinWalletMode.CreateWallet -> {
store.dispatchOnMain(GlobalAction.Onboarding.Stop)
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
}
- CreateTwinWalletMode.RecreateWallet -> {
+ is CreateTwinWalletMode.RecreateWallet -> {
scope.launch {
val walletsRepository = store.inject(DaggerGraphState::walletsRepository)
if (walletsRepository.shouldSaveUserWalletsSync()) {
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse)
} else {
- store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home))
+ store.dispatchNavigationAction { popTo() }
}
}
}
@@ -352,7 +358,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 +368,18 @@ 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 })
+ val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { 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..83e641bc8a 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,14 +1,12 @@
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
-import com.tangem.tap.store
import org.rekotlin.StateType
-import kotlin.properties.ReadOnlyProperty
/**
[REDACTED_AUTHOR]
@@ -29,11 +27,12 @@ data class TwinCardsState(
val balanceNonCriticalError: TapError? = null,
val balanceCriticalError: TapError? = null,
val showConfetti: Boolean = false,
+ val welcomeOnlyScanResponse: ScanResponse? = null,
) : StateType {
val steps: List
get() = when (mode) {
- CreateTwinWalletMode.CreateWallet -> listOf(
+ is CreateTwinWalletMode.CreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
@@ -41,7 +40,7 @@ data class TwinCardsState(
TwinCardsStep.TopUpWallet,
TwinCardsStep.Done,
)
- CreateTwinWalletMode.RecreateWallet -> listOf(
+ is CreateTwinWalletMode.RecreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
@@ -55,13 +54,15 @@ data class TwinCardsState(
val twinningInProgress: Boolean
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
-
- val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ ->
- store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency)
- }
}
-enum class CreateTwinWalletMode { CreateWallet, RecreateWallet }
+sealed class CreateTwinWalletMode {
+ data object CreateWallet : CreateTwinWalletMode()
+
+ data class RecreateWallet(
+ val scanResponse: ScanResponse,
+ ) : CreateTwinWalletMode()
+}
sealed class TwinCardsStep {
object None : TwinCardsStep()
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..eda118c3dc 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,15 @@ 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.OnboardingWalletBalance
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
@@ -52,15 +56,19 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment(
override fun configureTransitions() {
when (store.state.twinCardsState.mode) {
- CreateTwinWalletMode.CreateWallet -> {
+ is CreateTwinWalletMode.CreateWallet -> {
super.configureTransitions()
}
- CreateTwinWalletMode.RecreateWallet -> {
+ is CreateTwinWalletMode.RecreateWallet -> {
configureDefaultTransactions()
}
}
}
+ override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider(
+ scanResponseProvider = Provider { getActualScanResponse() },
+ )
+
@Suppress("MagicNumber")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -330,7 +338,7 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment(
else -> {}
}
- if (state.isBuyAllowed) {
+ if (availableForBuy(getActualScanResponse(), state.walletBalance)) {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
@@ -383,8 +391,8 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment(
tvBody.setText(R.string.onboarding_done_body)
val layout = when (state.mode) {
- CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins
- CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done
+ is CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins
+ is CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done
}
updateConstraints(state.currentStep, layout)
}
@@ -412,6 +420,18 @@ internal class OnboardingTwinsFragment : BaseOnboardingFragment(
}
}
+ private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean {
+ scanResponse ?: return false
+ return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency)
+ }
+
+ private fun getActualScanResponse(): ScanResponse {
+ return store.state.twinCardsState.welcomeOnlyScanResponse
+ ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
+ ?: store.state.detailsState.scanResponse
+ ?: error("ScanResponse must be not null")
+ }
+
override fun handleOnBackPressed() {
store.dispatch(
TwinCardsAction.OnBackPressed { should, popAction ->
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 2bf1bc1b5e..0e32a392c4 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
@@ -38,6 +35,7 @@ import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.R
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@@ -47,6 +45,8 @@ object OnboardingWalletMiddleware {
val handler = onboardingWalletMiddleware
}
+private const val HIDE_PROGRESS_DELAY = 400L
+
private val onboardingWalletMiddleware: Middleware = { dispatch, state ->
{ next ->
{ action ->
@@ -164,17 +164,13 @@ private fun handleWalletAction(action: Action) {
store.dispatch(GlobalAction.Onboarding.Stop)
if (scanResponse == null) {
- store.dispatch(NavigationAction.PopBackTo())
- store.dispatch(HomeAction.ReadCard(scope = action.scope))
+ action.scope.launch {
+ readCard { newScanResponse ->
+ handleFinishOnboardind(newScanResponse)
+ }
+ }
} else {
- val backupState = store.state.onboardingWalletState.backupState
- val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
- OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
- scanResponse = updatedScanResponse,
- accessCode = backupState.accessCode,
- backupCardsIds = backupState.backupCardIds,
- hasBackupError = backupState.hasBackupError,
- )
+ handleFinishOnboardind(scanResponse)
}
}
is OnboardingWalletAction.ResumeBackup -> {
@@ -199,6 +195,43 @@ private fun handleWalletAction(action: Action) {
}
}
+private fun handleFinishOnboardind(scanResponse: ScanResponse) {
+ val backupState = store.state.onboardingWalletState.backupState
+ val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
+ OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
+ scanResponse = updatedScanResponse,
+ accessCode = backupState.accessCode,
+ backupCardsIds = backupState.backupCardIds,
+ hasBackupError = backupState.hasBackupError,
+ )
+}
+
+private suspend fun readCard(onSuccess: (ScanResponse) -> Unit) {
+ val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
+
+ store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
+ isBiometricsRequestPolicy = shouldSaveAccessCodes,
+ )
+
+ store.inject(DaggerGraphState::scanCardProcessor).scan(
+ analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Intro,
+ onProgressStateChange = { showProgress ->
+ if (showProgress) {
+ store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
+ } else {
+ delay(HIDE_PROGRESS_DELAY)
+ store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
+ }
+ },
+ onFailure = {
+ Timber.e(it, "Unable to scan card")
+ delay(HIDE_PROGRESS_DELAY)
+ store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
+ },
+ onSuccess = onSuccess,
+ )
+}
+
private suspend fun loadArtworkForUnfinishedBackup(
cardId: String,
cardPublicKey: ByteArray,
@@ -529,7 +562,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 +648,7 @@ private fun handleOnBackPressed(state: OnboardingWalletState) {
}
BackupStep.Finished -> {
OnboardingHelper.onInterrupted()
- store.dispatch(NavigationAction.PopBackTo())
+ store.dispatchNavigationAction(AppRouter::pop)
}
}
}
@@ -625,7 +659,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/AttestationFailedDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt
index 3e9df5885e..5b6c8b8c85 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt
@@ -13,7 +13,7 @@ internal object AttestationFailedDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_error)
setMessage(R.string.issuer_signature_loading_failed)
- setPositiveButton(R.string.ok) { dialog, _ ->
+ setPositiveButton(R.string.common_ok) { dialog, _ ->
dialog.dismiss()
}
setOnDismissListener {
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/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt
index c7679a466d..4206df1904 100644
--- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt
+++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt
@@ -2,13 +2,7 @@ package com.tangem.tap.features.saveWallet.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.widthIn
+import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
@@ -20,16 +14,10 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
-import com.tangem.core.ui.components.PrimaryButton
-import com.tangem.core.ui.components.SpacerH16
-import com.tangem.core.ui.components.SpacerH32
-import com.tangem.core.ui.components.SpacerH4
-import com.tangem.core.ui.components.SpacerHHalf
-import com.tangem.core.ui.components.SpacerW24
-import com.tangem.core.ui.components.SpacerW8
+import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.atoms.Hand
-import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.wallet.R
@Composable
@@ -37,10 +25,7 @@ internal fun SaveWalletScreenContent(showProgress: Boolean, onSaveWalletClick: (
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Header(onCloseClick = onCloseClick)
SpacerHHalf()
- Title(
- modifier = Modifier
- .widthIn(max = TangemTheme.dimens.size200),
- )
+ Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing22))
SpacerH32()
Description(
modifier = Modifier
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/BriefNetworksList.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt
index e122327ff8..4091e53c06 100644
--- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt
+++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/BriefNetworksList.kt
@@ -4,7 +4,10 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.Text
@@ -123,14 +126,14 @@ internal fun HasMoreItem(moreCount: Int) {
) {
Text(
modifier = Modifier
- .padding(TangemTheme.dimens.spacing4)
.align(Alignment.Center)
.drawWithContent { if (readyToDraw) drawContent() },
text = "+$count",
style = textStyle,
+ color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Clip,
onTextLayout = { textLayoutResult ->
- if (textLayoutResult.didOverflowHeight) {
+ if (textLayoutResult.hasVisualOverflow) {
textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9)
} else {
readyToDraw = true
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..41495924b3 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
}
@@ -135,6 +135,7 @@ internal class TokensListMigration(
derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
+ store.dispatchNavigationAction { popTo() }
}
.onLeft { Timber.e(it, "Failed to derive public keys") }
}
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..6e471d192d 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
@@ -12,8 +12,6 @@ import androidx.paging.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
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 +24,6 @@ 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.fullNameWithoutTestnet
import com.tangem.tap.common.extensions.getNetworkName
import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType
@@ -326,7 +323,6 @@ internal class TokensListViewModel @Inject constructor(
)
uiState = state.copy(isSavingInProgress = false)
- store.dispatchWithMain(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
}
}
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..abb2a86125 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,17 @@
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.staking.model.stakekit.Yield
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.*
@@ -58,6 +57,11 @@ object TradeCryptoMiddleware {
is TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is TradeCryptoAction.Sell -> proceedSellAction(action)
is TradeCryptoAction.Swap -> openSwap(currency = action.cryptoCurrency)
+ is TradeCryptoAction.Stake -> openStaking(
+ userWalletId = action.userWalletId,
+ cryptoCurrencyId = action.cryptoCurrencyId,
+ yield = action.yield,
+ )
is TradeCryptoAction.SendToken -> {
if (isSendRedesignedEnabled) {
handleNewSendToken(action = action)
@@ -150,7 +154,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 +162,19 @@ object TradeCryptoMiddleware {
}
private fun openSwap(currency: CryptoCurrency) {
- val bundle = bundleOf(
- SwapFragment.CURRENCY_BUNDLE_KEY to currency,
- )
+ store.dispatchNavigationAction { push(AppRoute.Swap(currency = currency)) }
+ }
- store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
+ private fun openStaking(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yield: Yield) {
+ store.dispatchNavigationAction {
+ push(
+ AppRoute.Staking(
+ userWalletId = userWalletId,
+ cryptoCurrencyId = cryptoCurrencyId,
+ yield = yield,
+ ),
+ )
+ }
}
private fun handleSendToken(action: TradeCryptoAction.SendToken) {
@@ -214,11 +226,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 +298,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 +316,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..0ecd71bd5d 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)
+ ?.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/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
index e6c523e666..a8861eed2f 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
@@ -1,5 +1,6 @@
package com.tangem.tap.network.exchangeServices
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
@@ -32,7 +33,8 @@ internal class BuyExchangeService(
override fun isSellAllowed(): Boolean = currentService.isSellAllowed()
- override fun availableForBuy(currency: Currency): Boolean = currentService.availableForBuy(currency)
+ override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean =
+ currentService.availableForBuy(scanResponse, currency)
override fun availableForSell(currency: Currency): Boolean = currentService.availableForSell(currency)
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt
index 0f4fd42f7d..696f9288bc 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt
@@ -2,6 +2,7 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.features.demo.isDemoCard
@@ -38,8 +39,8 @@ class CardExchangeRules(
}
}
- override fun availableForBuy(currency: Currency): Boolean {
- val card = cardProvider() ?: return false
+ override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
+ val card = scanResponse.card
return when {
card.isDemoCard() -> true
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
index e962f7ebf4..fb01befe82 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
@@ -8,6 +8,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
@@ -38,8 +39,9 @@ class CurrencyExchangeManager(
override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed()
override fun isSellAllowed(): Boolean = primaryRules.isSellAllowed() && sellService.isSellAllowed()
- override fun availableForBuy(currency: Currency): Boolean {
- return primaryRules.availableForBuy(currency) && buyService.availableForBuy(currency)
+ override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
+ return primaryRules.availableForBuy(scanResponse, currency) &&
+ buyService.availableForBuy(scanResponse, currency)
}
override fun availableForSell(currency: Currency): Boolean {
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt
index 76de7375da..904e22c80e 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt
@@ -1,13 +1,15 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.domain.exchange.RampStateManager
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
class DefaultRampManager(private val exchangeService: ExchangeService?) : RampStateManager {
private val cryptoCurrencyConverter = CryptoCurrencyConverter()
- override fun availableForBuy(cryptoCurrency: CryptoCurrency): Boolean {
+ override fun availableForBuy(scanResponse: ScanResponse, cryptoCurrency: CryptoCurrency): Boolean {
return exchangeService?.availableForBuy(
+ scanResponse,
currency = cryptoCurrencyConverter.convertBack(cryptoCurrency),
) ?: false
}
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt
index e9b17488e3..1cfd8502ed 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt
@@ -1,5 +1,6 @@
package com.tangem.tap.network.exchangeServices
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.feature.Feature
import com.tangem.tap.domain.model.Currency
@@ -7,7 +8,7 @@ import com.tangem.tap.domain.model.Currency
interface Exchanger {
fun isBuyAllowed(): Boolean
fun isSellAllowed(): Boolean
- fun availableForBuy(currency: Currency): Boolean
+ fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean
fun availableForSell(currency: Currency): Boolean
}
@@ -20,7 +21,7 @@ interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder {
override suspend fun update() {}
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean = false
- override fun availableForBuy(currency: Currency): Boolean = false
+ override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
override fun getUrl(
action: CurrencyExchangeManager.Action,
@@ -45,7 +46,7 @@ interface ExchangeRules : Feature, Exchanger {
override fun featureIsSwitchedOn(): Boolean = false
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean = false
- override fun availableForBuy(currency: Currency): Boolean = false
+ override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
}
}
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt
index d653e2babe..8bcc8168e9 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt
@@ -6,6 +6,7 @@ import com.tangem.common.extensions.calculateSha512
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@@ -28,7 +29,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E
override fun isSellAllowed(): Boolean = false
- override fun availableForBuy(currency: Currency): Boolean {
+ override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean {
if (!isBuyAllowed()) return false
val mercuryoNetwork = currency.blockchain.mercuryoNetwork()
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
index c004c38b21..de34810420 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt
@@ -7,6 +7,7 @@ import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.datasource.api.common.createRetrofitInstance
import com.tangem.domain.common.extensions.withIOContext
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@@ -83,7 +84,7 @@ class MoonPayService(
return status?.responseUserStatus?.isSellAllowed ?: false
}
- override fun availableForBuy(currency: Currency): Boolean = false
+ override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean {
if (!isSellAllowed()) return false
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 8177d10df8..cfd1fc53f2 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,
@@ -364,60 +242,6 @@ class TransactionManagerImpl(
return createMultipleProxyFees(gasPrice, gas, blockchain)
}
- 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,
@@ -431,24 +255,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
*
@@ -502,46 +308,6 @@ class TransactionManagerImpl(
)
}
- private fun createAmount(amount: BigDecimal, currency: Currency, blockchain: Blockchain): Amount {
- return when (currency) {
- is Currency.NativeToken -> {
- Amount(value = amount, blockchain = blockchain)
- }
- is Currency.NonNativeToken -> {
- Amount(convertNonNativeToken(currency), amount)
- }
- }
- }
-
- 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,
- symbol = token.symbol,
- contractAddress = token.contractAddress,
- decimals = token.decimalCount,
- id = token.id,
- )
- }
-
private fun convertToProxyAmount(amount: Amount): ProxyAmount {
return ProxyAmount(
currencySymbol = amount.currencySymbol,
@@ -565,8 +331,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/drawable/ic_more_cards.xml b/app/src/main/res/drawable/ic_more_cards.xml
deleted file mode 100644
index 0cc559021b..0000000000
--- a/app/src/main/res/drawable/ic_more_cards.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
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/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt
index fdf0ed8db2..12be6b889d 100644
--- a/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt
+++ b/app/src/test/kotlin/com/tangem/tap/domain/card/DefaultDerivationsRepositoryTest.kt
@@ -12,7 +12,9 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
-import io.mockk.*
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -144,7 +146,7 @@ internal class DefaultDerivationsRepositoryTest {
coEvery { tangemSdkManager.derivePublicKeys(null, any()) } returns CompletionResult.Success(
DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys),
)
- coEvery { userWalletsStore.update(defaultUserWalletId, any()) } just Runs
+ coEvery { userWalletsStore.update(defaultUserWalletId, any()) } returns CompletionResult.Success(userWallet)
runCatching {
repository.derivePublicKeys(
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..a492ede7aa
--- /dev/null
+++ b/common/routing/build.gradle.kts
@@ -0,0 +1,27 @@
+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.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..6cd73dfcb0
--- /dev/null
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
@@ -0,0 +1,261 @@
+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.stakekit.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 ""}")
+
+ @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 class DetailsSecurity(
+ val userWalletId: UserWalletId,
+ ) : AppRoute(path = "/details/security"), RouteBundleParams {
+
+ override fun getBundle(): Bundle = bundle(serializer())
+
+ companion object {
+ const val USER_WALLET_ID_KEY = "userWalletId"
+ }
+ }
+
+ @Serializable
+ data class CardSettings(
+ val userWalletId: UserWalletId,
+ ) : AppRoute(path = "/card_settings/${userWalletId.stringValue}"), RouteBundleParams {
+
+ override fun getBundle(): Bundle = bundle(serializer())
+
+ companion object {
+ const val USER_WALLET_ID_KEY = "userWalletId"
+ }
+ }
+
+ @Serializable
+ data object AppSettings : AppRoute(path = "/app_settings")
+
+ /**
+ * Reset to factory
+ *
+ * @property userWalletId user wallet id
+ * @property cardId reset card id
+ * @property isActiveBackupStatus reset backup card status
+ * @property backupCardsCount backup cards count
+ */
+ @Serializable
+ data class ResetToFactory(
+ val userWalletId: UserWalletId,
+ val cardId: String,
+ val isActiveBackupStatus: Boolean,
+ val backupCardsCount: Int,
+ ) : AppRoute(
+ path = "/reset_to_factory" +
+ "/${userWalletId.stringValue}" +
+ "/$cardId" +
+ "/$isActiveBackupStatus" +
+ "/$backupCardsCount",
+ ),
+ RouteBundleParams {
+
+ override fun getBundle(): Bundle = bundle(serializer())
+
+ companion object {
+ const val USER_WALLET_ID = "userWalletId"
+ const val CARD_ID = "cardId"
+ const val IS_ACTIVE_BACKUP_STATUS = "isActiveBackupStatus"
+ const val BACKUP_CARDS_COUNT = "backupCardsCount"
+ }
+ }
+
+ @Serializable
+ data object AccessCodeRecovery : AppRoute(path = "/access_code_recovery"), RouteBundleParams {
+
+ override fun getBundle(): Bundle = bundle(serializer())
+ }
+
+ @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 class ReferralProgram(
+ val userWalletId: UserWalletId,
+ ) : AppRoute(path = "/referral_program"), RouteBundleParams {
+
+ override fun getBundle(): Bundle = bundle(serializer())
+
+ companion object {
+ const val USER_WALLET_ID_KEY = "userWalletId"
+ }
+ }
+
+ @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 61%
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 7e31eb9e60..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,42 +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 providerName: String,
+ 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/analytics/build.gradle.kts b/core/analytics/build.gradle.kts
index 9d05970191..c61c8e7af2 100644
--- a/core/analytics/build.gradle.kts
+++ b/core/analytics/build.gradle.kts
@@ -15,6 +15,7 @@ dependencies {
/** Domain */
implementation(projects.domain.analytics)
+ implementation(projects.domain.models)
/** Other */
implementation(deps.kotlin.coroutines)
diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
index c7c73612f8..093769910b 100644
--- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
+++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt
@@ -65,6 +65,7 @@ sealed class AnalyticsParam {
data object Intro : ScreensSources("Introduction")
data object MyWallets : ScreensSources("My Wallets")
data object Token : ScreensSources("Token")
+ data object Stories : ScreensSources("Stories")
}
sealed class TxSentFrom(val value: String) {
diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt b/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt
new file mode 100644
index 0000000000..cab4da6a7b
--- /dev/null
+++ b/core/analytics/src/main/java/com/tangem/core/analytics/utils/AnalyticsContextProxy.kt
@@ -0,0 +1,17 @@
+package com.tangem.core.analytics.utils
+
+import com.tangem.domain.models.scan.ScanResponse
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface AnalyticsContextProxy {
+
+ fun setContext(scanResponse: ScanResponse)
+
+ fun eraseContext()
+
+ fun addContext(scanResponse: ScanResponse)
+
+ fun removeContext()
+}
\ No newline at end of file
diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts
index 4deb2329d1..0a236d6e5a 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.models)
/** 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..b1e8675206
--- /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..2d5bd551ac 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,77 @@
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/pending")
+ suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse