Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-28 09:56:00 +03:00
parent 3d16de3f4f
commit d65b4039da
15 changed files with 26 additions and 945 deletions

View file

@ -1,65 +0,0 @@
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.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(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)
}
private class ComponentBuilder<C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>>(
private val contextProvider: Provider<AppComponentContext>,
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<String, ComponentBuilder<*, *, *>>()
fun <C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>> newInstance(
tag: String,
contextProvider: Provider<AppComponentContext>,
params: P,
componentFactory: F,
): Fragment {
this@Companion.componentsBuilders[tag] = ComponentBuilder(contextProvider, params, componentFactory)
return DecomposeFragment().apply {
this.arguments = bundleOf(TAG_KEY to tag)
}
}
}
}

View file

@ -12,7 +12,6 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
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
@ -20,19 +19,11 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE
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.subscribe
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.entity.SerializableIntent
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -41,12 +32,6 @@ import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.data.card.sdk.CardSdkOwner
import com.tangem.domain.apptheme.model.AppThemeMode
@ -70,11 +55,8 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.common.ActivityResultCallbackHolder
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.showFragmentAllowingStateLoss
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
@ -85,11 +67,8 @@ import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.toggle.RoutingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -102,7 +81,6 @@ lateinit var tangemSdkManager: TangemSdkManager
lateinit var backupService: BackupService
internal var lockUserWalletsTimer: LockUserWalletsTimer? = null
private set
var notificationsHandler: NotificationsHandler? = null
private val coroutineContext: CoroutineContext
get() = SupervisorJob() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope")
@ -114,7 +92,7 @@ val mainScope = CoroutineScope(mainCoroutineContext)
@Suppress("LargeClass")
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
@Inject
lateinit var appStateHolder: AppStateHolder
@ -187,9 +165,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var dispatchers: CoroutineDispatcherProvider
@Inject
internal lateinit var routingFeatureToggles: RoutingFeatureToggles
@Inject
internal lateinit var uiDependencies: UiDependencies
@ -203,9 +178,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
// TODO: fixme: inject through DI
private val intentProcessor: IntentProcessor = IntentProcessor()
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
@ -242,13 +215,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
installActivityDependencies()
observeAppThemeModeUpdates()
if (routingFeatureToggles.isNavigationRefactoringEnabled) {
setRootContent()
} else {
setContentView(R.layout.activity_main)
installRouting()
installEventMessageEffect()
}
setRootContent()
initContent()
@ -265,33 +232,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lifecycle.addObserver(defaultDeviceFlipDetector)
}
private fun installEventMessageEffect() {
binding.composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
binding.composeView.setContent {
TangemTheme(
activity = this,
uiDependencies = uiDependencies,
overrideSystemBarColors = false,
) {
EventMessageEffect(
onShowSnackbar = { message, _ ->
showSnackbar(
text = message.message.resolveReference(resources),
length = when (message.duration) {
SnackbarMessage.Duration.Short -> Snackbar.LENGTH_SHORT
SnackbarMessage.Duration.Long -> Snackbar.LENGTH_LONG
SnackbarMessage.Duration.Indefinite -> Snackbar.LENGTH_INDEFINITE
},
buttonTitle = message.actionLabel?.resolveReference(resources),
action = message.action,
onDismiss = message.onDismissRequest,
)
},
)
}
}
}
private fun setRootContent() {
// for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy
// doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it
@ -306,54 +246,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
private fun installRouting() {
// for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy
// doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it
// we try to init component with previous stack
val routingComponent = routingComponentFactory.create(
context = rootComponentContext,
initialStack = appRouterConfig.stack,
)
appRouterConfig.routerScope = mainScope
appRouterConfig.componentRouter = routingComponent.router
appRouterConfig.snackbarHandler = this
routingComponent.stack.subscribe(lifecycle.asEssentyLifecycle()) { childStack ->
val stack = childStack.backStack
.plus(childStack.active)
.map { it.configuration }
if (stack == appRouterConfig.stack) return@subscribe
appRouterConfig.stack = stack
if (stack.size == 1) {
supportFragmentManager.run {
val activeChildName = stack.first().path
(0 until backStackEntryCount)
.mapNotNull { getBackStackEntryAt(it).name }
.filter { it != activeChildName }
.forEach {
popBackStackImmediate(it, POP_BACK_STACK_INCLUSIVE)
}
}
}
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)
}
is RoutingComponent.Child.ComposableComponent -> error("Unsupported child: $child")
}
}
}
private fun installActivityDependencies() {
cardSdkOwner.register(activity = this)
tangemSdkManager = injectedTangemSdkManager
@ -429,17 +321,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onResume() {
super.onResume()
if (!routingFeatureToggles.isNavigationRefactoringEnabled) {
// TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
}
navigateToInitialScreenIfNeeded(intent)
}
override fun onStop() {
notificationsHandler = null
dialogManager.onStop()
super.onStop()
}
@ -472,31 +357,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme()
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
/*
* We need to manually change the background color of the activity when the UI mode changes to prevent
* flickering when navigating between fragments.
*
* `android:configChanges="uiMode"` is set in the manifest.
* */
if (routingFeatureToggles.isNavigationRefactoringEnabled.not()) {
updateAppBackground()
}
}
private fun updateAppBackground() {
val backgroundColor = if (isDarkTheme()) {
TangemColorPalette.Dark6
} else {
TangemColorPalette.White
}
findViewById<CoordinatorLayout>(R.id.fragment_container).setBackgroundColor(backgroundColor.toArgb())
}
private fun isDarkTheme(): Boolean {
return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> true
@ -518,29 +378,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
override fun showSnackbar(@StringRes text: Int, length: Int, @StringRes buttonTitle: Int?, action: (() -> Unit)?) {
showSnackbar(
getString(text),
length,
buttonTitle?.let(::getString),
action = { action?.invoke() },
)
}
override fun showSnackbar(text: TextReference, length: Int, buttonTitle: TextReference?, action: (() -> Unit)?) {
showSnackbar(
text = text.resolveReference(resources),
length = length,
buttonTitle = buttonTitle?.resolveReference(resources),
action = { action?.invoke() },
)
}
override fun dismissSnackbar() {
snackbar?.dismiss()
snackbar = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@ -568,40 +405,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
return if (result) super.dispatchTouchEvent(event) else false
}
private fun showSnackbar(
text: String,
length: Int,
buttonTitle: String?,
action: (() -> Unit)? = null,
onDismiss: () -> Unit = {},
) {
if (snackbar != null) return
snackbar = Snackbar.make(binding.fragmentContainer, text, length).apply {
val textColor = getColor(R.color.text_primary_2)
setBackgroundTint(getColor(R.color.button_primary))
setActionTextColor(textColor)
setTextColor(textColor)
if (buttonTitle != null && action != null) {
setAction(buttonTitle, { action() })
}
addCallback(
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
onDismiss()
snackbar = null
removeCallback(this)
}
},
)
}
snackbar?.show()
}
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
val backStack = appRouterConfig.stack ?: emptyList()
// TODO move inital navigation to navigation component ([REDACTED_JIRA])

