diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 45dae620de..6f45b6b45b 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -2,6 +2,7 @@ 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 @@ -116,4 +117,6 @@ interface ApplicationEntryPoint { fun getUrlOpener(): UrlOpener fun getShareManager(): ShareManager + + fun getAppRouter(): AppRouter } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 70192d4bb6..dfd4c4cc91 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -27,9 +27,13 @@ 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.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 @@ -60,6 +64,7 @@ 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.showFragmentAllowingStateLoss import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor @@ -72,6 +77,8 @@ 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.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 @@ -162,6 +169,16 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @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 + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -188,6 +205,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac observeAppThemeModeUpdates() setContentView(R.layout.activity_main) + installRouting() initContent() checkForNotificationPermission() @@ -199,6 +217,29 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } + private fun installRouting() { + val routingComponent = routingComponentFactory.create( + context = rootComponentContext, + ) + + appRouterConfig.routerScope = lifecycleScope + appRouterConfig.componentRouter = routingComponent.router + + routingComponent.stack.observe(lifecycle.asEssentyLifecycle()) { childStack -> + appRouterConfig.baclStack = childStack.backStack.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) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 414c48552b..5ecb0622ca 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 @@ -193,6 +194,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val shareManager get() = entryPoint.getShareManager() + + private val appRouter: AppRouter + get() = entryPoint.getAppRouter() // endregion override fun onCreate() { @@ -284,6 +288,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { detailsEntryPoint = detailsEntryPoint, urlOpener = urlOpener, shareManager = shareManager, + appRouter = appRouter, ), ), ) 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 70631857fc..8282d27be6 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 @@ -28,9 +28,53 @@ 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 com.tangem.utils.Provider import com.tangem.wallet.R import timber.log.Timber +fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider) { + 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 { + 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 Fragment.showFragment(fragmentManager: FragmentManager, name: String) { + val transaction = fragmentManager.beginTransaction() + + transaction.replace(R.id.fragment_container, this, name) + transaction.addToBackStack(name) + + transaction.commitAllowingStateLoss() +} + fun FragmentActivity.openFragment( screen: AppScreen, addToBackstack: Boolean, 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/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 08d6c0997f..08744d3939 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,6 +2,7 @@ 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 @@ -85,4 +86,5 @@ data class DaggerGraphState( val stakingRouter: StakingRouter? = null, val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, + val appRouter: AppRouter? = 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/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/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index 980934bf31..ae4552a1f0 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -17,9 +17,10 @@ dependencies { implementation(projects.domain.qrScanning.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.staking.models) /* Libs - Other */ + api(deps.kotlin.serialization) implementation(deps.androidx.core.ktx) - implementation(deps.kotlin.serialization) implementation(deps.timber) } \ 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 index 9e81df0ec5..cb0f21a793 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRouter.kt @@ -15,7 +15,7 @@ interface AppRouter { /** * The current navigation stack. */ - val backStack: List + val stack: List /** * Pushes a new route to the navigation stack. @@ -25,6 +25,14 @@ interface AppRouter { */ 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. * @@ -33,7 +41,15 @@ interface AppRouter { fun pop(onComplete: (isSuccess: Boolean) -> Unit = {}) /** - * Pops routes from the navigation stack until the specified route class is found. + * 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.