diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 9ec82d584d..a67081bcb8 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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() { diff --git a/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt b/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt index 53228bf674..2d8f270f89 100644 --- a/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/SnackbarHandler.kt @@ -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() 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 index aca0f1678a..628d889d40 100644 --- a/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt +++ b/app/src/main/java/com/tangem/tap/di/routing/RoutingComponentModule.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt similarity index 59% rename from app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt rename to app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt index a7f5148b19..97fa1777e1 100644 --- a/app/src/main/java/com/tangem/tap/routing/RoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt @@ -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> + @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 { diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt new file mode 100644 index 0000000000..26209a001e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -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( + 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 + } +} \ 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 deleted file mode 100644 index 77283b4c75..0000000000 --- a/app/src/main/java/com/tangem/tap/routing/impl/DefaultRoutingComponent.kt +++ /dev/null @@ -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( - 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 - } -} \ 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 index 8cbd01ea2e..960a2bd499 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -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() { 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 index bdade79e4e..e7aa49f4d9 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt @@ -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): Child { @@ -37,4 +37,14 @@ internal fun > createComponentChild( + contextProvider: Provider, + params: P, + componentFactory: F, +): Child { + val context = contextProvider() + + return Child.ComposableComponent(componentFactory.create(context, params)) } \ No newline at end of file