View file

@ -1,60 +0,0 @@
package com.tangem.tap.common.extensions
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 FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider<Fragment>) {
if (backStackEntryCount > 0) {
val currentFragmentName = getBackStackEntryAt(backStackEntryCount - 1).name
if (name == currentFragmentName) {
Timber.i("Fragment $name is already at the top of the stack")
return
}
}
Timber.i("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.i("Route $name is shown")
} else {
Timber.i("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 Fragment.showFragment(fragmentManager: FragmentManager, name: String) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, this, name)
transaction.addToBackStack(name)
transaction.commitAllowingStateLoss()
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.extensions
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.redux.StateDialog
import com.tangem.domain.wallets.models.UserWallet
@ -43,20 +45,16 @@ suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet) {
state.globalState.tapWalletManager.onWalletSelected(userWallet)
}
fun Store<*>.dispatchErrorNotification(error: TapError) {
dispatchOnMain(GlobalAction.ShowErrorNotification(error))
}
/**
* @param fatal used to indicate errors that should not normally occur
*/
fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
fun Store<AppState>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
}
fun Store<*>.dispatchDebugErrorNotification(error: TapError) {
dispatchOnMain(GlobalAction.DebugShowErrorNotification(error))
fun Store<AppState>.dispatchDebugErrorNotification(error: TapError) {
inject(DaggerGraphState::uiMessageSender).send(SnackbarMessage(stringReference(error.message ?: "debug error")))
}
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {

View file

@ -31,7 +31,6 @@ data class AppState(
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware,
notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
DetailsMiddleware().detailsMiddleware,

View file

@ -1,141 +0,0 @@
package com.tangem.tap.common.redux
import android.content.Context
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.domain.ArgError
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.notificationsHandler
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.lang.ref.WeakReference
class NotificationsHandler(coordinatorLayout: CoordinatorLayout) {
private val basicCoordinatorLayout = WeakReference(coordinatorLayout)
private var baseLayout = basicCoordinatorLayout
private fun showNotification(message: String) {
baseLayout.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar -> snackbar.show() }
}
}
private fun showDebugNotification(message: String) {
baseLayout.get()?.let { layout ->
Snackbar.make(layout, message, Snackbar.LENGTH_LONG)
.also { snackbar ->
snackbar.setBackgroundTint(layout.getColor(R.color.warning_warning))
snackbar.show()
}
}
}
fun showNotification(message: Int, args: List<Any>? = null) {
baseLayout.get()?.let {
showNotification(getMessageString(it.context, message, args))
}
}
fun showToastNotification(message: Int, args: List<Any>? = null) {
baseLayout.get()?.let {
Toast.makeText(it.context, getMessageString(it.context, message, args), Toast.LENGTH_LONG).show()
}
}
fun showNotification(errorList: List<Pair<Int, List<Any>?>>, builder: (List<String>) -> String) {
val context = baseLayout.get()?.context ?: return
val message = builder(errorList.map { getMessageString(context, it.first, it.second) })
showNotification(message)
}
fun showDebugErrorNotification(message: Int, args: List<Any>? = null) {
baseLayout.get()?.let {
showDebugNotification(getMessageString(it.context, message, args))
}
}
}
fun getMessageString(context: Context, message: Int, args: List<Any>?): String {
return if (args.isNullOrEmpty()) {
context.getString(message)
} else {
context.getString(message, *args.toTypedArray())
}
}
val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleNotificationAction(action)
next(action)
}
}
}
@Suppress("ComplexMethod")
private fun handleNotificationAction(action: Action) {
if (action is Debug && !BuildConfig.DEBUG) return
when (action) {
is NotificationAction -> {
notificationsHandler?.showNotification(action.messageResource)
}
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action) {
is Debug -> {
val args = (action.error as? ArgError)?.args ?: listOf()
when (action) {
is DebugNotification -> {
notificationsHandler?.showNotification(action.error.messageResource, args)
}
is DebugToastNotification -> {
notificationsHandler?.showToastNotification(action.error.messageResource, args)
}
is DebugErrorAction -> {
notificationsHandler?.showDebugErrorNotification(action.error.messageResource, args)
}
}
}
else -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.messageResource, args)
}
}
}
}
}
}
}
interface ToastNotificationAction : Action {
val messageResource: Int
}
interface NotificationAction : Action {
val messageResource: Int
}
interface ErrorAction : Action {
val error: TapError
}
// Processed only in the debug builds
interface Debug
interface DebugNotification : Debug, NotificationAction
interface DebugToastNotification : Debug, ToastNotificationAction
interface DebugErrorAction : Debug, ErrorAction

