Updated on 2026-08-14
This commit is contained in:
commit
bc84d8f5f8
579 changed files with 13604 additions and 3422 deletions
|
|
@ -15,7 +15,5 @@ dependencies {
|
|||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
||||
/* AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
implementation(projects.core.ui)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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})"
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) },
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
1
features/disclaimer/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
15
features/disclaimer/api/build.gradle.kts
Normal file
15
features/disclaimer/api/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
1
features/disclaimer/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
48
features/disclaimer/impl/build.gradle.kts
Normal file
48
features/disclaimer/impl/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.features.disclaimer.impl.presentation.viewmodel
|
||||
|
||||
internal interface DisclaimerClickIntents {
|
||||
|
||||
fun onAccept(shouldAskPushPermission: Boolean)
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ private fun Content(state: ManageTokensState, onHeaderSizeChange: (Dp) -> Unit)
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
1
features/push-notifications/api/.gitignore
vendored
Normal file
1
features/push-notifications/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
15
features/push-notifications/api/build.gradle.kts
Normal file
15
features/push-notifications/api/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.features.pushnotifications.api.featuretoggles
|
||||
|
||||
/**
|
||||
* Push notifications feature toggles
|
||||
*/
|
||||
interface PushNotificationsFeatureToggles {
|
||||
/** Availability of push notifications */
|
||||
val isPushNotificationsEnabled: Boolean
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.pushnotifications.api.navigation
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
interface PushNotificationsRouter {
|
||||
|
||||
fun entryFragment(): Fragment
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
1
features/push-notifications/impl/.gitignore
vendored
Normal file
1
features/push-notifications/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
48
features/push-notifications/impl/build.gradle.kts
Normal file
48
features/push-notifications/impl/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.features.pushnotifications.impl.navigation
|
||||
|
||||
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
|
||||
|
||||
interface InnerPushNotificationsRouter : PushNotificationsRouter {
|
||||
|
||||
fun openHome()
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.pushnotifications.impl.presentation.viewmodel
|
||||
|
||||
internal interface PushNotificationsClickIntents {
|
||||
fun onAskLater()
|
||||
|
||||
fun onAllowPermission()
|
||||
|
||||
fun onAllowedPermission()
|
||||
|
||||
fun openSettings()
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue