Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-09 16:09:13 +04:00
commit e8af92b599
18 changed files with 108 additions and 89 deletions

View file

@ -8,7 +8,6 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.utils.Provider
import dagger.hilt.android.AndroidEntryPoint
@ -33,11 +32,6 @@ internal class DecomposeFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
component.Content(modifier)
EventMessageEffect(
messageHandler = uiDependencies.eventMessageHandler,
snackbarHostState = uiDependencies.globalSnackbarHostState,
)
}
private class ComponentBuilder<C : ComposableContentComponent, P : Any, F : ComponentFactory<P, C>>(

View file

@ -7,7 +7,6 @@ import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
@ -21,6 +20,7 @@ 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.lifecycle.Lifecycle
@ -39,10 +39,14 @@ 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.email.EmailSender
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.event.StateEvent
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.card.sdk.CardSdkOwner
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
@ -216,6 +220,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
internal lateinit var routingFeatureToggles: RoutingFeatureToggles
@Inject
internal lateinit var uiDependencies: UiDependencies
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
@ -267,6 +274,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
} else {
setContentView(R.layout.activity_main)
installRouting()
installEventMessageEffect()
}
initContent()
@ -283,6 +291,33 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lifecycle.addObserver(WindowObscurationObserver)
}
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() {
val routingComponent = routingComponentFactory.create(
context = rootComponentContext,
@ -557,7 +592,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
return if (result) super.dispatchTouchEvent(event) else false
}
private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) {
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 {
@ -568,12 +609,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
setTextColor(textColor)
if (buttonTitle != null && action != null) {
setAction(buttonTitle, action)
setAction(buttonTitle, { action() })
}
addCallback(
object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
onDismiss()
snackbar = null
removeCallback(this)
}

View file

@ -6,8 +6,9 @@ import com.arkivanov.decompose.defaultComponentContext
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.DefaultAppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.ui.UiDependencies
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -27,13 +28,13 @@ internal object RootAppComponentContextModule {
@ActivityContext context: Context,
dispatchers: CoroutineDispatcherProvider,
componentBuilder: DecomposeComponent.Builder,
uiDependencies: UiDependencies,
@GlobalUiMessageSender messageSender: UiMessageSender,
): AppComponentContext {
return DefaultAppComponentContext(
componentContext = (context as AppCompatActivity).defaultComponentContext(),
messageHandler = uiDependencies.eventMessageHandler,
dispatchers = dispatchers,
hiltComponentBuilder = componentBuilder,
messageSender = messageSender,
)
}
}

View file

@ -1,6 +1,9 @@
package com.tangem.tap.di
import androidx.compose.material3.SnackbarHostState
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.EventMessageHandler
@ -28,4 +31,11 @@ internal object UiDependenciesModule {
override val eventMessageHandler: EventMessageHandler = EventMessageHandler()
}
}
@Provides
@Singleton
@GlobalUiMessageSender
fun provideUiMessageSender(uiDependencies: UiDependencies): UiMessageSender {
return DefaultUiMessageSender(uiDependencies.eventMessageHandler)
}
}

View file

@ -37,7 +37,6 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.setStatusBarColor
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.datasource.utils.isNullOrEmpty
@ -301,10 +300,6 @@ class OnboardingWalletFragment :
windowSize = rememberWindowSize(activity = requireActivity()),
) {
onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize())
EventMessageEffect(
messageHandler = uiDependencies.eventMessageHandler,
snackbarHostState = uiDependencies.globalSnackbarHostState,
)
}
}
}

View file

