Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-03 14:09:13 +04:00
parent 7699ee0de7
commit fd128f3bd7
17 changed files with 215 additions and 19 deletions

View file

@ -10,7 +10,9 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.network.exchangeServices.ExchangeService
import org.rekotlin.Action
@ -54,4 +56,8 @@ class AppStateHolder @Inject constructor() : ReduxNavController, ReduxStateHolde
override suspend fun onUserWalletSelected(userWallet: UserWallet) {
mainStore?.onUserWalletSelected(userWallet)
}
override fun sendFeedbackEmail() {
mainStore?.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
}
}

View file

@ -10,4 +10,6 @@ interface ReduxStateHolder {
suspend fun dispatchWithMain(action: Action)
suspend fun onUserWalletSelected(userWallet: UserWallet)
fun sendFeedbackEmail()
}

View file

@ -5,4 +5,9 @@ import androidx.fragment.app.Fragment
interface DetailsEntryPoint {
fun entryFragment(): Fragment
companion object {
const val USER_WALLET_ID_KEY = "user_wallet_id"
}
}

View file

@ -15,14 +15,18 @@ dependencies {
/* Project - API */
implementation(projects.features.details.api)
implementation(projects.features.tester.api)
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
implementation(projects.core.navigation)
implementation(projects.core.analytics.models)
/* Project - Domain */
implementation(projects.domain.wallets.models)
implementation(projects.domain.legacy)
/* AndroidX */
implementation(deps.androidx.fragment.ktx)

View file

@ -3,8 +3,13 @@ package com.tangem.features.details
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.Fragment
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.di.RootAppComponentContext
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.message.EventMessageEffect
import com.tangem.core.ui.message.EventMessageHandler
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.component.preview.PreviewDetailsComponent
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@ -16,10 +21,52 @@ internal class DetailsFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
// @Inject
// internal lateinit var componentFactory: DetailsComponent.Factory
@Inject
internal lateinit var detailsRouter: DetailsRouter
@Inject
@RootAppComponentContext
internal lateinit var rootContext: AppComponentContext
private val component: DetailsComponent by lazy { initComponent() }
private val messageHandler = EventMessageHandler()
@Composable
override fun ScreenContent(modifier: Modifier) {
// TODO: Update to use the actual component in [REDACTED_TASK_KEY]
PreviewDetailsComponent().View(modifier = modifier)
component.View(modifier = modifier)
EventMessageEffect(
messageHandler = messageHandler,
snackbarHostState = component.snackbarHostState,
)
}
private fun initComponent(): DetailsComponent {
// TODO: Uncomment in [REDACTED_JIRA]
// val selectedUserWalletId = arguments?.getString(DetailsEntryPoint.USER_WALLET_ID_KEY)
// ?.let(::UserWalletId)
//
//
// requireNotNull(selectedUserWalletId) { "UserWalletId must be provided" }
//
// val context = rootContext.childByContext(
// componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher),
// messageHandler = messageHandler,
// router = detailsRouter,
// )
//
// return componentFactory.create(
// context = context,
// params = DetailsComponent.Params(
// selectedUserWalletId = selectedUserWalletId,
// ),
// )
return PreviewDetailsComponent()
}
companion object : DetailsEntryPoint {

View file

@ -0,0 +1,55 @@
package com.tangem.features.details
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.features.details.routing.DetailsRoute
import com.tangem.features.tester.api.TesterRouter
import javax.inject.Inject
// TODO: Remove after [REDACTED_JIRA]
internal class DetailsRouter @Inject constructor(
private val reduxNavController: ReduxNavController,
private val reduxStateHolder: ReduxStateHolder,
private val testerRouter: TesterRouter,
) : Router {
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
if (route is DetailsRoute) {
when (route) {
is DetailsRoute.Screen -> {
reduxNavController.navigate(NavigationAction.NavigateTo(route.screen, bundle = route.params))
}
is DetailsRoute.Feedback -> {
reduxStateHolder.sendFeedbackEmail()
}
is DetailsRoute.TesterMenu -> {
testerRouter.startTesterScreen()
}
is DetailsRoute.Url -> {
reduxNavController.navigate(NavigationAction.OpenUrl(route.url))
}
}
onComplete(true)
} else {
onComplete(false)
}
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
reduxNavController.popBackStack()
onComplete(true)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
if (route is DetailsRoute.Screen) {
reduxNavController.popBackStack(route.screen)
onComplete(true)
} else {
reduxNavController.getBackStack()
onComplete(false)
}
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.details.component
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
@ -7,6 +8,8 @@ import com.tangem.domain.wallets.models.UserWalletId
interface DetailsComponent {
val snackbarHostState: SnackbarHostState
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
fun View(modifier: Modifier)

View file

@ -1,5 +1,6 @@
package com.tangem.features.details.component.preview
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.features.details.component.DetailsComponent
@ -12,15 +13,18 @@ import kotlinx.coroutines.runBlocking
internal class PreviewDetailsComponent : DetailsComponent {
override val snackbarHostState: SnackbarHostState = SnackbarHostState()
private val previewBlocks = runBlocking {
ItemsBuilder(
walletConnectComponent = PreviewWalletConnectComponent(),
userWalletListComponent = PreviewUserWalletListComponent(),
router = PreviewRouter(),
).buldAll()
}
private val previewFooter = DetailsFooterUM(
socials = SocialsBuilder(urlOpener = { /* no-op */ }).buildAll(),
socials = SocialsBuilder(PreviewRouter()).buildAll(),
appVersion = "1.0.0-preview",
)
@ -33,6 +37,10 @@ internal class PreviewDetailsComponent : DetailsComponent {
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
override fun View(modifier: Modifier) {
DetailsScreen(state = previewState, modifier = modifier)
DetailsScreen(
modifier = modifier,
state = previewState,
snackbarHostState = snackbarHostState,
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.details.component.preview
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
internal class PreviewRouter : Router {
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
/* no-op */
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
/* no-op */
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
/* no-op */
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.details.routing
import android.os.Bundle
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.navigation.AppScreen
// TODO: Remove after [REDACTED_JIRA]
internal sealed class DetailsRoute : Route {
data class Screen(
val screen: AppScreen,
val params: Bundle? = null,
) : DetailsRoute()
data class Url(val url: String) : DetailsRoute()
data object Feedback : DetailsRoute()
data object TesterMenu : DetailsRoute()
}

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.details.component.preview.PreviewDetailsComponent
@ -31,7 +32,7 @@ private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun DetailsScreen(state: DetailsUM, modifier: Modifier = Modifier) {
internal fun DetailsScreen(state: DetailsUM, snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) {
val backgroundColor = TangemTheme.colors.background.secondary
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
@ -44,6 +45,12 @@ internal fun DetailsScreen(state: DetailsUM, modifier: Modifier = Modifier) {
Scaffold(
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = backgroundColor,
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
hostState = snackbarHostState,
)
},
topBar = { TopBar(state, scrollBehavior) },
) { paddingValues ->
Content(

View file

@ -1,5 +1,7 @@
package com.tangem.features.details.utils
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.AppScreen
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.details.component.UserWalletListComponent
@ -7,6 +9,7 @@ import com.tangem.features.details.component.WalletConnectComponent
import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.impl.BuildConfig
import com.tangem.features.details.impl.R
import com.tangem.features.details.routing.DetailsRoute
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -14,6 +17,7 @@ import kotlinx.collections.immutable.toImmutableList
internal class ItemsBuilder(
private val walletConnectComponent: WalletConnectComponent,
private val userWalletListComponent: UserWalletListComponent,
private val router: Router,
) {
suspend fun buldAll(): ImmutableList<DetailsItemUM> = buildList {
@ -37,7 +41,7 @@ internal class ItemsBuilder(
}
}
fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.Component(
private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.Component(
id = "user_wallet_list",
content = {
userWalletListComponent.View(modifier = it)
@ -51,7 +55,7 @@ internal class ItemsBuilder(
id = "buy_tangem_wallet",
title = stringReference("Buy Tangem Wallet"), // TODO: Move to resources in [REDACTED_TASK_KEY]
iconRes = R.drawable.ic_tangem_24,
onClick = { /* [REDACTED_TODO_COMMENT] */ },
onClick = { router.push(DetailsRoute.Url(BUY_TANGEM_URL)) },
),
),
)
@ -63,7 +67,7 @@ internal class ItemsBuilder(
id = "app_settings",
title = resourceReference(R.string.app_settings_title),
iconRes = R.drawable.ic_settings_24,
onClick = { /* [REDACTED_TODO_COMMENT] */ },
onClick = { router.push(DetailsRoute.Screen(AppScreen.AppSettings)) },
).let(::add)
if (BuildConfig.TESTER_MENU_ENABLED) {
@ -71,7 +75,7 @@ internal class ItemsBuilder(
id = "tester_menu",
title = stringReference(value = "Tester menu"),
iconRes = R.drawable.ic_alert_24,
onClick = { /* [REDACTED_TODO_COMMENT] */ },
onClick = { router.push(DetailsRoute.TesterMenu) },
).let(::add)
}
}.toImmutableList(),
@ -84,14 +88,18 @@ internal class ItemsBuilder(
id = "send_feedback",
title = stringReference("Send feedback"), // TODO: Move to resources in [REDACTED_TASK_KEY]
iconRes = R.drawable.ic_comment_24,
onClick = { /* [REDACTED_TODO_COMMENT] */ },
onClick = { router.push(DetailsRoute.Feedback) },
),
DetailsItemUM.Basic.Item(
id = "disclaimer",
title = resourceReference(R.string.disclaimer_title),
iconRes = R.drawable.ic_text_24,
onClick = { /* [REDACTED_TODO_COMMENT] */ },
onClick = { router.push(DetailsRoute.Screen(AppScreen.Disclaimer)) },
),
),
)
private companion object {
const val BUY_TANGEM_URL = "https://buy.tangem.com/"
}
}

View file

@ -1,13 +1,15 @@
package com.tangem.features.details.utils
import androidx.compose.ui.text.intl.Locale
import com.tangem.core.decompose.navigation.Router
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.routing.DetailsRoute
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
internal class SocialsBuilder(
private val urlOpener: (url: String) -> Unit,
private val router: Router,
) {
fun buildAll(): ImmutableList<DetailsFooterUM.Social> = Social.all.map { social ->
@ -21,11 +23,13 @@ internal class SocialsBuilder(
private fun openUrl(social: Social) {
val locale = Locale.current.region
if (locale == RUSSIA_LOCALE && social.urlRu != null) {
urlOpener(social.urlRu)
val url = if (locale == RUSSIA_LOCALE && social.urlRu != null) {
social.urlRu
} else {
urlOpener(social.url)
social.url
}
router.push(DetailsRoute.Url(url))
}
private enum class Social(

View file

@ -87,4 +87,5 @@ dependencies {
implementation(projects.features.send.api)
implementation(projects.features.tester.api)
implementation(projects.features.manageTokens.api)
implementation(projects.features.details.api)
}

View file

@ -28,6 +28,7 @@ import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScree
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel
import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen
import com.tangem.feature.wallet.presentation.wallet.viewmodels.WalletViewModel
import com.tangem.features.details.DetailsEntryPoint
import com.tangem.features.managetokens.navigation.ExpandableState
import com.tangem.features.managetokens.navigation.ManageTokensUi
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
@ -116,8 +117,15 @@ internal class DefaultWalletRouter(
navController.navigate(WalletRoute.OrganizeTokens.createRoute(userWalletId))
}
override fun openDetailsScreen() {
reduxNavController.navigate(action = NavigationAction.NavigateTo(AppScreen.Details))
override fun openDetailsScreen(selectedWalletId: UserWalletId) {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.Details,
bundle = bundleOf(
DetailsEntryPoint.USER_WALLET_ID_KEY to selectedWalletId.stringValue,
),
),
)
}
override fun openOnboardingScreen() {

View file

@ -35,7 +35,7 @@ internal interface InnerWalletRouter : WalletRouter {
fun openOrganizeTokensScreen(userWalletId: UserWalletId)
/** Open details screen */
fun openDetailsScreen()
fun openDetailsScreen(selectedWalletId: UserWalletId)
/** Open onboarding screen */
fun openOnboardingScreen()

View file

@ -82,7 +82,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
),
)
} else {
router.openDetailsScreen()
router.openDetailsScreen(stateHolder.getSelectedWalletId())
}
}
}