Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-24 10:50:03 +01:00
commit bc84d8f5f8
579 changed files with 13604 additions and 3422 deletions

View file

@ -15,7 +15,5 @@ dependencies {
/* Project - Core */
implementation(projects.core.decompose)
/* AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(projects.core.ui)
}

View file

@ -1,13 +0,0 @@
package com.tangem.features.details
import androidx.fragment.app.Fragment
interface DetailsEntryPoint {
fun entryFragment(): Fragment
companion object {
const val USER_WALLET_ID_KEY = "user_wallet_id"
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.details.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
interface DetailsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, DetailsComponent>
data class Params(
val userWalletId: UserWalletId,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.details.component
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
interface UserWalletListComponent : ComposableContentComponent {
interface Factory {
fun create(context: AppComponentContext): UserWalletListComponent
}
}

View file

@ -15,6 +15,7 @@ dependencies {
/* Project - API */
implementation(projects.features.details.api)
implementation(projects.features.disclaimer.api)
implementation(projects.features.tester.api)
/* Project - Core */
@ -23,11 +24,24 @@ dependencies {
implementation(projects.core.featuretoggles)
implementation(projects.core.navigation)
implementation(projects.core.analytics.models)
implementation(projects.common.routing)
/* Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.card)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.walletConnect)
implementation(projects.domain.legacy)
/* SDK */
// TODO: For TangemError model, should be removed after card domain scanning refactoring
implementation(deps.tangem.card.core)
/* AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.activity.compose)
@ -47,4 +61,5 @@ dependencies {
/* Other */
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
}

View file

@ -1,76 +0,0 @@
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
// TODO: Remove after [REDACTED_JIRA]
@AndroidEntryPoint
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) {
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 {
override fun entryFragment(): Fragment = DetailsFragment()
}
}

View file

@ -1,55 +0,0 @@
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,25 +0,0 @@
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
import com.tangem.domain.wallets.models.UserWalletId
interface DetailsComponent {
val snackbarHostState: SnackbarHostState
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
fun View(modifier: Modifier)
interface Factory {
fun create(context: AppComponentContext, params: Params): DetailsComponent
}
data class Params(
val selectedUserWalletId: UserWalletId,
)
}

View file