@ -6,4 +6,10 @@
android:background="@color/background_primary"
android:filterTouchesWhenObscured="true">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/compose_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -7,8 +7,6 @@ import com.tangem.core.decompose.navigation.AppNavigationProvider
import com.tangem.core.decompose.navigation.DefaultAppNavigationProvider
import com.tangem.core.decompose.navigation.DefaultRouter
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.DefaultUiMessageSender
import com.tangem.core.decompose.ui.UiMessageHandler
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.decompose.utils.ComponentCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -16,9 +14,9 @@ import kotlinx.coroutines.CoroutineScope
class DefaultAppComponentContext(
componentContext: ComponentContext,
messageHandler: UiMessageHandler,
override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: DecomposeComponent.Builder,
override val messageSender: UiMessageSender,
private val replaceRouter: Router? = null,
) : AppComponentContext, ComponentContext by componentContext {
@ -26,8 +24,6 @@ class DefaultAppComponentContext(
override val componentScope: CoroutineScope = ComponentCoroutineScope(lifecycle, dispatchers)
override val messageSender: UiMessageSender = DefaultUiMessageSender(messageHandler)
override val navigationProvider: AppNavigationProvider
get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() }

View file

@ -3,4 +3,7 @@ package com.tangem.core.decompose.di
import javax.inject.Qualifier
@Qualifier
annotation class RootAppComponentContext
annotation class RootAppComponentContext
@Qualifier
annotation class GlobalUiMessageSender

View file

@ -1,6 +1,6 @@
package com.tangem.core.decompose.ui
internal class DefaultUiMessageSender(
class DefaultUiMessageSender(
private val handler: UiMessageHandler,
) : UiMessageSender {

View file

@ -9,15 +9,18 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.TangemThemePreview
/**
* Tangem snackbar host.
* Based on Material3 component. It's best way to show [TangemSnackbar].
* Based on Material3 component. It's the best way to show [TangemSnackbar].
*
* @param hostState snackbar host state
* @param modifier modifier
* @param actionOnNewLine flag that indicates if the action should be displayed on a new line (default: false)
* @param modifier Modifier to be applied to the layout.
* @param hostState SnackbarHostState that will be used to show snackbar.
* Optional parameter. By default, it uses [LocalSnackbarHostState.current].
* @param actionOnNewLine Flag that indicates if the action should be displayed on a new line.
* Optional parameter, `false` by default.
*
* @see <a href = https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=682-761&t=kDzSZDx0m0sk4iYz-4
* >Figma</a>
@ -25,7 +28,11 @@ import com.tangem.core.ui.res.TangemThemePreview
[REDACTED_AUTHOR]
*/
@Composable
fun TangemSnackbarHost(hostState: SnackbarHostState, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false) {
fun TangemSnackbarHost(
modifier: Modifier = Modifier,
hostState: SnackbarHostState = LocalSnackbarHostState.current,
actionOnNewLine: Boolean = false,
) {
SnackbarHost(hostState = hostState, modifier = modifier) { data ->
TangemSnackbar(data = data, actionOnNewLine = actionOnNewLine)
}

View file

@ -22,10 +22,8 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.block.BlockItem
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
@ -48,12 +46,6 @@ internal fun DetailsScreen(
Scaffold(
modifier = modifier,
containerColor = backgroundColor,
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
hostState = LocalSnackbarHostState.current,
)
},
topBar = {
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),

View file

@ -50,14 +50,12 @@ import com.tangem.core.ui.components.rows.ChainRow
import com.tangem.core.ui.components.rows.ChainRowContainer
import com.tangem.core.ui.components.rows.model.BlockchainRowUM
import com.tangem.core.ui.components.rows.model.ChainRowUM
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
@ -100,12 +98,6 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi
state = state,
)
},
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
hostState = LocalSnackbarHostState.current,
)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
if (state is ManageTokensUM.ManageContent) {

View file

@ -17,10 +17,8 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.fields.SearchBar
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
@ -53,14 +51,6 @@ internal fun OnboardingManageTokensContent(state: OnboardingManageTokensUM, modi
state = state,
)
},
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier
.padding(all = TangemTheme.dimens.spacing16)
.navigationBarsPadding(),
hostState = LocalSnackbarHostState.current,
)
},
floatingActionButtonPosition = FabPosition.Center,
floatingActionButton = {
FloatingActionButton(

View file

@ -1,23 +1,26 @@
package com.tangem.features.markets.entry.impl
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pushNew
import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.domain.markets.toSerializableParam
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.domain.markets.toSerializableParam
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent
import dagger.assisted.Assisted
@ -28,7 +31,6 @@ import dagger.assisted.AssistedInject
internal class DefaultMarketsEntryComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
private val marketsEntryChildFactory: MarketsEntryChildFactory,
private val uiDependencies: UiDependencies,
) : MarketsEntryComponent, AppComponentContext by context {
private val stackNavigation = StackNavigation<Child>()
@ -64,10 +66,6 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
stackState = stack.subscribeAsState(),
modifier = modifier,
)
EventMessageEffect(
messageHandler = uiDependencies.eventMessageHandler,
snackbarHostState = uiDependencies.globalSnackbarHostState,
)
}
@OptIn(ExperimentalDecomposeApi::class)

View file

@ -3,15 +3,17 @@ package com.tangem.feature.tokendetails.presentation
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.defaultComponentContext
import androidx.fragment.app.viewModels
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.routing.utils.asRouter
import com.tangem.core.decompose.context.DefaultAppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.screen.ComposeFragment
@ -32,7 +34,11 @@ internal class TokenDetailsFragment : ComposeFragment() {
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var tokenDetailsRouter: TokenDetailsRouter
@GlobalUiMessageSender
internal lateinit var messageSender: UiMessageSender
@Inject
internal lateinit var tokenDetailsRouter: TokenDetailsRouter
@Inject
internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider
@ -70,7 +76,7 @@ internal class TokenDetailsFragment : ComposeFragment() {
val appContext = DefaultAppComponentContext(
componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher),
messageHandler = uiDependencies.eventMessageHandler,
messageSender = messageSender,
dispatchers = coroutineDispatcherProvider,
hiltComponentBuilder = componentBuilder,
replaceRouter = appRouter.asRouter(),

View file

@ -32,7 +32,6 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.OkxPromoNotification
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.event.EventEffect
@ -65,16 +64,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = bottomBarHeight + TangemTheme.dimens.spacing16,
),
hostState = snackbarHostState,
)
},
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->

View file

@ -18,10 +18,8 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.BlockItem
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
@ -43,12 +41,6 @@ internal fun WalletSettingsScreen(
Scaffold(
modifier = modifier,
containerColor = backgroundColor,
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
hostState = LocalSnackbarHostState.current,
)
},
topBar = {
TangemTopAppBar(
modifier = Modifier.statusBarsPadding(),

View file

@ -9,6 +9,8 @@ import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.utils.asRouter
import com.tangem.core.decompose.context.DefaultAppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.di.GlobalUiMessageSender
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.wallet.presentation.router.InnerWalletRouter
@ -45,6 +47,10 @@ internal class WalletFragment : ComposeFragment() {
@Inject
internal lateinit var appRouter: AppRouter
@Inject
@GlobalUiMessageSender
internal lateinit var messageSender: UiMessageSender
private lateinit var marketsEntryComponent: MarketsEntryComponent
private val _walletRouter: InnerWalletRouter
@ -57,7 +63,7 @@ internal class WalletFragment : ComposeFragment() {
val appContext = DefaultAppComponentContext(
componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher),
messageHandler = uiDependencies.eventMessageHandler,
messageSender = messageSender,
dispatchers = coroutineDispatcherProvider,
hiltComponentBuilder = componentBuilder,
replaceRouter = appRouter.asRouter(),