View file

@ -5,17 +5,10 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.domain.TapError
import org.rekotlin.Action
sealed class GlobalAction : Action {
// notifications
data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction
data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()

View file

@ -42,7 +42,11 @@ internal class ProxyAppRouter(
override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) {
safeNavigate(onComplete, message = "Replace all routes with $routes") {
innerRouter.replaceAll(*routes, onComplete = onComplete)
runCatching {
innerRouter.replaceAll(*routes, onComplete = onComplete)
}.getOrElse {
Timber.tag("ASDASD").e(it)
}
}
}
@ -78,6 +82,7 @@ internal class ProxyAppRouter(
override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) {
if (!isSuccess) {
Timber.tag("ASDASD").d(errorMessage)
analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage)))
Timber.w(errorMessage)

View file

@ -3,22 +3,13 @@ package com.tangem.tap.routing.component
import android.content.Intent
import androidx.compose.runtime.Immutable
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.core.ui.decompose.ComposableContentComponent
import com.tangem.utils.Provider
internal interface RoutingComponent : ComposableContentComponent {
// TODO: Remove after full navigation refactoring
val router: Router
// TODO: Remove after full navigation refactoring
val stack: Value<ChildStack<AppRoute, Child>>
@Immutable
sealed class Child {

View file

@ -1,14 +1,11 @@
package com.tangem.tap.routing.component.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
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.decompose.value.subscribe
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
@ -23,7 +20,6 @@ import com.tangem.tap.routing.RootContent
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
import com.tangem.tap.routing.configurator.AppRouterConfig
import com.tangem.tap.routing.toggle.RoutingFeatureToggles
import com.tangem.tap.routing.utils.ChildFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -36,35 +32,30 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val childFactory: ChildFactory,
private val appRouterConfig: AppRouterConfig,
private val uiDependencies: UiDependencies,
routingFeatureToggles: RoutingFeatureToggles,
) : RoutingComponent,
AppComponentContext by context,
SnackbarHandler {
override val stack: Value<ChildStack<AppRoute, Child>> = childStack(
private val stack: Value<ChildStack<AppRoute, Child>> = childStack(
source = navigationProvider.getOrCreateTyped(),
initialStack = { getInitialStackOrInit() },
serializer = null, // AppRoute.serializer(), // Disabled until Nav refactoring completes
handleBackButton = routingFeatureToggles.isNavigationRefactoringEnabled,
childFactory = ::child,
handleBackButton = true,
childFactory = { route, childContext ->
childFactory.createChild(route, childByContext(childContext))
},
)
init {
lifecycle.doOnDestroy {
childFactory.doOnDestroy()
}
appRouterConfig.routerScope = componentScope
appRouterConfig.componentRouter = router
appRouterConfig.snackbarHandler = this
if (routingFeatureToggles.isNavigationRefactoringEnabled) {
appRouterConfig.routerScope = componentScope
appRouterConfig.componentRouter = router
appRouterConfig.snackbarHandler = this
stack.subscribe(lifecycle) { stack ->
val stackItems = stack.items.map { it.configuration }
stack.subscribe(lifecycle) { stack ->
val stackItems = stack.items.map { it.configuration }
if (appRouterConfig.stack != stackItems) {
appRouterConfig.stack = stackItems
}
if (appRouterConfig.stack != stackItems) {
appRouterConfig.stack = stackItems
}
}
}
@ -114,10 +105,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
initialStack
}
private fun child(route: AppRoute, context: ComponentContext): Child {
return childFactory.createChild(route) { childByContext(context) }
}
@AssistedFactory
interface Factory : RoutingComponent.Factory {
override fun create(context: AppComponentContext, initialStack: List<AppRoute>?): DefaultRoutingComponent

View file

@ -1,14 +0,0 @@
package com.tangem.tap.routing.toggle
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class RoutingFeatureToggles @Inject constructor(
private val featureTogglesManager: FeatureTogglesManager,
) {
val isNavigationRefactoringEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "NAVIGATION_REFACTORING")
}

View file

@ -35,10 +35,7 @@ import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectCompone
import com.tangem.tap.features.home.api.HomeComponent
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
import com.tangem.tap.routing.toggle.RoutingFeatureToggles
import com.tangem.utils.Provider
import dagger.hilt.android.scopes.ActivityScoped
import java.util.WeakHashMap
import javax.inject.Inject
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent
@ -80,20 +77,11 @@ internal class ChildFactory @Inject constructor(
private val nftComponentFactory: NFTComponent.Factory,
private val nftSendComponentFactory: NFTSendComponent.Factory,
private val testerRouter: TesterRouter,
private val routingFeatureToggles: RoutingFeatureToggles,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
) {
fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child {
return if (routingFeatureToggles.isNavigationRefactoringEnabled) {
createChildNew(route, contextFactory(route))
} else {
createChildLegacy(route, contextFactory)
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun createChildNew(route: AppRoute, context: AppComponentContext): Child {
fun createChild(route: AppRoute, context: AppComponentContext): Child {
return when (route) {
is AppRoute.Initial -> {
Child.Initial
@ -419,355 +407,5 @@ internal class ChildFactory @Inject constructor(
)
}
}
// endregion
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun createChildLegacy(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child {
componentContexts[route] = contextFactory(route)
// region Child creation
return when (route) {
is AppRoute.Initial -> {
Child.Initial
}
is AppRoute.AccessCodeRecovery -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = accessCodeRecoveryComponentFactory,
)
}
is AppRoute.AppCurrencySelector -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appCurrencySelectorComponentFactory,
)
}
is AppRoute.Send -> {
if (sendFeatureToggles.isSendV2Enabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = com.tangem.features.send.v2.api.SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactoryV2,
)
} else {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SendComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
transactionId = route.transactionId,
amount = route.amount,
tag = route.tag,
destinationAddress = route.destinationAddress,
),
componentFactory = sendComponentFactory,
)
}
}
is AppRoute.AppSettings -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = appSettingsComponentFactory,
)
}
is AppRoute.CardSettings -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = CardSettingsComponent.Params(userWalletId = route.userWalletId),
componentFactory = cardSettingsComponentFactory,
)
}
is AppRoute.Details -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = DetailsComponent.Params(route.userWalletId),
componentFactory = detailsComponentFactory,
)
}
is AppRoute.DetailsSecurity -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SecurityModeComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = securityModeComponentFactory,
)
}
is AppRoute.Disclaimer -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = DisclaimerComponent.Params(route.isTosAccepted),
componentFactory = disclaimerComponentFactory,
)
}
is AppRoute.Home -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = homeComponentFactory,
)
}
is AppRoute.ManageTokens -> {
val source = when (route.source) {
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
}
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ManageTokensComponent.Params(route.userWalletId, source),
componentFactory = manageTokensComponentFactory,
)
}
is AppRoute.QrScanning -> {
val source = when (route.source) {
is AppRoute.QrScanning.Source.Send -> SourceType.SEND
is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT
}
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = QrScanningComponent.Params(
source = source,
networkName = (route.source as? AppRoute.QrScanning.Source.Send)?.networkName,
),
componentFactory = qrScanningComponentFactory,
)
}
is AppRoute.ReferralProgram -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ReferralComponent.Params(route.userWalletId),
componentFactory = referralComponentFactory,
)
}
is AppRoute.ResetToFactory -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ResetCardComponent.Params(
userWalletId = route.userWalletId,
cardId = route.cardId,
isActiveBackupStatus = route.isActiveBackupStatus,
backupCardsCount = route.backupCardsCount,
),
componentFactory = resetCardComponentFactory,
)
}
is AppRoute.Swap -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SwapComponent.Params(
currencyFrom = route.currencyFrom,
currencyTo = route.currencyTo,
userWalletId = route.userWalletId,
isInitialReverseOrder = route.isInitialReverseOrder,
screenSource = route.screenSource,
),
componentFactory = swapComponentFactory,
)
}
is AppRoute.Wallet -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletComponentFactory,
)
}
is AppRoute.WalletConnectSessions -> {
if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = redesignedWalletConnectComponentFactory,
)
} else {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = walletConnectComponentFactory,
)
}
}
is AppRoute.CurrencyDetails -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = TokenDetailsComponent.Params(
userWalletId = route.userWalletId,
currency = route.currency,
),
componentFactory = tokenDetailsComponentFactory,
)
}
is AppRoute.Welcome -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = WelcomeComponent.Params(
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
)
}
is AppRoute.TesterMenu -> {
Child.LegacyIntent(testerRouter.getEntryIntent())
}
is AppRoute.Staking -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = StakingComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrencyId = route.cryptoCurrencyId,
yieldId = route.yieldId,
),
componentFactory = stakingComponentFactory,
)
}
is AppRoute.PushNotification -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = Unit,
componentFactory = pushNotificationsComponentFactory,
)
}
is AppRoute.WalletSettings -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = WalletSettingsComponent.Params(route.userWalletId),
componentFactory = walletSettingsComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = MarketsTokenDetailsComponent.Params(
token = route.token,
appCurrency = route.appCurrency,
showPortfolio = route.showPortfolio,
analyticsParams = route.analyticsParams?.let {
MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = it.blockchain,
source = it.source,
)
},
),
componentFactory = marketsTokenDetailsComponentFactory,
)
}
is AppRoute.Onramp -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrency = route.currency,
source = route.source,
),
componentFactory = onrampComponentFactory,
)
}
is AppRoute.OnrampSuccess -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampSuccessComponent.Params(route.txId),
componentFactory = onrampSuccessComponentFactory,
)
}
is AppRoute.BuyCrypto -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = BuyCryptoComponent.Params(userWalletId = route.userWalletId),
componentFactory = buyCryptoComponentFactory,
)
}
is AppRoute.SellCrypto -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SellCryptoComponent.Params(userWalletId = route.userWalletId),
componentFactory = sellCryptoComponentFactory,
)
}
is AppRoute.SwapCrypto -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId),
componentFactory = swapSelectTokensComponentFactory,
)
}
is AppRoute.Onboarding -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
mode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding
AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1
AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin
AppRoute.Onboarding.Mode.RecreateWalletTwin ->
OnboardingEntryComponent.Mode.RecreateWalletTwin
AppRoute.Onboarding.Mode.ContinueFinalize ->
OnboardingEntryComponent.Mode.ContinueFinalize
},
),
componentFactory = onboardingEntryComponentFactory,
)
}
is AppRoute.Stories -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = StoriesComponent.Params(
storyId = route.storyId,
nextScreen = route.nextScreen,
screenSource = route.screenSource,
),
componentFactory = storiesComponentFactory,
)
}
is AppRoute.NFT ->
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = NFTComponent.Params(
userWalletId = route.userWalletId,
walletName = route.walletName,
),
componentFactory = nftComponentFactory,
)
is AppRoute.NFTSend -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = NFTSendComponent.Params(
userWalletId = route.userWalletId,
nftAsset = route.nftAsset,
nftCollectionName = route.nftCollectionName,
),
componentFactory = nftSendComponentFactory,
)
}
}
// endregion
}
fun doOnDestroy() {
componentContexts.clear()
}
private fun contextProvider(
appRoute: AppRoute,
contextFactory: (route: AppRoute) -> AppComponentContext,
): Provider<AppComponentContext> = Provider {
componentContexts.getOrPut(appRoute) { contextFactory(appRoute) }
}
private companion object {
val componentContexts = WeakHashMap<AppRoute, AppComponentContext>()
}
}

View file

@ -1,43 +1,9 @@
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.component.RoutingComponent.Child
import com.tangem.utils.Provider
internal fun AppRoute.asFragmentChild(fragmentProvider: Provider<Fragment>): Child {
val provider = Provider {
val bundle = (this as? RouteBundleParams)?.getBundle()
fragmentProvider().apply {
arguments = bundle
}
}
return Child.LegacyFragment(path, provider)
}
internal fun <C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>> AppRoute.asComponentChild(
contextProvider: Provider<AppComponentContext>,
params: P,
componentFactory: F,
): Child {
val fragmentProvider = Provider {
DecomposeFragment.newInstance(
tag = path,
contextProvider = contextProvider,
params = params,
componentFactory = componentFactory,
)
}
return Child.LegacyFragment(path, fragmentProvider)
}
internal fun <C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>> createComponentChild(
context: AppComponentContext,