Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-18 13:06:49 +04:00
parent 1e7ef6ec50
commit e5c9320ea5
8 changed files with 325 additions and 82 deletions

View file

@ -85,7 +85,7 @@ import com.tangem.tap.features.main.model.Toast
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.routing.RoutingComponent
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
@ -307,6 +307,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
is RoutingComponent.Child.LegacyIntent -> {
startActivity(child.intent)
}
is RoutingComponent.Child.ComposableComponent -> error("Unsupported child: $child")
}
}
}
@ -410,8 +411,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onResume() {
super.onResume()
// TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
if (!routingFeatureToggles.isNavigationRefactoringEnabled) {
// TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
}
navigateToInitialScreenIfNeeded(intent)
}
@ -491,22 +495,22 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
}
override fun showSnackbar(
@StringRes text: Int,
length: Int,
@StringRes buttonTitle: Int?,
action: View.OnClickListener?,
) {
showSnackbar(getString(text), length, buttonTitle?.let(::getString), action)
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: View.OnClickListener?,
) {
showSnackbar(text.resolveReference(resources), length, buttonTitle?.resolveReference(resources), action)
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() {

View file

@ -1,6 +1,5 @@
package com.tangem.tap.common
import android.view.View
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
import com.tangem.core.ui.extensions.TextReference
@ -11,14 +10,14 @@ interface SnackbarHandler {
@StringRes text: Int,
length: Int = Snackbar.LENGTH_INDEFINITE,
@StringRes buttonTitle: Int? = null,
action: View.OnClickListener? = null,
action: (() -> Unit)? = null,
)
fun showSnackbar(
text: TextReference,
length: Int = Snackbar.LENGTH_INDEFINITE,
buttonTitle: TextReference? = null,
action: View.OnClickListener? = null,
action: (() -> Unit)? = null,
)
fun dismissSnackbar()

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di.routing
import com.tangem.tap.routing.RoutingComponent
import com.tangem.tap.routing.impl.DefaultRoutingComponent
import com.tangem.tap.routing.component.RoutingComponent
import com.tangem.tap.routing.component.impl.DefaultRoutingComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn

View file

@ -1,22 +1,28 @@
package com.tangem.tap.routing
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 {
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 {
// TODO: Remove and find initial intent in RoutingComponent: [REDACTED_JIRA]
data object Initial : Child()
data class LegacyFragment(
@ -25,6 +31,10 @@ internal interface RoutingComponent {
) : Child()
data class LegacyIntent(val intent: Intent) : Child()
data class ComposableComponent(
val component: ComposableContentComponent,
) : Child()
}
interface Factory {

View file

@ -0,0 +1,118 @@
package com.tangem.tap.routing.component.impl
import androidx.compose.material3.SnackbarDuration
import androidx.compose.runtime.Composable
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.observe
import com.arkivanov.essenty.backhandler.BackCallback
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
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.getOrCreateTyped
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.tap.common.SnackbarHandler
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
import dagger.assisted.AssistedInject
@Suppress("LongParameterList")
internal class DefaultRoutingComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
private val childFactory: ChildFactory,
private val appRouterConfig: AppRouterConfig,
private val routingFeatureToggles: RoutingFeatureToggles,
) : RoutingComponent,
AppComponentContext by context,
SnackbarHandler {
private val backCallback = BackCallback(priority = Int.MIN_VALUE, onBack = router::pop)
override val stack: Value<ChildStack<AppRoute, Child>> = childStack(
source = navigationProvider.getOrCreateTyped(),
serializer = AppRoute.serializer(), // TODO Maybe set this to null for AppRoute.Onboarding case. Need to check
initialConfiguration = getInitialRoute(),
handleBackButton = false,
childFactory = ::child,
)
init {
backHandler.register(backCallback)
lifecycle.doOnDestroy {
childFactory.doOnDestroy()
}
if (routingFeatureToggles.isNavigationRefactoringEnabled) {
appRouterConfig.routerScope = componentScope
appRouterConfig.componentRouter = router
appRouterConfig.snackbarHandler = this
stack.observe(lifecycle) { stack ->
val stackItems = stack.items.map { it.configuration }
if (appRouterConfig.stack != stackItems) {
appRouterConfig.stack = stackItems
}
}
}
}
@Composable
override fun Content(modifier: Modifier) {
// TODO: Implement in [REDACTED_JIRA]
}
override fun showSnackbar(text: Int, length: Int, buttonTitle: Int?, action: (() -> Unit)?) {
showSnackbar(
text = resourceReference(text),
length = length,
buttonTitle = buttonTitle?.let(::resourceReference),
action = action,
)
}
override fun showSnackbar(text: TextReference, length: Int, buttonTitle: TextReference?, action: (() -> Unit)?) {
val message = SnackbarMessage(
message = text,
duration = when (length) {
Snackbar.LENGTH_SHORT -> SnackbarDuration.Short
Snackbar.LENGTH_LONG -> SnackbarDuration.Long
Snackbar.LENGTH_INDEFINITE -> SnackbarDuration.Indefinite
else -> SnackbarDuration.Short
},
actionLabel = buttonTitle,
action = action,
)
messageSender.send(message)
}
override fun dismissSnackbar() {
messageSender.send(SnackbarMessage(message = TextReference.EMPTY))
}
// TODO: Find correct initial route here: [REDACTED_JIRA]
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
}
}

View file

@ -1,54 +0,0 @@
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<AppRoute, Child>> = childStack(
source = navigationProvider.getOrCreateTyped(),
serializer = AppRoute.serializer(), // TODO Maybe set this to null for AppRoute.Onboarding case. Need to check
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
}
}

View file

@ -33,8 +33,9 @@ import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCar
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.welcome.component.WelcomeComponent
import com.tangem.tap.features.welcome.ui.WelcomeFragment
import com.tangem.tap.routing.RoutingComponent.Child
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
@ -42,7 +43,7 @@ import java.util.WeakHashMap
import javax.inject.Inject
@ActivityScoped
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class ChildFactory @Inject constructor(
private val detailsComponentFactory: DetailsComponent.Factory,
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
@ -55,6 +56,7 @@ internal class ChildFactory @Inject constructor(
private val sellCryptoComponentFactory: SellCryptoComponent.Factory,
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val welcomeComponentFactory: WelcomeComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
private val walletRouter: WalletRouter,
@ -67,16 +69,169 @@ internal class ChildFactory @Inject constructor(
fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child {
return if (routingFeatureToggles.isNavigationRefactoringEnabled) {
TODO("Will be implemented in [REDACTED_JIRA]")
createChildNew(route, contextFactory)
} else {
createChildLegacy(route, contextFactory)
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
private fun createChildNew(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child {
componentContexts[route] = contextFactory(route)
// region Child creation
return when (route) {
is AppRoute.Initial -> {
Child.Initial
}
is AppRoute.Details -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = DetailsComponent.Params(route.userWalletId),
componentFactory = detailsComponentFactory,
)
}
is AppRoute.Disclaimer -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = DisclaimerComponent.Params(route.isTosAccepted),
componentFactory = disclaimerComponentFactory,
)
}
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
}
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = ManageTokensComponent.Params(route.userWalletId, source),
componentFactory = manageTokensComponentFactory,
)
}
is AppRoute.Welcome -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = WelcomeComponent.Params(
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
)
}
is AppRoute.TesterMenu -> {
Child.LegacyIntent(testerRouter.getEntryIntent())
}
is AppRoute.WalletSettings -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = WalletSettingsComponent.Params(route.userWalletId),
componentFactory = walletSettingsComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> {
createComponentChild(
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 -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampComponent.Params(
userWalletId = route.userWalletId,
cryptoCurrency = route.currency,
source = route.source,
),
componentFactory = onrampComponentFactory,
)
}
is AppRoute.OnrampSuccess -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnrampSuccessComponent.Params(route.externalTxId),
componentFactory = onrampSuccessComponentFactory,
)
}
is AppRoute.BuyCrypto -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = BuyCryptoComponent.Params(userWalletId = route.userWalletId),
componentFactory = buyCryptoComponentFactory,
)
}
is AppRoute.SellCrypto -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SellCryptoComponent.Params(userWalletId = route.userWalletId),
componentFactory = sellCryptoComponentFactory,
)
}
is AppRoute.SwapCrypto -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId),
componentFactory = swapSelectTokensComponentFactory,
)
}
is AppRoute.Onboarding -> {
createComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = OnboardingEntryComponent.Params(
scanResponse = route.scanResponse,
startBackupFlow = route.startFromBackup,
multiWalletMode = when (route.mode) {
AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.MultiWalletMode.Onboarding
AppRoute.Onboarding.Mode.AddBackup -> OnboardingEntryComponent.MultiWalletMode.AddBackup
},
),
componentFactory = onboardingEntryComponentFactory,
)
}
is AppRoute.AccessCodeRecovery,
is AppRoute.AppCurrencySelector,
is AppRoute.ModalNotification,
is AppRoute.SaveWallet,
is AppRoute.Send,
is AppRoute.AppSettings,
is AppRoute.CardSettings,
is AppRoute.DetailsSecurity,
is AppRoute.Home,
is AppRoute.OnboardingNote,
is AppRoute.OnboardingOther,
is AppRoute.OnboardingTwins,
is AppRoute.OnboardingWallet,
is AppRoute.QrScanning,
is AppRoute.ReferralProgram,
is AppRoute.ResetToFactory,
is AppRoute.Swap,
is AppRoute.Wallet,
is AppRoute.WalletConnectSessions,
is AppRoute.CurrencyDetails,
is AppRoute.Staking,
is AppRoute.PushNotification,
-> error("Unsupported route: $route")
}
// 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
@ -258,6 +413,7 @@ internal class ChildFactory @Inject constructor(
)
}
}
// endregion
}
fun doOnDestroy() {

View file

@ -7,7 +7,7 @@ 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.tap.routing.component.RoutingComponent.Child
import com.tangem.utils.Provider
internal fun AppRoute.asFragmentChild(fragmentProvider: Provider<Fragment>): Child {
@ -37,4 +37,14 @@ internal fun <C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C
}
return Child.LegacyFragment(path, fragmentProvider)
}
internal fun <C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>> createComponentChild(
contextProvider: Provider<AppComponentContext>,
params: P,
componentFactory: F,
): Child {
val context = contextProvider()
return Child.ComposableComponent(componentFactory.create(context, params))
}