@ -1,16 +0,0 @@
package com.tangem.features.details.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
interface UserWalletListComponent {
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
fun View(modifier: Modifier)
interface Factory {
fun create(context: AppComponentContext): UserWalletListComponent
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.features.details.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.wallets.models.UserWalletId
interface WalletConnectComponent {
suspend fun checkIsAvailable(): Boolean
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
fun View(modifier: Modifier)
interface Factory {
fun create(context: AppComponentContext, params: Params): WalletConnectComponent
}
data class Params(
val userWalletId: UserWalletId,
)
}

View file

@ -0,0 +1,45 @@
package com.tangem.features.details.component.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.model.DetailsModel
import com.tangem.features.details.ui.DetailsScreen
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultDetailsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: DetailsComponent.Params,
userWalletListComponentFactory: UserWalletListComponent.Factory,
) : DetailsComponent, AppComponentContext by context {
private val model: DetailsModel = getOrCreateModel(params)
private val userWalletListComponent = userWalletListComponentFactory.create(
context = child(key = "user_wallet_list"),
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
DetailsScreen(
modifier = modifier,
state = state,
userWalletListBlockContent = userWalletListComponent,
)
}
@AssistedFactory
interface Factory : DetailsComponent.Factory {
override fun create(context: AppComponentContext, params: DetailsComponent.Params): DefaultDetailsComponent
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.details.component.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.model.UserWalletListModel
import com.tangem.features.details.ui.UserWalletListBlock
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultUserWalletListComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
) : UserWalletListComponent, AppComponentContext by context {
private val model: UserWalletListModel = getOrCreateModel()
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
UserWalletListBlock(
modifier = modifier,
state = state,
)
}
@AssistedFactory
interface Factory : UserWalletListComponent.Factory {
override fun create(context: AppComponentContext): DefaultUserWalletListComponent
}
}

View file

@ -1,8 +1,10 @@
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.core.decompose.navigation.DummyRouter
import com.tangem.core.navigation.feedback.DummyFeedbackManager
import com.tangem.core.navigation.url.DummyUrlOpener
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsUM
@ -13,18 +15,16 @@ 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()
router = DummyRouter(),
urlOpener = DummyUrlOpener(),
feedbackManager = DummyFeedbackManager(),
).buldAll(isWalletConnectAvailable = true)
}
private val previewFooter = DetailsFooterUM(
socials = SocialsBuilder(PreviewRouter()).buildAll(),
socials = SocialsBuilder(DummyUrlOpener()).buildAll(),
appVersion = "1.0.0-preview",
)
@ -36,11 +36,11 @@ internal class PreviewDetailsComponent : DetailsComponent {
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
override fun View(modifier: Modifier) {
override fun Content(modifier: Modifier) {
DetailsScreen(
modifier = modifier,
state = previewState,
snackbarHostState = snackbarHostState,
userWalletListBlockContent = PreviewUserWalletListComponent(),
)
}
}

View file

@ -1,18 +0,0 @@
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

@ -46,7 +46,7 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent {
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
override fun View(modifier: Modifier) {
override fun Content(modifier: Modifier) {
UserWalletListBlock(state = previewState, modifier = modifier)
}

View file

@ -1,17 +0,0 @@
package com.tangem.features.details.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.features.details.component.WalletConnectComponent
import com.tangem.features.details.ui.WalletConnectBlock
internal class PreviewWalletConnectComponent : WalletConnectComponent {
override suspend fun checkIsAvailable(): Boolean = true
@Composable
@Suppress("TopLevelComposableFunctions") // TODO: Remove this check
override fun View(modifier: Modifier) {
WalletConnectBlock(onClick = { /* no-op */ }, modifier = modifier)
}
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.details.di
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.component.impl.DefaultDetailsComponent
import com.tangem.features.details.component.impl.DefaultUserWalletListComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModule {
@Binds
@Singleton
fun bindDetailsComponentFactory(factory: DefaultDetailsComponent.Factory): DetailsComponent.Factory
@Binds
@Singleton
fun bindUserWalletListComponentFactory(
factory: DefaultUserWalletListComponent.Factory,
): UserWalletListComponent.Factory
}

View file

@ -2,9 +2,7 @@ package com.tangem.features.details.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.details.DefaultDetailsFeatureToggles
import com.tangem.features.details.DetailsEntryPoint
import com.tangem.features.details.DetailsFeatureToggles
import com.tangem.features.details.DetailsFragment
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -20,10 +18,4 @@ internal object FeatureModule {
fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): DetailsFeatureToggles {
return DefaultDetailsFeatureToggles(featureTogglesManager)
}
@Provides
@Singleton
fun provideEntryPoint(): DetailsEntryPoint {
return DetailsFragment.Companion
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.details.di
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.details.model.DetailsModel
import com.tangem.features.details.model.UserWalletListModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -17,4 +18,9 @@ internal interface ModelModule {
@IntoMap
@ClassKey(DetailsModel::class)
fun provideDetailsModel(model: DetailsModel): Model
@Binds
@IntoMap
@ClassKey(UserWalletListModel::class)
fun provideUserWalletListModel(model: UserWalletListModel): Model
}

View file

@ -1,8 +1,10 @@
package com.tangem.features.details.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class DetailsFooterUM(
val appVersion: String,
val socials: ImmutableList<Social>,

View file

@ -1,10 +1,7 @@
package com.tangem.features.details.entity
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.block.model.BlockUM
import kotlinx.collections.immutable.ImmutableList
@Immutable
@ -19,23 +16,15 @@ internal sealed class DetailsItemUM {
data class Item(
val id: String,
val title: TextReference,
@DrawableRes
val iconRes: Int,
val onClick: () -> Unit,
val block: BlockUM,
)
}
data class Component(
override val id: String,
val content: Content,
) : DetailsItemUM() {
data class WalletConnect(val onClick: () -> Unit) : DetailsItemUM() {
override val id: String = "wallet_connect"
}
fun interface Content {
@Composable
@Suppress("TopLevelComposableFunctions", "ComposableFunctionName")
operator fun invoke(modifier: Modifier)
}
data object UserWalletList : DetailsItemUM() {
override val id: String = "user_wallet_list"
}
}

View file

@ -1,10 +1,81 @@
package com.tangem.features.details.model
import arrow.core.getOrElse
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.entity.DetailsUM
import com.tangem.features.details.utils.ItemsBuilder
import com.tangem.features.details.utils.SocialsBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.version.AppVersionProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
// TODO: Will be implemented later
@ComponentScoped
@Suppress("LongParameterList")
internal class DetailsModel @Inject constructor(
private val socialsBuilder: SocialsBuilder,
private val itemsBuilder: ItemsBuilder,
private val appVersionProvider: AppVersionProvider,
private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase,
private val router: Router,
private val paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model()
) : Model() {
private val params: DetailsComponent.Params = paramsContainer.require()
private val items: MutableStateFlow<ImmutableList<DetailsItemUM>> = MutableStateFlow(value = persistentListOf())
val state: MutableStateFlow<DetailsUM> = MutableStateFlow(
value = DetailsUM(
items = items.value,
footer = DetailsFooterUM(
socials = socialsBuilder.buildAll(),
appVersion = getAppVersion(),
),
popBack = router::pop,
),
)
init {
items
.onEach(::updateState)
.launchIn(modelScope)
checkWalletConnectAvailability()
}
private fun checkWalletConnectAvailability() = modelScope.launch {
val isWalletConnectAvailable = checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse {
Timber.w("Unable to check WalletConnect availability: $it")
false
}
items.value = itemsBuilder.buldAll(isWalletConnectAvailable)
}
private suspend fun updateState(items: ImmutableList<DetailsItemUM>) {
state.update { prevState ->
prevState.copy(
items = items,
)
}
}
private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})"
}

View file

@ -0,0 +1,74 @@
package com.tangem.features.details.model
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.utils.UserWalletSaver
import com.tangem.features.details.utils.UserWalletsFetcher
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ComponentScoped
internal class UserWalletListModel @Inject constructor(
private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
private val userWalletSaver: UserWalletSaver,
private val userWalletsFetcher: UserWalletsFetcher,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
private val userWallets: SharedFlow<ImmutableList<UserWalletUM>> = userWalletsFetcher
.userWallets
.share()
private val shouldSaveUserWallets: SharedFlow<Boolean> = shouldSaveUserWalletsUseCase()
.distinctUntilChanged()
.share()
val state: MutableStateFlow<UserWalletListUM> = MutableStateFlow(
value = UserWalletListUM(
userWallets = persistentListOf(),
isWalletSavingInProgress = false,
addNewWalletText = TextReference.EMPTY,
onAddNewWalletClick = ::addUserWallet,
),
)
init {
combine(
userWallets,
shouldSaveUserWallets,
isWalletSavingInProgress,
transform = ::updateState,
).launchIn(modelScope)
}
private suspend fun updateState(
userWallets: ImmutableList<UserWalletUM>,
shouldSaveUserWallets: Boolean,
isWalletSavingInProgress: Boolean,
) = state.update { value ->
value.copy(
userWallets = userWallets,
isWalletSavingInProgress = isWalletSavingInProgress,
addNewWalletText = if (shouldSaveUserWallets) {
resourceReference(R.string.user_wallet_list_add_button)
} else {
resourceReference(R.string.scan_card_settings_button)
},
)
}
private fun addUserWallet() = withProgress(isWalletSavingInProgress) {
userWalletSaver.scanAndSaveUserWallet()
}
}

View file

@ -1,20 +0,0 @@
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

@ -1,36 +0,0 @@
package com.tangem.features.details.ui
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.Card
import androidx.compose.material3.CardColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun BlockCard(
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit = {},
content: @Composable ColumnScope.() -> Unit = {},
) {
Card(
modifier = modifier,
onClick = onClick,
shape = TangemTheme.shapes.roundedCornersXMedium,
colors = BlockColors,
enabled = enabled,
content = content,
)
}
private val BlockColors: CardColors
@Composable
@ReadOnlyComposable
get() = CardColors(
containerColor = TangemTheme.colors.background.primary,
contentColor = TangemTheme.colors.text.primary1,
disabledContainerColor = TangemTheme.colors.button.disabled,
disabledContentColor = TangemTheme.colors.text.disabled,
)

View file

@ -1,45 +0,0 @@
package com.tangem.features.details.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.details.entity.DetailsItemUM
@Composable
internal fun BlockItem(model: DetailsItemUM.Basic.Item, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
onClick = model.onClick,
) {
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12, Alignment.Start),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = model.iconRes),
tint = TangemTheme.colors.icon.secondary,
contentDescription = null,
)
Text(
text = model.title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}

View file

@ -15,11 +15,13 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
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.appbar.models.TopAppBarMedium
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.resourceReference
import com.tangem.core.ui.res.LocalSnackbarHostState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.details.component.preview.PreviewDetailsComponent
@ -28,18 +30,16 @@ import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.entity.DetailsUM
import com.tangem.features.details.impl.R
private const val COLLAPSED_APP_BAR_THRESHOLD = 0.4f
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun DetailsScreen(state: DetailsUM, snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier) {
internal fun DetailsScreen(
state: DetailsUM,
userWalletListBlockContent: ComposableContentComponent,
modifier: Modifier = Modifier,
) {
val backgroundColor = TangemTheme.colors.background.secondary
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
SystemBarsEffect {
setSystemBarsColor(backgroundColor)
}
BackHandler(onBack = state.popBack)
Scaffold(
@ -48,68 +48,31 @@ internal fun DetailsScreen(state: DetailsUM, snackbarHostState: SnackbarHostStat
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
hostState = snackbarHostState,
hostState = LocalSnackbarHostState.current,
)
},
topBar = {
TopAppBarMedium(
title = resourceReference(R.string.details_title),
scrollBehavior = scrollBehavior,
onBackClick = state.popBack,
)
},
topBar = { TopBar(state, scrollBehavior) },
) { paddingValues ->
Content(
modifier = Modifier.padding(paddingValues),
state = state,
userWalletListBlockContent = userWalletListBlockContent,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(state: DetailsUM, scrollBehavior: TopAppBarScrollBehavior, modifier: Modifier = Modifier) {
MediumTopAppBar(
modifier = modifier,
scrollBehavior = scrollBehavior,
colors = TopAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
scrolledContainerColor = TangemTheme.colors.background.secondary,
navigationIconContentColor = TangemTheme.colors.icon.primary1,
titleContentColor = TangemTheme.colors.text.primary1,
actionIconContentColor = TangemTheme.colors.icon.primary1,
),
title = {
val collapsedStyle = TangemTheme.typography.subtitle1
val expandedStyle = TangemTheme.typography.h1
val style by remember(scrollBehavior.state.collapsedFraction) {
derivedStateOf {
if (scrollBehavior.state.collapsedFraction >= COLLAPSED_APP_BAR_THRESHOLD) {
collapsedStyle
} else {
expandedStyle
}
}
}
Text(
text = stringResource(id = R.string.details_title),
style = style,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
IconButton(
modifier = Modifier.size(TangemTheme.dimens.size32),
onClick = state.popBack,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_back_24),
contentDescription = null,
)
}
},
)
}
@Composable
private fun Content(state: DetailsUM, modifier: Modifier = Modifier) {
private fun Content(
state: DetailsUM,
userWalletListBlockContent: ComposableContentComponent,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
@ -125,6 +88,7 @@ private fun Content(state: DetailsUM, modifier: Modifier = Modifier) {
Block(
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
model = block,
userWalletListBlockContent = userWalletListBlockContent,
)
}
@ -138,7 +102,11 @@ private fun Content(state: DetailsUM, modifier: Modifier = Modifier) {
}
@Composable
private fun Block(model: DetailsItemUM, modifier: Modifier = Modifier) {
private fun Block(
model: DetailsItemUM,
userWalletListBlockContent: ComposableContentComponent,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
@ -149,22 +117,28 @@ private fun Block(model: DetailsItemUM, modifier: Modifier = Modifier) {
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.Top,
) {
val itemModifier = Modifier.fillMaxWidth()
when (model) {
is DetailsItemUM.Basic -> {
model.items.forEach { item ->
key(item.id) {
BlockItem(
modifier = Modifier.fillMaxWidth(),
model = item,
modifier = itemModifier,
model = item.block,
)
}
}
}
is DetailsItemUM.Component -> {
model.content(
modifier = Modifier.fillMaxWidth(),
is DetailsItemUM.WalletConnect -> {
WalletConnectBlock(
modifier = itemModifier,
onClick = model.onClick,
)
}
is DetailsItemUM.UserWalletList -> {
userWalletListBlockContent.Content(modifier = itemModifier)
}
}
}
}
@ -222,7 +196,7 @@ private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) {
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_DetailsScreen() {
TangemThemePreview {
PreviewDetailsComponent().View(modifier = Modifier.fillMaxSize())
PreviewDetailsComponent().Content(modifier = Modifier.fillMaxSize())
}
}
// endregion Preview

View file

@ -1,7 +1,9 @@
package com.tangem.features.details.ui
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -11,6 +13,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -104,12 +107,24 @@ private fun AddWalletButton(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Icon(
AnimatedContent(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_plus_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
targetState = isInProgress,
) { isInProgress ->
if (isInProgress) {
CircularProgressIndicator(
modifier = Modifier.size(TangemTheme.dimens.size24),
color = TangemTheme.colors.icon.accent,
)
} else {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = R.drawable.ic_plus_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
}
Text(
text = text.resolveReference(),

View file

@ -8,6 +8,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.details.impl.R

View file

@ -1,61 +1,59 @@
package com.tangem.features.details.utils
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.feedback.FeedbackManager
import com.tangem.core.navigation.feedback.FeedbackType
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.details.component.UserWalletListComponent
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
import javax.inject.Inject
internal class ItemsBuilder(
private val walletConnectComponent: WalletConnectComponent,
private val userWalletListComponent: UserWalletListComponent,
@ComponentScoped
internal class ItemsBuilder @Inject constructor(
private val router: Router,
private val urlOpener: UrlOpener,
private val feedbackManager: FeedbackManager,
) {
suspend fun buldAll(): ImmutableList<DetailsItemUM> = buildList {
buildWalletConnectBlock()?.let(::add)
suspend fun buldAll(isWalletConnectAvailable: Boolean): ImmutableList<DetailsItemUM> = buildList {
buildWalletConnectBlock(isWalletConnectAvailable)?.let(::add)
buildUserWalletListBlock().let(::add)
buildShopBlock().let(::add)
buildSettingsBlock().let(::add)
buildSupportBlock().let(::add)
}.toImmutableList()
private suspend fun buildWalletConnectBlock(): DetailsItemUM? {
return if (walletConnectComponent.checkIsAvailable()) {
DetailsItemUM.Component(
id = "wallet_connect",
content = {
walletConnectComponent.View(modifier = it)
},
private suspend fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean): DetailsItemUM? {
return if (isWalletConnectAvailable) {
DetailsItemUM.WalletConnect(
onClick = { router.push(AppRoute.WalletConnectSessions) },
)
} else {
null
}
}
private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.Component(
id = "user_wallet_list",
content = {
userWalletListComponent.View(modifier = it)
},
)
private fun buildUserWalletListBlock(): DetailsItemUM = DetailsItemUM.UserWalletList
private fun buildShopBlock(): DetailsItemUM = DetailsItemUM.Basic(
id = "shop",
items = persistentListOf(
DetailsItemUM.Basic.Item(
id = "buy_tangem_wallet",
title = stringReference("Buy Tangem Wallet"), // TODO: Move to resources in [REDACTED_TASK_KEY]
iconRes = R.drawable.ic_tangem_24,
onClick = { router.push(DetailsRoute.Url(BUY_TANGEM_URL)) },
block = BlockUM(
text = resourceReference(R.string.details_buy_wallet),
iconRes = R.drawable.ic_tangem_24,
onClick = { urlOpener.openUrl(BUY_TANGEM_URL) },
),
),
),
)
@ -65,17 +63,21 @@ internal class ItemsBuilder(
items = buildList {
DetailsItemUM.Basic.Item(
id = "app_settings",
title = resourceReference(R.string.app_settings_title),
iconRes = R.drawable.ic_settings_24,
onClick = { router.push(DetailsRoute.Screen(AppScreen.AppSettings)) },
block = BlockUM(
text = resourceReference(R.string.app_settings_title),
iconRes = R.drawable.ic_settings_24,
onClick = { router.push(AppRoute.AppSettings) },
),
).let(::add)
if (BuildConfig.TESTER_MENU_ENABLED) {
DetailsItemUM.Basic.Item(
id = "tester_menu",
title = stringReference(value = "Tester menu"),
iconRes = R.drawable.ic_alert_24,
onClick = { router.push(DetailsRoute.TesterMenu) },
block = BlockUM(
text = stringReference(value = "Tester menu"),
iconRes = R.drawable.ic_alert_24,
onClick = { router.push(AppRoute.TesterMenu) },
),
).let(::add)
}
}.toImmutableList(),
@ -86,15 +88,19 @@ internal class ItemsBuilder(
items = persistentListOf(
DetailsItemUM.Basic.Item(
id = "send_feedback",
title = stringReference("Send feedback"), // TODO: Move to resources in [REDACTED_TASK_KEY]
iconRes = R.drawable.ic_comment_24,
onClick = { router.push(DetailsRoute.Feedback) },
block = BlockUM(
text = resourceReference(R.string.details_send_feedback),
iconRes = R.drawable.ic_comment_24,
onClick = { feedbackManager.sendEmail(FeedbackType.Feedback) },
),
),
DetailsItemUM.Basic.Item(
id = "disclaimer",
title = resourceReference(R.string.disclaimer_title),
iconRes = R.drawable.ic_text_24,
onClick = { router.push(DetailsRoute.Screen(AppScreen.Disclaimer)) },
block = BlockUM(
text = resourceReference(R.string.disclaimer_title),
iconRes = R.drawable.ic_text_24,
onClick = { router.push(AppRoute.Disclaimer(isTosAccepted = true)) },
),
),
),
)

View file

@ -1,15 +1,17 @@
package com.tangem.features.details.utils
import androidx.compose.ui.text.intl.Locale
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.navigation.url.UrlOpener
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
import javax.inject.Inject
internal class SocialsBuilder(
private val router: Router,
@ComponentScoped
internal class SocialsBuilder @Inject constructor(
private val urlOpener: UrlOpener,
) {
fun buildAll(): ImmutableList<DetailsFooterUM.Social> = Social.all.map { social ->
@ -29,7 +31,7 @@ internal class SocialsBuilder(
social.url
}
router.push(DetailsRoute.Url(url))
urlOpener.openUrl(url)
}
private enum class Social(

View file

@ -0,0 +1,84 @@
package com.tangem.features.details.utils
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
internal fun List<UserWallet>.toUiModels(
onClick: (UserWalletId) -> Unit,
appCurrency: AppCurrency? = null,
balances: Map<UserWalletId, TotalFiatBalance> = emptyMap(),
): ImmutableList<UserWalletUM> = this.map { model ->
val balance = balances[model.walletId]
model.mapToUiModel(
balance = balance,
appCurrency = appCurrency,
onClick = { onClick(model.walletId) },
)
}.toImmutableList()
private fun UserWallet.mapToUiModel(
balance: TotalFiatBalance?,
appCurrency: AppCurrency?,
onClick: () -> Unit,
): UserWalletUM = UserWalletUM(
id = walletId,
name = name,
information = getInfo(appCurrency, balance),
imageResId = resolveImage(),
onClick = onClick,
)
private fun UserWallet.getInfo(appCurrency: AppCurrency?, balance: TotalFiatBalance?): TextReference {
val cardCount = getCardCount()
val cardCountRef = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
val amount = when (balance) {
is TotalFiatBalance.Loaded -> balance.amount.takeIf { balance.isAllAmountsSummarized }
is TotalFiatBalance.Failed,
is TotalFiatBalance.Loading,
null,
-> null
}
return if (amount != null && appCurrency != null) {
val divider = stringReference(value = "")
val formattedAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
val amountRef = stringReference(formattedAmount)
TextReference.Combined(wrappedList(cardCountRef, divider, amountRef))
} else {
cardCountRef
}
}
private fun UserWallet.getCardCount() = when (val status = scanResponse.card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount.inc()
is CardDTO.BackupStatus.CardLinked -> status.cardCount.inc()
is CardDTO.BackupStatus.NoBackup,
null,
-> 1
}
@DrawableRes
private fun UserWallet.resolveImage(): Int {
// TODO: Implement image resolving [REDACTED_JIRA]
return R.drawable.ill_card_wallet_2_211_343
}

View file

@ -0,0 +1,139 @@
package com.tangem.features.details.utils
import arrow.core.raise.*
import arrow.core.recover
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.navigation.popTo
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.builder.UserWalletBuilder
import com.tangem.domain.wallets.models.SaveWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.details.impl.R
import javax.inject.Inject
@ComponentScoped
@Suppress("LongParameterList")
internal class UserWalletSaver @Inject constructor(
private val scanCardProcessor: ScanCardProcessor,
private val saveWalletUseCase: SaveWalletUseCase,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val selectWalletUseCase: SelectWalletUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val messageSender: UiMessageSender,
private val router: Router,
) {
suspend fun scanAndSaveUserWallet() = recover(
block = {
val response = scanCard()
val userWallet = createUserWallet(response)
saveWallet(userWallet)
router.popTo<AppRoute.Wallet>()
},
recover = { error ->
val message = error.message
if (!message.isNullOrEmpty()) {
messageSender.send(SnackbarMessage(message))
}
},
)
private suspend fun Raise<Error>.saveWallet(userWallet: UserWallet) {
saveWalletUseCase(userWallet).recover { error ->
when (error) {
is SaveWalletError.WalletAlreadySaved -> selectUserWallet(userWallet)
is SaveWalletError.DataError -> {
val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) {
Error.Unkonwn
}
raise(Error.Message(messageRef))
}
}
}.bind()
reduxStateHolder.onUserWalletSelected(userWallet)
}
private suspend fun Raise<Error>.selectUserWallet(userWallet: UserWallet) {
withError({ Error.Unkonwn }) {
selectWalletUseCase(userWallet.walletId).bind()
}
router.popTo<AppRoute.Wallet>()
}
private suspend fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {
val userWallet = UserWalletBuilder(response, generateWalletNameUseCase).build()
return ensureNotNull(userWallet) { Error.Unkonwn }
}
private suspend fun Raise<Error>.scanCard(): ScanResponse {
var response: ScanResponse? = null
scanCardProcessor.scan(
analyticsSource = AnalyticsParam.ScreensSources.Settings,
onWalletNotCreated = {
raise(Error.WalletNotCreated)
},
disclaimerWillShow = {
router.pop()
raise(Error.DisclaimerWillShow)
},
onSuccess = {
response = it
},
onFailure = { tangemError ->
val error = if (!tangemError.silent) {
val message = tangemError.messageResId
?.let(::resourceReference)
?: stringReference(tangemError.customMessage)
Error.Message(message)
} else {
Error.Silent
}
raise(error)
},
)
return response!!
}
sealed class Error {
open val message: TextReference? = null
data object WalletNotCreated : Error()
data object DisclaimerWillShow : Error()
data object Silent : Error()
data class Message(override val message: TextReference) : Error()
data object Unkonwn : Error() {
override val message: TextReference = resourceReference(R.string.common_unknown_error)
}
}
}

View file

@ -0,0 +1,95 @@
package com.tangem.features.details.utils
import arrow.core.Either
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.lce.lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.core.utils.toLce
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.entity.UserWalletListUM.UserWalletUM
import com.tangem.features.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ComponentScoped
internal class UserWalletsFetcher @Inject constructor(
private val getWalletsUseCase: GetWalletsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val messageSender: UiMessageSender,
) {
val userWallets: Flow<ImmutableList<UserWalletUM>> = getWalletsUseCase()
.distinctUntilChanged()
.transform { wallets ->
if (wallets.isEmpty()) {
error("Wallets must not be empty")
} else {
emit(wallets.toUiModels(onClick = ::navigateToWalletSettings))
}
combine(
getSelectedAppCurrencyUseCase(),
getWalletTotalBalanceUseCase(wallets.map(UserWallet::walletId)),
) { maybeAppCurrency, maybeBalances ->
val models = createUiModels(wallets, maybeAppCurrency, maybeBalances).getOrElse(
ifLoading = { return@combine },
ifError = {
val message = resourceReference(R.string.common_unknown_error)
messageSender.send(SnackbarMessage(message))
return@combine
},
)
emit(models)
}.collect()
}
private fun createUiModels(
wallets: List<UserWallet>,
maybeAppCurrency: Either<SelectedAppCurrencyError, AppCurrency>,
maybeBalances: Lce<TokenListError, Map<UserWalletId, TotalFiatBalance>>,
): Lce<Error, ImmutableList<UserWalletUM>> = lce {
val balances = withError(
transform = { Error.UnableToGetBalances },
block = { maybeBalances.bind() },
)
val appCurrency = withError(
transform = { Error.UnableToGetAppCurrency },
block = { maybeAppCurrency.toLce().bind() },
)
wallets.toUiModels(
appCurrency = appCurrency,
balances = balances,
onClick = ::navigateToWalletSettings,
)
}
private fun navigateToWalletSettings(userWalletId: UserWalletId) {
val message = stringReference("Wallet settings have not yet been implemented: $userWalletId")
messageSender.send(SnackbarMessage(message))
}
sealed class Error {
data object UnableToGetAppCurrency : Error()
data object UnableToGetBalances : Error()
}
}

1
features/disclaimer/api/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,15 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.disclaimer.api"
}
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.disclaimer.api
import androidx.fragment.app.Fragment
interface DisclaimerRouter {
fun entryFragment(): Fragment
}

1
features/disclaimer/impl/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,48 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.disclaimer.impl"
}
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.appCompat)
implementation(deps.lifecycle.compose)
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.permission)
implementation(deps.compose.material3)
implementation(deps.compose.material)
/** Core modules */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.featuretoggles)
implementation(projects.core.navigation)
implementation(projects.common.routing)
/** Domain modules */
implementation(projects.domain.models)
implementation(projects.domain.card)
implementation(projects.domain.settings)
/** Feature modules */
implementation(projects.features.disclaimer.api)
implementation(projects.features.pushNotifications.api)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,52 @@
package com.tangem.features.disclaimer.impl
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.common.routing.AppRoute.Disclaimer.Companion.IS_TOS_ACCEPTED_KEY
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.features.disclaimer.impl.presentation.ui.DisclaimerScreen
import com.tangem.features.disclaimer.impl.presentation.viewmodel.DisclaimerViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class DisclaimerFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
lateinit var appRouter: AppRouter
private val viewModel by viewModels<DisclaimerViewModel>()
private val isTosAccepted: Boolean
get() = arguments?.getBoolean(IS_TOS_ACCEPTED_KEY) ?: false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
}
@Composable
override fun ScreenContent(modifier: Modifier) {
BackHandler {
if (isTosAccepted) {
appRouter.pop()
} else {
requireActivity().finish()
}
}
DisclaimerScreen(viewModel.state, appRouter::pop)
}
companion object {
/** Create disclaimer fragment instance */
fun create(): DisclaimerFragment = DisclaimerFragment()
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.disclaimer.impl.di
import com.tangem.common.routing.AppRouter
import com.tangem.features.disclaimer.api.DisclaimerRouter
import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
/**
* DI module provides implementation of [DisclaimerRouter]
*/
@Module
@InstallIn(ActivityComponent::class)
object DisclaimerRouterModule {
@Provides
@ActivityScoped
fun provideDisclaimerRouter(appRouter: AppRouter): DisclaimerRouter {
return DefaultDisclaimerRouter(appRouter)
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.disclaimer.impl.navigation
import androidx.fragment.app.Fragment
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.features.disclaimer.impl.DisclaimerFragment
import javax.inject.Inject
internal class DefaultDisclaimerRouter @Inject constructor(
private val appRouter: AppRouter,
) : InnerDisclaimerRouter {
override fun entryFragment(): Fragment = DisclaimerFragment.create()
override fun openPushNotificationPermission() {
appRouter.push(AppRoute.PushNotification)
}
override fun openHome() {
appRouter.push(AppRoute.Home)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.disclaimer.impl.navigation
import com.tangem.features.disclaimer.api.DisclaimerRouter
internal interface InnerDisclaimerRouter : DisclaimerRouter {
fun openPushNotificationPermission()
fun openHome()
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.disclaimer.impl.presentation.state
internal data class DisclaimerState(
val url: String,
val isTosAccepted: Boolean,
val onAccept: (Boolean) -> Unit,
)
internal object DummyDisclaimer {
val state = DisclaimerState(
url = "https://tangem.com/tangem_tos.html",
isTosAccepted = false,
onAccept = {},
)
}

View file

@ -0,0 +1,170 @@
package com.tangem.features.disclaimer.impl.presentation.ui
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.view.View
import android.view.ViewGroup
import android.webkit.WebView
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidView
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons
import com.tangem.core.ui.components.appbar.models.AdditionalButton
import com.tangem.core.ui.components.buttons.common.TangemButtonColors
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.disclaimer.impl.R
import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState
import com.tangem.features.disclaimer.impl.presentation.state.DummyDisclaimer
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
@Composable
internal fun DisclaimerScreen(state: DisclaimerState, onBackClick: () -> Unit) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val bottomPadding = if (state.isTosAccepted) {
bottomBarHeight + TangemTheme.dimens.size16
} else {
bottomBarHeight + TangemTheme.dimens.size64
}
val backgroundColor = if (state.isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6
val (textColor, iconColor) = if (state.isTosAccepted) {
TangemTheme.colors.text.primary1 to TangemTheme.colors.icon.primary1
} else {
TangemColorPalette.Light4 to TangemColorPalette.Light4
}
Box(
modifier = Modifier
.background(backgroundColor)
.statusBarsPadding(),
) {
Column(modifier = Modifier.padding(bottom = bottomPadding)) {
AppBarWithAdditionalButtons(
text = resourceReference(R.string.disclaimer_title),
startButton = AdditionalButton(
iconRes = R.drawable.ic_back_24,
onIconClicked = onBackClick,
).takeIf { state.isTosAccepted },
textColor = textColor,
iconColor = iconColor,
)
DisclaimerContent(state.url, state.isTosAccepted)
}
if (!state.isTosAccepted) {
BottomFade(Modifier.align(Alignment.BottomCenter), backgroundColor = backgroundColor)
DisclaimerButton(state.onAccept)
} else {
NavigationBar3ButtonsScrim()
}
}
}
@SuppressLint("SetJavaScriptEnabled")
@Composable
private fun DisclaimerContent(url: String, isTosAccepted: Boolean) {
val progressState = remember { mutableStateOf(ProgressState.Loading) }
val webClient = remember { DisclaimerWebViewClient(progressState) }
val transparent = Color.Transparent
val backgroundColor = if (isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6
Box(
modifier = Modifier,
) {
AndroidView(
factory = {
WebView(it).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
)
setBackgroundColor(transparent.toArgb())
settings.allowFileAccess = false
// to inject css style to display only in dark theme
settings.javaScriptEnabled = !isTosAccepted
overScrollMode = View.OVER_SCROLL_NEVER
webViewClient = webClient
clearHistory()
clearFormData()
clearCache(true)
loadUrl(url)
}
},
)
when (progressState.value) {
ProgressState.Loading -> {
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor),
) {
CircularProgressIndicator(
color = TangemTheme.colors.icon.informative,
modifier = Modifier
.align(Alignment.Center)
.padding(TangemTheme.dimens.spacing8),
)
}
}
else -> Unit
}
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) {
val shouldAskPushPermission = getPushPermissionOrNull()?.let { permission ->
rememberPermissionState(permission = permission).status.isGranted
} ?: true
PrimaryButton(
text = stringResource(id = R.string.common_accept),
onClick = { onAccept(shouldAskPushPermission) },
colors = TangemButtonColors(
backgroundColor = TangemColorPalette.Light4,
contentColor = TangemColorPalette.Dark6,
disabledBackgroundColor = TangemColorPalette.Light4,
disabledContentColor = TangemColorPalette.Dark6,
),
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
)
.fillMaxWidth(),
)
}
// region Preview
@Preview(showBackground = true, widthDp = 360, heightDp = 800)
@Preview(showBackground = true, widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun DisclaimerScreen_Preview() {
TangemThemePreview {
DisclaimerScreen(state = DummyDisclaimer.state, onBackClick = {})
}
}
// endregion

View file

@ -0,0 +1,82 @@
package com.tangem.features.disclaimer.impl.presentation.ui
import android.graphics.Bitmap
import android.webkit.*
import androidx.compose.runtime.MutableState
internal enum class ProgressState {
Loading,
Done,
Error,
}
/**
* Workaround to display web view with ToS only in dark theme
*/
private fun WebView.injectCSS() {
val code = "javascript:(function() {" +
"var node = document.createElement('style');" +
"node.type = 'text/css';" +
" node.innerHTML = 'body, label,th,p,a, td, tr,li,ul,span,table,h1,h2,h3,h4,h5,h6,h7,div,small {" +
" color: #C9C9C9;" +
"background-color: #1E1E1E;" +
" } ';" +
" document.head.appendChild(node);})();"
evaluateJavascript(code, null)
}
internal class DisclaimerWebViewClient(private val progressState: MutableState<ProgressState>) : WebViewClient() {
private var loadingUrl: String? = null
private var loadedUrl: String? = null
fun reset() {
loadingUrl = null
loadedUrl = null
progressState.value = ProgressState.Loading
}
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
view?.injectCSS()
super.onPageStarted(view, url, favicon)
if (loadingUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Loading
loadingUrl = url
}
override fun onPageFinished(view: WebView?, url: String?) {
view?.injectCSS()
super.onPageFinished(view, url)
if (loadedUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Done
loadedUrl = url
}
override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) {
view?.injectCSS()
super.onReceivedError(view, resourceRequest, error)
error?.let { progressState.value = ProgressState.Error }
}
override fun onReceivedHttpError(
view: WebView?,
resourceRequest: WebResourceRequest?,
errorResponse: WebResourceResponse?,
) {
view?.injectCSS()
super.onReceivedHttpError(view, resourceRequest, errorResponse)
if (resourceRequest != null && errorResponse != null) {
val isDifferentUrl = resourceRequest.url?.toString() != loadingUrl
val isSuccessCode = errorResponse.statusCode < RESPONSE_USER_ERROR_STATUS_CODE
val isNotDone = progressState.value != ProgressState.Done
if (isDifferentUrl || isSuccessCode || isNotDone) return
progressState.value = ProgressState.Error
}
}
companion object {
private const val RESPONSE_USER_ERROR_STATUS_CODE = 400
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.disclaimer.impl.presentation.viewmodel
internal interface DisclaimerClickIntents {
fun onAccept(shouldAskPushPermission: Boolean)
}

View file

@ -0,0 +1,41 @@
package com.tangem.features.disclaimer.impl.presentation.viewmodel
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRoute.Disclaimer.Companion.IS_TOS_ACCEPTED_KEY
import com.tangem.domain.card.repository.CardRepository
import com.tangem.features.disclaimer.impl.navigation.DefaultDisclaimerRouter
import com.tangem.features.disclaimer.impl.presentation.state.DisclaimerState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class DisclaimerViewModel @Inject constructor(
private val cardRepository: CardRepository,
private val disclaimerRouter: DefaultDisclaimerRouter,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, DisclaimerClickIntents {
private val isTosAccepted: Boolean = savedStateHandle[IS_TOS_ACCEPTED_KEY] ?: false
val state: DisclaimerState
get() = DisclaimerState(
onAccept = ::onAccept,
url = DISCLAIMER_URL,
isTosAccepted = isTosAccepted,
)
override fun onAccept(shouldAskPushPermission: Boolean) {
viewModelScope.launch {
cardRepository.acceptTangemTOS()
disclaimerRouter.openPushNotificationPermission()
}
}
private companion object {
const val DISCLAIMER_URL = "https://tangem.com/tangem_tos.html"
}
}

View file

@ -4,12 +4,7 @@ import android.annotation.SuppressLint
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.*
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
@ -36,6 +31,7 @@ import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalDensity
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@ -43,7 +39,9 @@ import androidx.navigation.compose.rememberNavController
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetDraggableHeader
import com.tangem.core.ui.components.bottomsheets.collapse
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRoute
import com.tangem.managetokens.presentation.addcustomtoken.router.AddCustomTokenRouter
import com.tangem.managetokens.presentation.addcustomtoken.viewmodels.AddCustomTokenViewModel
@ -57,6 +55,7 @@ fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig) {
var isVisible by remember { mutableStateOf(value = config.isShow) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
if (isVisible) {
// ViewModel cannot be scoped to ModalBottomSheet's lifecycle,
@ -67,11 +66,13 @@ fun AddCustomTokenBottomSheet(config: TangemBottomSheetConfig) {
}
ModalBottomSheetWithBackHandling(
modifier = Modifier
.sizeIn(maxHeight = LocalWindowSize.current.height - statusBarHeight),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = TangemTheme.colors.background.tertiary,
shape = TangemTheme.shapes.bottomSheetLarge,
windowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Top),
windowInsets = WindowInsetsZero,
dragHandle = { TangemBottomSheetDraggableHeader(color = TangemTheme.colors.background.tertiary) },
properties = ModalBottomSheetDefaults.properties(shouldDismissOnBackPress = false),
) {

View file

@ -65,7 +65,6 @@ private fun Content(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit)
Box(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding()
.imePadding()
.background(color = TangemTheme.colors.background.primary),
) {

View file

@ -42,6 +42,7 @@ dependencies {
/** Compose libraries */
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.animation)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)

View file

@ -1,7 +1,6 @@
package com.tangem.feature.onboarding.api
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.runtime.Composable
@ -19,11 +18,9 @@ class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi {
@Composable
override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) {
BackHandler(onBack = uiState.onBackClick)
TangemTheme(isDark = isSystemInDarkTheme()) {
Column {
ProgressIndicator(progress)
Content(subScreen, uiState)
}
Column {
ProgressIndicator(progress)
Content(subScreen, uiState)
}
}
}

View file

@ -4,9 +4,4 @@ package com.tangem.feature.onboarding.navigation
* Onboarding router
*/
// TODO: Move to onboarding api module [REDACTED_JIRA]
interface OnboardingRouter {
companion object {
const val CAN_SKIP_BACKUP = "onboarding_wallet_can_skip_backup"
}
}
interface OnboardingRouter

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,15 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.pushnotifications.api"
}
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.pushnotifications.api.featuretoggles
/**
* Push notifications feature toggles
*/
interface PushNotificationsFeatureToggles {
/** Availability of push notifications */
val isPushNotificationsEnabled: Boolean
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.pushnotifications.api.navigation
import androidx.fragment.app.Fragment
interface PushNotificationsRouter {
fun entryFragment(): Fragment
}

View file

@ -0,0 +1,20 @@
package com.tangem.features.pushnotifications.api.utils
import android.Manifest
import android.os.Build
import androidx.annotation.ChecksSdkIntAtLeast
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
private val isRequirePushPermission = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
val PUSH_PERMISSION = if (isRequirePushPermission) {
Manifest.permission.POST_NOTIFICATIONS
} else {
"android.permission.POST_NOTIFICATIONS"
}
fun getPushPermissionOrNull() = if (isRequirePushPermission) {
Manifest.permission.POST_NOTIFICATIONS
} else {
null
}

View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,48 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.feature.pushnotifications.impl"
}
dependencies {
/** AndroidX */
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.permission)
/** Other dependencies */
implementation(deps.timber)
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
/** Core modules */
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
implementation(projects.core.navigation)
/** Common modules */
implementation(projects.common.routing)
/** Common modules */
implementation(projects.common.routing)
/** Domain module */
implementation(projects.domain.settings)
/** Feature modules */
implementation(projects.features.pushNotifications.api)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.pushnotifications.impl
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.features.pushnotifications.impl.presentation.ui.PushNotificationsScreen
import com.tangem.features.pushnotifications.impl.presentation.viewmodel.PushNotificationViewModel
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class PushNotificationsFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
private val viewModel by viewModels<PushNotificationViewModel>()
@Composable
override fun ScreenContent(modifier: Modifier) {
BackHandler(onBack = requireActivity()::finish)
NavigationBar3ButtonsScrim()
PushNotificationsScreen(
onShowAllow = viewModel::onAllowPermission,
onAllow = viewModel::onAllowedPermission,
onLater = viewModel::onAskLater,
onOpenSettings = viewModel::openSettings,
)
}
companion object {
/** Create push notifications fragment instance */
fun create(): PushNotificationsFragment = PushNotificationsFragment()
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.pushnotifications.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.impl.featuretoggles.DefaultPushNotificationsFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
/**
* DI module provides implementation of [PushNotificationsFeatureToggles]
*/
@Module
@InstallIn(SingletonComponent::class)
internal object PushNotificationsFeatureTogglesModule {
@Provides
@Singleton
fun provideSendFeatureToggles(featureTogglesManager: FeatureTogglesManager): PushNotificationsFeatureToggles {
return DefaultPushNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.features.pushnotifications.impl.di
import com.tangem.common.routing.AppRouter
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.scopes.ActivityScoped
/**
* DI module provides implementation of [PushNotificationsRouter]
*/
@Module
@InstallIn(ActivityComponent::class)
object PushNotificationsModule {
@Provides
@ActivityScoped
fun provideDisclaimerRouter(appRouter: AppRouter): PushNotificationsRouter {
return DefaultPushNotificationsRouter(appRouter)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.pushnotifications.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
internal class DefaultPushNotificationsFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : PushNotificationsFeatureToggles {
override val isPushNotificationsEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED")
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.pushnotifications.impl.navigation
import androidx.fragment.app.Fragment
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.features.pushnotifications.impl.PushNotificationsFragment
import javax.inject.Inject
internal class DefaultPushNotificationsRouter @Inject constructor(
private val appRouter: AppRouter,
) : InnerPushNotificationsRouter {
override fun entryFragment(): Fragment = PushNotificationsFragment.create()
override fun openHome() {
appRouter.push(AppRoute.Home)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.features.pushnotifications.impl.navigation
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
interface InnerPushNotificationsRouter : PushNotificationsRouter {
fun openHome()
}

View file

@ -0,0 +1,61 @@
package com.tangem.features.pushnotifications.impl.presentation.ui
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.showcase.Showcase
import com.tangem.core.ui.components.showcase.model.ShowcaseButtonModel
import com.tangem.core.ui.components.showcase.model.ShowcaseItemModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.utils.requestPushPermission
import com.tangem.feature.pushnotifications.impl.R
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun PushNotificationsScreen(
onShowAllow: () -> Unit,
onAllow: () -> Unit,
onLater: () -> Unit,
onOpenSettings: () -> Unit,
) {
val isClicked = remember { mutableStateOf(false) }
val requestPushPermission = requestPushPermission(
isFirstTimeAsking = true,
isClicked = isClicked,
onAllow = onAllow,
onDeny = onLater,
onOpenSettings = onOpenSettings,
pushPermission = getPushPermissionOrNull(),
)
Showcase(
headerIconRes = R.drawable.ic_notifications_unread_24,
headerText = resourceReference(R.string.user_push_notification_agreement_header),
showcaseItems = persistentListOf(
ShowcaseItemModel(
R.drawable.ic_rocket_launch_24,
resourceReference(R.string.user_push_notification_agreement_argument_one),
),
ShowcaseItemModel(
R.drawable.ic_storefront_24,
resourceReference(R.string.user_push_notification_agreement_argument_two),
),
),
primaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_allow),
onClick = {
isClicked.value = true
onShowAllow()
requestPushPermission()
},
),
secondaryButton = ShowcaseButtonModel(
buttonText = resourceReference(R.string.common_later),
onClick = onLater,
),
modifier = Modifier.systemBarsPadding(),
)
}

View file

@ -0,0 +1,46 @@
package com.tangem.features.pushnotifications.impl.presentation.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.navigation.settings.SettingsManager
import com.tangem.domain.settings.DelayPermissionRequestUseCase
import com.tangem.domain.settings.NeverRequestPermissionUseCase
import com.tangem.domain.settings.SetFirstTimeAskingPermissionUseCase
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.impl.navigation.DefaultPushNotificationsRouter
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
internal class PushNotificationViewModel @Inject constructor(
private val setFirstTimeAskingPermissionUseCase: SetFirstTimeAskingPermissionUseCase,
private val delayPermissionRequestUseCase: DelayPermissionRequestUseCase,
private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase,
private val router: DefaultPushNotificationsRouter,
private val settingsManager: SettingsManager,
) : ViewModel(), PushNotificationsClickIntents {
override fun onAskLater() {
viewModelScope.launch {
delayPermissionRequestUseCase(PUSH_PERMISSION)
setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION)
}
router.openHome()
}
override fun onAllowPermission() {
viewModelScope.launch {
setFirstTimeAskingPermissionUseCase(PUSH_PERMISSION)
}
}
override fun onAllowedPermission() {
viewModelScope.launch {
neverRequestPermissionUseCase(PUSH_PERMISSION)
}
router.openHome()
}
override fun openSettings() = settingsManager.openSettings()
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.pushnotifications.impl.presentation.viewmodel
internal interface PushNotificationsClickIntents {
fun onAskLater()
fun onAllowPermission()
fun onAllowedPermission()
fun openSettings()
}

View file

@ -5,9 +5,4 @@ import androidx.fragment.app.Fragment
interface QrScanningRouter {
fun getEntryFragment(): Fragment
companion object {
const val SOURCE_KEY = "source"
const val NETWORK_KEY = "network"
}
}

View file

@ -16,6 +16,7 @@ dependencies {
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.navigation)
implementation(projects.common.routing)
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.activity.compose)

View file

@ -7,21 +7,16 @@ import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.google.mlkit.vision.common.InputImage
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.SystemBarsIconsDisposable
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.qrscanning.inner.MLKitBarcodeAnalyzer
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
@ -105,7 +100,8 @@ internal class QrScanningFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
StatusBarTransparencyDisposable()
SystemBarsIconsDisposable(darkIcons = false)
QrScanningContent(
executor = { cameraExecutor },
analyzer = { cameraAnalyzer },
@ -145,29 +141,4 @@ internal class QrScanningFragment : ComposeFragment() {
fun create() = QrScanningFragment()
}
}
@Composable
private fun StatusBarTransparencyDisposable() {
val systemUiController = rememberSystemUiController()
val systemBarsColor = TangemTheme.colors.background.secondary
val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(Unit) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
systemUiController.setSystemBarsColor(
color = Color.Transparent,
darkIcons = false,
)
}
if (event == Lifecycle.Event.ON_STOP) {
systemUiController.setSystemBarsColor(systemBarsColor)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}

View file

@ -1,6 +1,6 @@
package com.tangem.feature.qrscanning.di
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRouter
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.qrscanning.navigation.DefaultQrScanningRouter
import dagger.Module
@ -15,7 +15,7 @@ internal object QrScanningRouterModule {
@Provides
@ActivityScoped
fun provideQrScanRouter(reduxNavController: ReduxNavController): QrScanningRouter {
return DefaultQrScanningRouter(reduxNavController)
fun provideQrScanRouter(appRouter: AppRouter): QrScanningRouter {
return DefaultQrScanningRouter(appRouter)
}
}

View file

@ -1,15 +1,16 @@
package com.tangem.feature.qrscanning.navigation
import androidx.fragment.app.Fragment
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRouter
import com.tangem.feature.qrscanning.QrScanningFragment
class DefaultQrScanningRouter(
private val reduxNavController: ReduxNavController,
private val router: AppRouter,
) : QrScanningInnerRouter {
override fun getEntryFragment(): Fragment = QrScanningFragment.create()
override fun popBackStack() {
reduxNavController.popBackStack()
router.pop()
}
}

View file

@ -3,8 +3,7 @@ package com.tangem.feature.qrscanning.viewmodel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.NETWORK_KEY
import com.tangem.feature.qrscanning.QrScanningRouter.Companion.SOURCE_KEY
import com.tangem.common.routing.AppRoute
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.feature.qrscanning.navigation.QrScanningInnerRouter
import com.tangem.feature.qrscanning.presentation.QrScanningState
@ -24,8 +23,8 @@ internal class QrScanningViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val source: SourceType = savedStateHandle[SOURCE_KEY] ?: error("Source is mandatory")
private val network: String? = savedStateHandle[NETWORK_KEY]
private val source: SourceType = savedStateHandle[AppRoute.QrScanning.SOURCE_KEY] ?: error("Source is mandatory")
private val network: String? = savedStateHandle[AppRoute.QrScanning.NETWORK_KEY]
val uiState: StateFlow<QrScanningState> = stateHolder.uiState
val launchGalleryEvent: SharedFlow<GalleryRequest> = clickIntents.launchGallery

View file

@ -12,12 +12,13 @@ android {
dependencies {
/** Core modules */
implementation(project(":core:analytics"))
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(project(":core:res"))
implementation(project(":core:utils"))
implementation(project(":core:ui"))
implementation(project(":libs:crypto"))
implementation(projects.core.res)
implementation(projects.core.utils)
implementation(projects.core.ui)
implementation(projects.libs.crypto)
implementation(projects.common.routing)
/** AndroidX */
implementation(deps.androidx.appCompat)
@ -30,7 +31,7 @@ dependencies {
implementation(deps.compose.ui.tooling)
/** Domain */
implementation(project(":features:referral:domain"))
implementation(projects.features.referral.domain)
/** Other libraries */
implementation(deps.compose.shimmer)

View file

@ -1,19 +1,16 @@
package com.tangem.feature.referral
import android.os.Bundle
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import com.tangem.common.routing.AppRouter
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.feature.referral.router.ReferralRouter
import com.tangem.feature.referral.ui.ReferralScreen
import com.tangem.feature.referral.viewmodels.ReferralViewModel
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import javax.inject.Inject
@AndroidEntryPoint
@ -22,22 +19,19 @@ class ReferralFragment : ComposeFragment() {
@Inject
override lateinit var uiDependencies: UiDependencies
@Inject
internal lateinit var appRouter: AppRouter
private val viewModel by viewModels<ReferralViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.onScreenOpened()
viewModel.setRouter(ReferralRouter(fragmentManager = WeakReference(parentFragmentManager)))
viewModel.setRouter(ReferralRouter(appRouter))
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val backgroundColor = TangemTheme.colors.background.secondary
SystemBarsEffect { setSystemBarsColor(backgroundColor) }
ReferralScreen(
modifier = Modifier.systemBarsPadding(),
stateHolder = viewModel.uiState,
)
ReferralScreen(stateHolder = viewModel.uiState)
}
}

View file

@ -1,11 +1,12 @@
package com.tangem.feature.referral.router
import androidx.fragment.app.FragmentManager
import java.lang.ref.WeakReference
import com.tangem.common.routing.AppRouter
internal class ReferralRouter(private val fragmentManager: WeakReference<FragmentManager>) {
internal class ReferralRouter(
private val appRouter: AppRouter,
) {
fun back() {
fragmentManager.get()?.popBackStack()
appRouter.pop()
}
}

View file

@ -2,23 +2,20 @@ package com.tangem.feature.referral.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.tangem.core.ui.components.appbar.AppBarWithAdditionalButtons
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.presentation.R
/**
@ -28,15 +25,16 @@ import com.tangem.feature.referral.presentation.R
*/
@Composable
internal fun AgreementBottomSheetContent(url: String) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(
modifier = Modifier
.fillMaxWidth()
.height(LocalConfiguration.current.screenHeightDp.dp - TangemTheme.dimens.spacing16),
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title))
AgreementHtmlView(url = url)
}
AppBarWithAdditionalButtons(text = stringResource(id = R.string.details_referral_title))
AgreementHtmlView(url = url)
Spacer(modifier = Modifier.height(bottomBarHeight))
}
}
@ -46,7 +44,7 @@ private fun AgreementHtmlView(url: String) {
val isInPreviewMode = LocalInspectionMode.current
WebView(
state = state,
modifier = Modifier.background(TangemTheme.colors.background.secondary),
modifier = Modifier.background(TangemTheme.colors.background.primary),
captureBackPresses = false,
onCreated = {
if (!isInPreviewMode) {

View file

@ -1,72 +0,0 @@
package com.tangem.feature.referral.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Suppress("LongParameterList")
@Composable
internal fun AwardText(
startText: String,
startTextColor: Color,
startTextStyle: TextStyle,
endText: String,
endTextColor: Color,
endTextStyle: TextStyle,
cornersToRound: CornersToRound,
) {
Surface(
shape = cornersToRound.getShape(),
color = TangemTheme.colors.background.primary,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.heightIn(TangemTheme.dimens.size48)
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = startText,
color = startTextColor,
maxLines = 1,
style = startTextStyle,
)
Text(
text = endText,
color = endTextColor,
maxLines = 1,
style = endTextStyle,
)
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun Preview_AwardItem() {
TangemThemePreview {
AwardText(
startText = "startText",
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
endText = "endText",
endTextColor = TangemTheme.colors.text.primary1,
endTextStyle = TangemTheme.typography.subtitle2,
cornersToRound = CornersToRound.TOP_2,
)
}
}

View file

@ -1,27 +0,0 @@
package com.tangem.feature.referral.ui
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
internal enum class CornersToRound {
ALL_4,
TOP_2,
BOTTOM_2,
ZERO,
;
@Suppress("TopLevelComposableFunctions")
@Composable
fun getShape(): RoundedCornerShape {
val radius = TangemTheme.dimens.radius12
return when (this) {
ALL_4 -> RoundedCornerShape(radius)
TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius)
BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius)
ZERO -> RoundedCornerShape(0.dp)
}
}
}

View file

@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.core.content.ContextCompat.startActivity
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.components.rows.RoundableCornersRow
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
@ -82,7 +84,7 @@ private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: Expected
private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) {
val isExpectedAwardsPresent = expectedAwards != null
AwardText(
RoundableCornersRow(
startText = stringResource(id = R.string.referral_friends_bought_title),
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
@ -111,7 +113,7 @@ private fun Awards(expectedAwards: ExpectedAwards) {
thickness = TangemTheme.dimens.size0_5,
color = TangemTheme.colors.stroke.primary,
)
AwardText(
RoundableCornersRow(
startText = if (expectedAwards.expectedAwards.isNotEmpty()) {
stringResource(id = R.string.referral_expected_awards)
} else {
@ -137,7 +139,7 @@ private fun Awards(expectedAwards: ExpectedAwards) {
val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode)
initialItems.forEachIndexed { index, expectedAward ->
AwardText(
RoundableCornersRow(
startText = expectedAward.paymentDate,
startTextColor = TangemTheme.colors.text.primary1,
startTextStyle = TangemTheme.typography.subtitle2,
@ -218,7 +220,7 @@ private fun LessMoreButton(isExpanded: MutableState<Boolean>) {
private fun ExtraItems(extraItems: List<ExpectedAward>) {
Column {
extraItems.forEach { expectedAward ->
AwardText(
RoundableCornersRow(
startText = expectedAward.paymentDate,
startTextColor = TangemTheme.colors.text.primary1,
startTextStyle = TangemTheme.typography.subtitle2,

View file

@ -1,11 +1,18 @@
package com.tangem.feature.referral.ui
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.feature.referral.models.ReferralStateHolder
@OptIn(ExperimentalMaterial3Api::class)
@ -16,14 +23,18 @@ internal fun ReferralBottomSheet(
onDismissRequest: () -> Unit,
config: ReferralStateHolder.ReferralInfoState,
) {
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
if (isVisible) {
ModalBottomSheet(
modifier = Modifier.height(LocalWindowSize.current.height - statusBarHeight),
onDismissRequest = onDismissRequest,
sheetState = sheetState,
shape = RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
windowInsets = WindowInsetsZero,
containerColor = TangemTheme.colors.background.primary,
) {
AgreementBottomSheetContent(

View file

@ -46,16 +46,16 @@ import kotlinx.coroutines.launch
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier = Modifier) {
internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
var isBottomSheetVisible by remember { mutableStateOf(value = false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val snackbarHostState = remember(::SnackbarHostState)
Scaffold(
modifier = modifier,
topBar = {
AppBarWithBackButton(
modifier = Modifier.statusBarsPadding(),
text = stringResource(R.string.details_referral_title),
onBackClick = stateHolder.headerState.onBackClicked,
)

View file

@ -5,13 +5,4 @@ import androidx.fragment.app.Fragment
interface SendRouter {
fun getEntryFragment(): Fragment
companion object {
const val CRYPTO_CURRENCY_KEY = "send_crypto_currency"
const val USER_WALLET_ID_KEY = "send_user_wallet_id"
const val TRANSACTION_ID_KEY = "send_transaction_id"
const val AMOUNT_KEY = "send_amount"
const val TAG_KEY = "send_tag"
const val DESTINATION_ADDRESS_KEY = "send_destination_address"
}
}

View file

@ -24,6 +24,7 @@ dependencies {
implementation(deps.jodatime)
implementation(deps.timber)
implementation(deps.reKotlin)
implementation(deps.kotlin.serialization)
/** Compose */
implementation(deps.compose.accompanist.systemUiController)
@ -51,7 +52,8 @@ dependencies {
implementation(projects.core.datasource)
/** Common */
implementation(projects.common)
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Libs */
implementation(projects.libs.crypto)

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.impl.di
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.DefaultSendRouter
import dagger.Module
@ -18,7 +19,7 @@ internal object SendRouterModule {
@Provides
@ActivityScoped
fun provideSendRouter(reduxNavController: ReduxNavController): SendRouter {
return DefaultSendRouter(reduxNavController)
fun provideSendRouter(appRouter: AppRouter, urlOpener: UrlOpener): SendRouter {
return DefaultSendRouter(appRouter, urlOpener)
}
}

View file

@ -1,48 +1,43 @@
package com.tangem.features.send.impl.navigation
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.ReduxNavController
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.features.send.impl.presentation.SendFragment
import com.tangem.features.tokendetails.navigation.TokenDetailsRouter
internal class DefaultSendRouter(
private val reduxNavController: ReduxNavController,
private val router: AppRouter,
private val urlOpener: UrlOpener,
) : InnerSendRouter {
override fun getEntryFragment(): Fragment = SendFragment.create()
override fun openUrl(url: String) {
reduxNavController.navigate(NavigationAction.OpenUrl(url = url))
urlOpener.openUrl(url)
}
override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) {
reduxNavController.popBackStack()
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.WalletDetails,
bundle = bundleOf(
TokenDetailsRouter.USER_WALLET_ID_KEY to userWalletId.stringValue,
TokenDetailsRouter.CRYPTO_CURRENCY_KEY to currency,
),
),
)
router.pop { isSuccess ->
if (isSuccess) {
router.push(
AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = currency,
),
)
}
}
}
override fun openQrCodeScanner(network: String) {
reduxNavController.navigate(
action = NavigationAction.NavigateTo(
screen = AppScreen.QrScanning,
bundle = bundleOf(
QrScanningRouter.SOURCE_KEY to SourceType.SEND,
QrScanningRouter.NETWORK_KEY to network,
),
router.push(
AppRoute.QrScanning(
source = SourceType.SEND,
networkName = network,
),
)
}

View file

@ -5,10 +5,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.features.send.api.navigation.SendRouter
import com.tangem.features.send.impl.navigation.InnerSendRouter
@ -16,7 +16,6 @@ import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.features.send.impl.presentation.ui.SendScreen
import com.tangem.features.send.impl.presentation.viewmodel.SendViewModel
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
import javax.inject.Inject
/**
@ -31,6 +30,9 @@ internal class SendFragment : ComposeFragment() {
@Inject
lateinit var router: SendRouter
@Inject
lateinit var appRouter: AppRouter
@Inject
lateinit var analyticsEventsHandler: AnalyticsEventHandler
@ -44,11 +46,11 @@ internal class SendFragment : ComposeFragment() {
super.onCreate(savedInstanceState)
lifecycle.addObserver(viewModel)
val isEditingDisabled = arguments?.getString(SendRouter.TRANSACTION_ID_KEY) != null
val isEditingDisabled = arguments?.getString(AppRoute.Send.TRANSACTION_ID_KEY) != null
viewModel.setRouter(
innerSendRouter,
StateRouter(
fragmentManager = WeakReference(parentFragmentManager),
appRouter = appRouter,
isEditingDisabled = isEditingDisabled,
analyticsEventsHandler = analyticsEventsHandler,
),
@ -57,10 +59,6 @@ internal class SendFragment : ComposeFragment() {
@Composable
override fun ScreenContent(modifier: Modifier) {
val systemBarsColor = TangemTheme.colors.background.tertiary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
val currentState = viewModel.stateRouter.currentState.collectAsStateWithLifecycle()
SendScreen(viewModel.uiState, currentState.value)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.send.impl.presentation.analytics.utils
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
@ -36,7 +37,7 @@ internal class SendScreenAnalyticSender(
}
}
SendUiStateType.Amount -> {
val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return
val amountState = state.getAmountState(stateRouterProvider().isEditState) as? AmountState.Data ?: return
val isFiatSelected = amountState.amountTextField.isFiatValue
val selectedCurrency = if (!isFiatSelected) {
SelectedCurrencyType.Token

View file

@ -1,18 +1,18 @@
package com.tangem.features.send.impl.presentation.state
import com.tangem.blockchain.common.TransactionData
import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.event.consumedEvent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.send.impl.presentation.state.amount.SendAmountStateConverter
import com.tangem.features.send.impl.presentation.state.common.SendSyncEditConverter
import com.tangem.features.send.impl.presentation.state.confirm.SendConfirmStateConverter
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fee.SendFeeStateConverter
import com.tangem.features.send.impl.presentation.state.fee.checkFeeCoverage
import com.tangem.features.send.impl.presentation.state.fields.SendAmountFieldConverter
import com.tangem.features.send.impl.presentation.state.recipient.SendRecipientStateConverter
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Provider
@ -33,20 +33,12 @@ internal class SendStateFactory(
) {
private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter)
private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountFieldConverter(
clickIntents = clickIntents,
stateRouterProvider = stateRouterProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
appCurrencyProvider = appCurrencyProvider,
)
}
private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) {
SendAmountStateConverter(
AmountStateConverter(
clickIntents = clickIntents,
appCurrencyProvider = appCurrencyProvider,
iconStateConverter = iconStateConverter,
userWalletProvider = userWalletProvider,
sendAmountFieldConverter = amountFieldConverter,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
@ -79,12 +71,19 @@ internal class SendStateFactory(
isBalanceHidden = false,
cryptoCurrencyName = "",
isSubtracted = false,
amountState = AmountState.Empty(false),
editAmountState = AmountState.Empty(false),
)
fun getReadyState(): SendUiState {
val state = currentStateProvider()
val amountState = if (state.amountState is AmountState.Empty) {
amountStateConverter.convert("")
} else {
state.amountState
}
return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(""),
amountState = amountState,
recipientState = state.recipientState
?: recipientStateConverter.convert(SendRecipientStateConverter.Data("", null)),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
@ -95,8 +94,13 @@ internal class SendStateFactory(
fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState {
val state = currentStateProvider()
val amountState = if (state.amountState is AmountState.Empty) {
amountStateConverter.convert(amount)
} else {
state.amountState
}
return state.copy(
amountState = state.amountState ?: amountStateConverter.convert(amount),
amountState = amountState,
recipientState = state.recipientState
?: recipientStateConverter.convert(SendRecipientStateConverter.Data(destinationAddress, memo)),
feeState = state.feeState ?: feeStateConverter.convert(Unit),
@ -117,7 +121,7 @@ internal class SendStateFactory(
fun getIsAmountSubtractedState(isAmountSubtractAvailable: Boolean): SendUiState {
val state = currentStateProvider()
val balance = cryptoCurrencyStatusProvider().value.amount ?: return state
val amountState = state.getAmountState(stateRouterProvider().isEditState) ?: return state
val amountState = state.getAmountState(stateRouterProvider().isEditState) as? AmountState.Data ?: return state
val feeState = state.getFeeState(stateRouterProvider().isEditState) ?: return state
val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state
val feeValue = feeState.fee?.amount?.value ?: BigDecimal.ZERO

View file

@ -3,17 +3,14 @@ package com.tangem.features.send.impl.presentation.state
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.send.impl.presentation.domain.SendRecipientListContent
import com.tangem.features.send.impl.presentation.state.amount.SendAmountSegmentedButtonsConfig
import com.tangem.features.send.impl.presentation.state.fee.FeeSelectorState
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import java.math.BigDecimal
/**
@ -24,11 +21,11 @@ internal data class SendUiState(
val clickIntents: SendClickIntents,
val isEditingDisabled: Boolean,
val cryptoCurrencyName: String,
val amountState: SendStates.AmountState? = null,
val amountState: AmountState,
val recipientState: SendStates.RecipientState? = null,
val feeState: SendStates.FeeState? = null,
val sendState: SendStates.SendState? = null,
val editAmountState: SendStates.AmountState? = null,
val editAmountState: AmountState,
val editRecipientState: SendStates.RecipientState? = null,
val editFeeState: SendStates.FeeState? = null,
val isBalanceHidden: Boolean,
@ -36,7 +33,7 @@ internal data class SendUiState(
val event: StateEvent<SendEvent>,
) {
fun getAmountState(isEditState: Boolean): SendStates.AmountState? {
fun getAmountState(isEditState: Boolean): AmountState {
return if (isEditState) {
editAmountState
} else {
@ -62,7 +59,7 @@ internal data class SendUiState(
fun copyWrapped(
isEditState: Boolean,
amountState: SendStates.AmountState? = this.amountState,
amountState: AmountState = this.amountState,
feeState: SendStates.FeeState? = this.feeState,
recipientState: SendStates.RecipientState? = this.recipientState,
sendState: SendStates.SendState? = this.sendState,
@ -90,21 +87,6 @@ internal sealed class SendStates {
abstract val isPrimaryButtonEnabled: Boolean
/** Amount state */
@Stable
data class AmountState(
override val type: SendUiStateType = SendUiStateType.Amount,
override val isPrimaryButtonEnabled: Boolean,
val walletName: String,
val walletBalance: TextReference,
val tokenIconState: TokenIconState,
val segmentedButtonConfig: PersistentList<SendAmountSegmentedButtonsConfig>,
val selectedButton: Int,
val isSegmentedButtonsEnabled: Boolean,
val amountTextField: SendTextField.AmountField,
val appCurrencyCode: String,
) : SendStates()
/** Recipient state */
@Stable
data class RecipientState(

View file

@ -1,15 +1,14 @@
package com.tangem.features.send.impl.presentation.state
import androidx.fragment.app.FragmentManager
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.features.send.impl.presentation.analytics.SendAnalyticEvents
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import java.lang.ref.WeakReference
internal class StateRouter(
private val fragmentManager: WeakReference<FragmentManager>,
private val appRouter: AppRouter,
private val analyticsEventsHandler: AnalyticsEventHandler,
private val isEditingDisabled: Boolean,
) {
@ -26,7 +25,7 @@ internal class StateRouter(
}
fun popBackStack() {
fragmentManager.get()?.popBackStack()
appRouter.pop()
}
fun onBackClick(isSuccess: Boolean = false) {

View file

@ -1,5 +1,6 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
@ -64,7 +65,7 @@ internal class AmountStateFactory(
fun getOnAmountReduceByState(reduceAmountBy: BigDecimal, reduceAmountByDiff: BigDecimal) =
amountReduceByConverter.convert(
SendAmountReduceByConverter.ReduceByData(
AmountReduceByTransformer.ReduceByData(
reduceAmountBy = reduceAmountBy,
reduceAmountByDiff = reduceAmountByDiff,
),

View file

@ -1,59 +0,0 @@
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.ui.text.input.ImeAction
import com.tangem.common.extensions.isZero
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import java.math.BigDecimal
import java.math.RoundingMode
internal fun String.getCryptoValue(fiatRate: BigDecimal?, isFiatValue: Boolean, decimals: Int): String {
return if (isFiatValue && fiatRate != null) {
parseToBigDecimal(decimals).divide(fiatRate, decimals, RoundingMode.DOWN)
.parseBigDecimal(decimals)
} else {
this
}
}
internal fun String.getFiatValue(
fiatRate: BigDecimal?,
isFiatValue: Boolean,
decimals: Int,
): Pair<String, BigDecimal?> {
return if (fiatRate != null) {
val fiatValue = if (!isFiatValue) {
parseToBigDecimal(decimals).multiply(fiatRate).parseBigDecimal(decimals)
} else {
this
}
val decimalFiatValue = fiatValue.parseToBigDecimal(decimals)
fiatValue to decimalFiatValue
} else {
"" to null
}
}
internal fun String.checkExceedBalance(
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountTextField: SendTextField.AmountField,
): Boolean {
val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO
val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO
val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals)
val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals)
return if (amountTextField.isFiatValue) {
fiatDecimal > currencyFiatAmount
} else {
cryptoDecimal > currencyCryptoAmount
}
}
internal fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) =
if (!isExceedBalance && !decimalCryptoValue.isZero()) {
ImeAction.Done
} else {
ImeAction.None
}

View file

@ -1,46 +1,27 @@
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.ui.amountScreen.converters.AmountCurrencyTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
internal class SendAmountCurrencyConverter(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<Boolean, SendUiState> {
override fun convert(value: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountTextField = amountState.amountTextField
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return state
val isValidFiatRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero()
val isDoneActionEnabled = amountState.isPrimaryButtonEnabled
return if (amountTextField.isFiatValue == value && !isValidFiatRate) {
state
} else {
return state.copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
amountTextField = amountTextField.copy(
isFiatValue = value,
isValuePasted = true,
keyboardOptions = KeyboardOptions(
imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None,
keyboardType = KeyboardType.Number,
),
),
selectedButton = amountState.segmentedButtonConfig.indexOfFirst { it.isFiat == value },
),
)
}
return state.copyWrapped(
isEditState = isEditState,
amountState = AmountCurrencyTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState),
)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.features.send.impl.presentation.state.amount
import com.tangem.common.ui.amountScreen.converters.AmountPastedTriggerDismissTransformer
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
@ -9,17 +11,15 @@ internal class SendAmountPastedTriggerDismissConverter(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
) : Converter<Boolean, SendUiState> {
override fun convert(value: Boolean): SendUiState {
val state = currentStateProvider()
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountState = state.getAmountState(isEditState) as? AmountState.Data ?: return state
return state.copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
amountTextField = amountState.amountTextField.copy(
isValuePasted = false,
),
),
amountState = AmountPastedTriggerDismissTransformer().transform(amountState),
)
}
}

View file

@ -1,67 +1,29 @@
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.extensions.isZero
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import java.math.BigDecimal
internal class SendAmountReduceByConverter(
private val stateRouterProvider: Provider<StateRouter>,
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<SendAmountReduceByConverter.ReduceByData, SendUiState> {
override fun convert(value: ReduceByData): SendUiState {
) : Converter<AmountReduceByTransformer.ReduceByData, SendUiState> {
override fun convert(value: AmountReduceByTransformer.ReduceByData): SendUiState {
val state = currentStateProvider()
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountTextField = amountState.amountTextField
val cryptoDecimals = amountTextField.cryptoAmount.decimals
val fiatDecimals = amountTextField.fiatAmount.decimals
val amountValue = amountState.amountTextField.cryptoAmount.value ?: return state
val decimalCryptoValue = amountValue.minus(value.reduceAmountByDiff)
val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals)
val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue(
fiatRate = cryptoCurrencyStatus.value.fiatRate,
isFiatValue = false,
decimals = fiatDecimals,
)
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField)
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else decimalCryptoValue.isZero()
return state.copyWrapped(
isEditState = isEditState,
sendState = state.sendState?.copy(
reduceAmountBy = value.reduceAmountBy,
),
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
keyboardOptions = KeyboardOptions(
imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue),
keyboardType = KeyboardType.Number,
),
),
),
amountState = AmountReduceByTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState),
)
}
internal data class ReduceByData(
val reduceAmountBy: BigDecimal,
val reduceAmountByDiff: BigDecimal,
)
}

View file

@ -1,15 +1,11 @@
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.common.extensions.isZero
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.impl.presentation.state.SendUiState
import com.tangem.features.send.impl.presentation.state.StateRouter
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.isNullOrZero
import java.math.BigDecimal
internal class SendAmountReduceToConverter(
@ -17,41 +13,15 @@ internal class SendAmountReduceToConverter(
private val currentStateProvider: Provider<SendUiState>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
) : Converter<BigDecimal, SendUiState> {
override fun convert(value: BigDecimal): SendUiState {
val state = currentStateProvider()
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val isEditState = stateRouterProvider().isEditState
val amountState = state.getAmountState(isEditState) ?: return state
val amountTextField = amountState.amountTextField
val cryptoDecimals = amountTextField.cryptoAmount.decimals
val fiatDecimals = amountTextField.fiatAmount.decimals
val cryptoValue = value.parseBigDecimal(cryptoDecimals)
val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue(
fiatRate = cryptoCurrencyStatus.value.fiatRate,
isFiatValue = false,
decimals = fiatDecimals,
)
val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue
val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField)
val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isZero()
return state.copyWrapped(
isEditState = isEditState,
amountState = amountState.copy(
isPrimaryButtonEnabled = !isExceedBalance && !isZero,
amountTextField = amountTextField.copy(
value = cryptoValue,
fiatValue = fiatValue,
isError = isExceedBalance,
cryptoAmount = amountTextField.cryptoAmount.copy(value = value),
fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue),
keyboardOptions = KeyboardOptions(
imeAction = getKeyboardAction(isExceedBalance, value),
keyboardType = KeyboardType.Number,
),
),
),
amountState = AmountReduceToTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState),
)
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.features.send.impl.presentation.state.amount
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.extensions.TextReference
/**
* Segmented buttons config
*
* @param title button title
* @param iconState currency icon state
* @param iconUrl currency icon url
* @param isFiat is fiat currency
*/
@Immutable
internal data class SendAmountSegmentedButtonsConfig(
val title: TextReference,
val iconState: TokenIconState? = null,
val iconUrl: String? = null,
val isFiat: Boolean,
)

Some files were not shown because too many files have changed in this diff Show more