Updated on 2026-08-14
This commit is contained in:
commit
fe2cfac54d
1140 changed files with 23493 additions and 8579 deletions
1
features/account/api/.gitignore
vendored
Normal file
1
features/account/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
19
features/account/api/build.gradle.kts
Normal file
19
features/account/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.account.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.features.account
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface AccountCreateEditComponent : ComposableContentComponent {
|
||||
interface Factory : ComponentFactory<Params, AccountCreateEditComponent>
|
||||
|
||||
sealed interface Params {
|
||||
|
||||
data class Create(
|
||||
val userWalletId: UserWalletId,
|
||||
) : Params
|
||||
|
||||
data class Edit(
|
||||
val account: Account,
|
||||
) : Params
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.account
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.Account
|
||||
|
||||
interface AccountDetailsComponent : ComposableContentComponent {
|
||||
interface Factory : ComponentFactory<Params, AccountDetailsComponent>
|
||||
|
||||
data class Params(val account: Account)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.features.account
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface ArchivedAccountListComponent : ComposableContentComponent {
|
||||
interface Factory : ComponentFactory<Params, ArchivedAccountListComponent>
|
||||
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
}
|
||||
1
features/account/impl/.gitignore
vendored
Normal file
1
features/account/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
61
features/account/impl/build.gradle.kts
Normal file
61
features/account/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.kotlin.serialization)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.account.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.account.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.error)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.datasource)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.account)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.core.ktx)
|
||||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/** Compose libraries */
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.animation)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
implementation(deps.androidx.activity.compose)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.arrow.core)
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.firebase.crashlytics)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.features.account.archived
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.features.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("UnusedPrivateMember") // todo account
|
||||
internal class ArchivedAccountListModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ArchivedAccountListComponent.Params>()
|
||||
|
||||
val uiState: StateFlow<AccountArchivedUM> get() = _uiState
|
||||
private val _uiState: MutableStateFlow<AccountArchivedUM> = MutableStateFlow(getInitialState())
|
||||
|
||||
private fun confirmRecoverDialog(accountId: AccountId) {
|
||||
val account: Account? = null // todo account find
|
||||
account ?: return
|
||||
val secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
)
|
||||
val firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.account_archived_recover),
|
||||
onClick = { recoverCryptoPortfolio(account.accountId) },
|
||||
)
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
title = stringReference(account.name.value),
|
||||
message = TextReference.EMPTY,
|
||||
firstActionBuilder = { firstAction },
|
||||
secondActionBuilder = { secondAction },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch {
|
||||
recoverCryptoPortfolioUseCase(accountId)
|
||||
}
|
||||
|
||||
private fun getInitialState(): AccountArchivedUM {
|
||||
return AccountArchivedUM.Loading(
|
||||
onCloseClick = { router.pop() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.account.archived
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
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.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.archived.ui.ArchivedAccountListContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultArchivedAccountListComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: ArchivedAccountListComponent.Params,
|
||||
) : AppComponentContext by appComponentContext, ArchivedAccountListComponent {
|
||||
|
||||
private val model: ArchivedAccountListModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
ArchivedAccountListContent(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
BackHandler(onBack = state.onCloseClick)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : ArchivedAccountListComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: ArchivedAccountListComponent.Params,
|
||||
): DefaultArchivedAccountListComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.account.archived.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.archived.ArchivedAccountListModel
|
||||
import com.tangem.features.account.archived.DefaultArchivedAccountListComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AccountArchivedModule {
|
||||
|
||||
@Binds
|
||||
fun bindArchivedAccountListComponentFactory(
|
||||
impl: DefaultArchivedAccountListComponent.Factory,
|
||||
): ArchivedAccountListComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(ArchivedAccountListModel::class)
|
||||
fun bindArchivedAccountListModel(model: ArchivedAccountListModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.account.archived.entity
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.account.common.CryptoPortfolioIconUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal sealed interface AccountArchivedUM {
|
||||
val onCloseClick: () -> Unit
|
||||
|
||||
data class Loading(override val onCloseClick: () -> Unit) : AccountArchivedUM
|
||||
data class Error(
|
||||
override val onCloseClick: () -> Unit,
|
||||
val onRetryClick: () -> Unit,
|
||||
) : AccountArchivedUM
|
||||
data class Content(
|
||||
override val onCloseClick: () -> Unit,
|
||||
val accounts: ImmutableList<ArchivedAccountUM>,
|
||||
) : AccountArchivedUM
|
||||
}
|
||||
|
||||
internal data class ArchivedAccountUM(
|
||||
val accountId: String,
|
||||
val accountName: String,
|
||||
val accountIcon: CryptoPortfolioIconUM,
|
||||
val tokensInfo: TextReference,
|
||||
val onClick: (accountId: String) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
package com.tangem.features.account.archived.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUM
|
||||
import com.tangem.features.account.archived.entity.ArchivedAccountUM
|
||||
import com.tangem.features.account.common.toUM
|
||||
import com.tangem.features.account.details.ui.AccountIcon
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun ArchivedAccountListContent(state: AccountArchivedUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
text = stringResourceSafe(R.string.account_archived_title),
|
||||
onBackClick = state.onCloseClick,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(1f),
|
||||
|
||||
) {
|
||||
when (state) {
|
||||
is AccountArchivedUM.Content -> ArchiveAccountContent(state)
|
||||
is AccountArchivedUM.Error -> ArchiveAccountError(state)
|
||||
is AccountArchivedUM.Loading -> ArchiveAccountLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveAccountLoading(modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
color = TangemTheme.colors.icon.primary1,
|
||||
modifier = Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveAccountError(state: AccountArchivedUM.Error, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
text = stringResourceSafe(R.string.common_unable_to_load),
|
||||
)
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.try_to_load_data_again_button_title),
|
||||
onClick = state.onRetryClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveAccountContent(state: AccountArchivedUM.Content, modifier: Modifier = Modifier) {
|
||||
LazyColumn(modifier = modifier) {
|
||||
itemsIndexed(
|
||||
items = state.accounts,
|
||||
key = { index, item -> item.accountId },
|
||||
) { index, account ->
|
||||
ArchivedAccountRow(
|
||||
item = account,
|
||||
modifier = Modifier.roundedShapeItemDecoration(
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
radius = TangemTheme.dimens.radius20,
|
||||
currentIndex = index,
|
||||
addDefaultPadding = true,
|
||||
lastIndex = state.accounts.lastIndex,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchivedAccountRow(item: ArchivedAccountUM, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = { item.onClick(item.accountId) })
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AccountIcon(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(9.dp)),
|
||||
accountName = item.accountName,
|
||||
accountIcon = item.accountIcon,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Text(
|
||||
text = item.accountName,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
text = item.tokensInfo.resolveReference(),
|
||||
)
|
||||
}
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.account_archived_recover),
|
||||
onClick = { item.onClick(item.accountId) },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountArchivedUM) {
|
||||
TangemThemePreview {
|
||||
ArchivedAccountListContent(state = params)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountArchivedUM>(
|
||||
buildList {
|
||||
fun portfolioIcon() = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
|
||||
|
||||
val firstList = List(10) {
|
||||
ArchivedAccountUM(
|
||||
accountId = it.toString(),
|
||||
accountName = "Account name",
|
||||
accountIcon = portfolioIcon(),
|
||||
tokensInfo = stringReference("10 tokens in 2 networks"),
|
||||
onClick = {},
|
||||
|
||||
)
|
||||
}.toImmutableList()
|
||||
val first = AccountArchivedUM.Content(
|
||||
onCloseClick = {},
|
||||
accounts = firstList,
|
||||
)
|
||||
add(first)
|
||||
add(AccountArchivedUM.Loading {})
|
||||
add(AccountArchivedUM.Error({}, {}))
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.account.common
|
||||
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon.Color
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon.Icon
|
||||
|
||||
data class CryptoPortfolioIconUM(
|
||||
val value: Icon,
|
||||
val color: Color,
|
||||
) {
|
||||
constructor(domainModel: CryptoPortfolioIcon) : this(
|
||||
value = domainModel.value,
|
||||
color = domainModel.color,
|
||||
)
|
||||
}
|
||||
|
||||
fun CryptoPortfolioIcon.toUM() = CryptoPortfolioIconUM(this)
|
||||
fun CryptoPortfolioIconUM.toDomain() = CryptoPortfolioIcon.ofCustomAccount(this.value, this.color)
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.tangem.features.account.createedit
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.utils.showErrorDialog
|
||||
import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase
|
||||
import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase
|
||||
import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.common.toDomain
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUM
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateButton
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateColorSelect
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateDerivationIndex
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateIconSelect
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName
|
||||
import com.tangem.features.account.createedit.error.AccountFeatureError
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class AccountCreateEditModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val updateCryptoPortfolioUseCase: UpdateCryptoPortfolioUseCase,
|
||||
private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase,
|
||||
private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AccountCreateEditComponent.Params>()
|
||||
private val umBuilder = AccountCreateEditUMBuilder(params)
|
||||
|
||||
val uiState: StateFlow<AccountCreateEditUM>
|
||||
field = MutableStateFlow(value = getInitialState())
|
||||
|
||||
init {
|
||||
if (params is AccountCreateEditComponent.Params.Create) {
|
||||
updateDerivationInfo(userWalletId = params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun unsaveChangeDialog() {
|
||||
val secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.account_unsaved_dialog_action_first),
|
||||
onClick = {},
|
||||
)
|
||||
val firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.account_unsaved_dialog_action_second),
|
||||
warning = true,
|
||||
onClick = { router.pop() },
|
||||
)
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.account_unsaved_dialog_title),
|
||||
message = resourceReference(R.string.account_unsaved_dialog_message_create),
|
||||
firstActionBuilder = { firstAction },
|
||||
secondActionBuilder = { secondAction },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onConfirmClick() = modelScope.launch {
|
||||
when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> createNewCryptoPortfolio(params)
|
||||
is AccountCreateEditComponent.Params.Edit -> editCryptoPortfolio(params)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) {
|
||||
val state = uiState.value
|
||||
val name = AccountName(value = state.account.name).getOrNull() ?: return
|
||||
val icon = state.account.portfolioIcon.toDomain()
|
||||
val index = state.account.derivationInfo.index ?: return
|
||||
val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return
|
||||
|
||||
addCryptoPortfolioUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
accountName = name,
|
||||
icon = icon,
|
||||
derivationIndex = derivationIndex,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun editCryptoPortfolio(params: AccountCreateEditComponent.Params.Edit) {
|
||||
val state = uiState.value
|
||||
val name = AccountName(state.account.name).getOrNull() ?: return
|
||||
val icon = state.account.portfolioIcon.toDomain()
|
||||
val isNewName = name != params.account.name
|
||||
val isNewIcon = icon != params.account.portfolioIcon
|
||||
updateCryptoPortfolioUseCase(
|
||||
icon = if (isNewIcon) icon else null,
|
||||
accountName = if (isNewName) name else null,
|
||||
accountId = params.account.accountId,
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCloseClick() = unsaveChangeDialog()
|
||||
|
||||
private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) {
|
||||
uiState.value = uiState.value
|
||||
.updateIconSelect(icon)
|
||||
.validateNewState()
|
||||
}
|
||||
|
||||
private fun onColorSelect(color: CryptoPortfolioIcon.Color) {
|
||||
uiState.value = uiState.value
|
||||
.updateColorSelect(color)
|
||||
.validateNewState()
|
||||
}
|
||||
|
||||
private fun onNameChange(name: String) {
|
||||
uiState.value = uiState.value
|
||||
.updateName(name)
|
||||
.validateNewState()
|
||||
}
|
||||
|
||||
private fun AccountCreateEditUM.validateNewState(): AccountCreateEditUM {
|
||||
val isValidName = AccountName(this.account.name).isRight()
|
||||
val isAvailableForConfirm = when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> isValidName
|
||||
is AccountCreateEditComponent.Params.Edit -> {
|
||||
val isNewName = this.account.name != params.account.name.value
|
||||
val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon
|
||||
isValidName && (isNewName || isNewIcon)
|
||||
}
|
||||
}
|
||||
return this.updateButton(isButtonEnabled = isAvailableForConfirm)
|
||||
}
|
||||
|
||||
private fun getInitialState(): AccountCreateEditUM {
|
||||
return AccountCreateEditUM(
|
||||
title = umBuilder.toolbarTitle,
|
||||
account = umBuilder.initAccountUM(::onNameChange),
|
||||
colorsState = umBuilder.initColorsUM(::onColorSelect),
|
||||
iconsState = umBuilder.initIconsUM(::onIconSelect),
|
||||
buttonState = umBuilder.initButtonUM(::onConfirmClick),
|
||||
onCloseClick = ::onCloseClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateDerivationInfo(userWalletId: UserWalletId) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
getUnoccupiedAccountIndexUseCase(userWalletId = userWalletId)
|
||||
.onRight { derivationIndex ->
|
||||
uiState.update {
|
||||
it.updateDerivationIndex(derivationIndex = derivationIndex.value)
|
||||
}
|
||||
}
|
||||
.onLeft {
|
||||
handleError(
|
||||
error = AccountFeatureError.CreateAccount.UnableToGetDerivationIndex,
|
||||
params = mapOf("userWalletId" to userWalletId.stringValue),
|
||||
)
|
||||
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(error: AccountFeatureError, params: Map<String, String> = mapOf()) {
|
||||
val exception = IllegalStateException(error.toString())
|
||||
|
||||
Timber.e(exception)
|
||||
|
||||
analyticsExceptionHandler.sendException(
|
||||
event = ExceptionAnalyticsEvent(exception = exception, params = params),
|
||||
)
|
||||
|
||||
messageSender.showErrorDialog(universalError = error, onDismiss = router::pop)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.account.createedit
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
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.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.createedit.ui.AccountCreateEditContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultAccountCreateEditComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: AccountCreateEditComponent.Params,
|
||||
) : AppComponentContext by appComponentContext, AccountCreateEditComponent {
|
||||
|
||||
private val model: AccountCreateEditModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
AccountCreateEditContent(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
BackHandler(onBack = state.onCloseClick)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AccountCreateEditComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: AccountCreateEditComponent.Params,
|
||||
): DefaultAccountCreateEditComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.account.createedit.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.createedit.AccountCreateEditModel
|
||||
import com.tangem.features.account.createedit.DefaultAccountCreateEditComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AccountCreateEditModule {
|
||||
|
||||
@Binds
|
||||
fun bindAccountCreateEditComponentFactory(
|
||||
impl: DefaultAccountCreateEditComponent.Factory,
|
||||
): AccountCreateEditComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AccountCreateEditModel::class)
|
||||
fun bindAccountCreateEditModel(model: AccountCreateEditModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.features.account.createedit.entity
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.features.account.common.CryptoPortfolioIconUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class AccountCreateEditUM(
|
||||
val title: TextReference,
|
||||
val account: Account,
|
||||
val colorsState: Colors,
|
||||
val iconsState: Icons,
|
||||
val buttonState: Button,
|
||||
val onCloseClick: () -> Unit,
|
||||
) {
|
||||
|
||||
data class Account(
|
||||
val name: String,
|
||||
val portfolioIcon: CryptoPortfolioIconUM,
|
||||
val derivationInfo: DerivationInfo,
|
||||
val inputPlaceholder: TextReference,
|
||||
val onNameChange: (String) -> Unit,
|
||||
)
|
||||
|
||||
sealed interface DerivationInfo {
|
||||
val text: TextReference
|
||||
val index: Int?
|
||||
|
||||
data class Content(override val text: TextReference, override val index: Int) : DerivationInfo
|
||||
|
||||
data object Empty : DerivationInfo {
|
||||
override val text: TextReference = TextReference.EMPTY
|
||||
override val index: Int? = null
|
||||
}
|
||||
}
|
||||
|
||||
data class Colors(
|
||||
val selected: CryptoPortfolioIcon.Color,
|
||||
val list: ImmutableList<CryptoPortfolioIcon.Color>,
|
||||
val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit,
|
||||
)
|
||||
|
||||
data class Icons(
|
||||
val selected: CryptoPortfolioIcon.Icon,
|
||||
val list: ImmutableList<CryptoPortfolioIcon.Icon>,
|
||||
val onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit,
|
||||
)
|
||||
|
||||
data class Button(
|
||||
val isButtonEnabled: Boolean,
|
||||
val onConfirmClick: () -> Unit,
|
||||
val text: TextReference,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
package com.tangem.features.account.createedit.entity
|
||||
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.common.toUM
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
internal class AccountCreateEditUMBuilder(
|
||||
private val params: AccountCreateEditComponent.Params,
|
||||
) {
|
||||
|
||||
private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList()
|
||||
private val accountIcons = CryptoPortfolioIcon.Icon.entries.toImmutableList()
|
||||
private val createIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
|
||||
|
||||
val toolbarTitle: TextReference
|
||||
get() = when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> resourceReference(R.string.account_form_title_create)
|
||||
is AccountCreateEditComponent.Params.Edit -> resourceReference(R.string.account_form_title_edit)
|
||||
}
|
||||
|
||||
fun initAccountUM(onNameChange: (String) -> Unit): AccountCreateEditUM.Account {
|
||||
return when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> AccountCreateEditUM.Account(
|
||||
name = "",
|
||||
portfolioIcon = createIcon,
|
||||
derivationInfo = AccountCreateEditUM.DerivationInfo.Empty,
|
||||
inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account),
|
||||
onNameChange = onNameChange,
|
||||
)
|
||||
is AccountCreateEditComponent.Params.Edit -> AccountCreateEditUM.Account(
|
||||
name = params.account.name.value,
|
||||
portfolioIcon = params.account.portfolioIcon.toUM(),
|
||||
derivationInfo = createAccountDerivationInfo(
|
||||
index = (params.account as Account.CryptoPortfolio).derivationIndex.value,
|
||||
),
|
||||
inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account),
|
||||
onNameChange = onNameChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun initColorsUM(onColorSelect: (CryptoPortfolioIcon.Color) -> Unit): AccountCreateEditUM.Colors {
|
||||
val selected: CryptoPortfolioIcon.Color = when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> createIcon.color
|
||||
is AccountCreateEditComponent.Params.Edit -> params.account.portfolioIcon.color
|
||||
}
|
||||
return AccountCreateEditUM.Colors(
|
||||
selected = selected,
|
||||
list = accountColors,
|
||||
onColorSelect = onColorSelect,
|
||||
)
|
||||
}
|
||||
|
||||
fun initIconsUM(onIconSelect: (CryptoPortfolioIcon.Icon) -> Unit): AccountCreateEditUM.Icons {
|
||||
val selected: CryptoPortfolioIcon.Icon = when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> createIcon.value
|
||||
is AccountCreateEditComponent.Params.Edit -> params.account.portfolioIcon.value
|
||||
}
|
||||
return AccountCreateEditUM.Icons(
|
||||
selected = selected,
|
||||
list = accountIcons,
|
||||
onIconSelect = onIconSelect,
|
||||
)
|
||||
}
|
||||
|
||||
fun initButtonUM(onConfirmClick: () -> Unit): AccountCreateEditUM.Button {
|
||||
val text: TextReference = when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> resourceReference(R.string.account_form_create_button)
|
||||
is AccountCreateEditComponent.Params.Edit -> resourceReference(R.string.account_form_edit_button)
|
||||
}
|
||||
return AccountCreateEditUM.Button(
|
||||
isButtonEnabled = false,
|
||||
onConfirmClick = onConfirmClick,
|
||||
text = text,
|
||||
)
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
|
||||
val Account.portfolioIcon: CryptoPortfolioIcon
|
||||
get() = when (this) {
|
||||
is Account.CryptoPortfolio -> this.icon
|
||||
}
|
||||
|
||||
fun AccountCreateEditUM.updateColorSelect(color: CryptoPortfolioIcon.Color): AccountCreateEditUM {
|
||||
val newIcon = this.account.portfolioIcon.copy(
|
||||
color = color,
|
||||
)
|
||||
return this.copy(
|
||||
account = this.account.copy(portfolioIcon = newIcon),
|
||||
colorsState = this.colorsState.copy(selected = color),
|
||||
)
|
||||
}
|
||||
|
||||
fun AccountCreateEditUM.updateIconSelect(icon: CryptoPortfolioIcon.Icon): AccountCreateEditUM {
|
||||
val newIcon = this.account.portfolioIcon.copy(
|
||||
value = icon,
|
||||
)
|
||||
return this.copy(
|
||||
account = this.account.copy(portfolioIcon = newIcon),
|
||||
iconsState = this.iconsState.copy(selected = icon),
|
||||
)
|
||||
}
|
||||
|
||||
fun AccountCreateEditUM.updateName(name: String): AccountCreateEditUM {
|
||||
return this.copy(account = this.account.copy(name = name))
|
||||
}
|
||||
|
||||
fun AccountCreateEditUM.updateButton(isButtonEnabled: Boolean): AccountCreateEditUM {
|
||||
return this.copy(buttonState = this.buttonState.copy(isButtonEnabled = isButtonEnabled))
|
||||
}
|
||||
|
||||
fun AccountCreateEditUM.updateDerivationIndex(derivationIndex: Int): AccountCreateEditUM {
|
||||
return this.copy(
|
||||
account = this.account.copy(
|
||||
derivationInfo = createAccountDerivationInfo(index = derivationIndex),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createAccountDerivationInfo(index: Int): AccountCreateEditUM.DerivationInfo {
|
||||
val derivationIndexText = if (index.toString().length == 1) "0$index" else "$index"
|
||||
|
||||
return AccountCreateEditUM.DerivationInfo.Content(
|
||||
text = resourceReference(
|
||||
id = R.string.account_form_account_index,
|
||||
formatArgs = wrappedList(derivationIndexText),
|
||||
),
|
||||
index = index,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.features.account.createedit.error
|
||||
|
||||
import com.tangem.core.error.UniversalError
|
||||
|
||||
sealed interface AccountFeatureError : UniversalError {
|
||||
|
||||
val subsystemCode: String
|
||||
val specificErrorCode: String
|
||||
|
||||
override val errorCode: Int
|
||||
get() = "108$subsystemCode$specificErrorCode".toInt()
|
||||
|
||||
sealed interface CreateAccount : AccountFeatureError {
|
||||
|
||||
override val subsystemCode: String get() = "001"
|
||||
|
||||
data object UnableToGetDerivationIndex : CreateAccount {
|
||||
override val specificErrorCode: String = "001"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface EditAccount : AccountFeatureError {
|
||||
|
||||
override val subsystemCode: String get() = "002"
|
||||
|
||||
data object RequiredCryptoPortfolio : EditAccount {
|
||||
override val specificErrorCode: String = "001"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,366 @@
|
|||
package com.tangem.features.account.createedit.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.account.getResId
|
||||
import com.tangem.common.ui.account.getUiColor
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.fields.AutoSizeTextField
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.features.account.common.toUM
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUM
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.tertiary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
text = state.title.resolveReference(),
|
||||
onBackClick = state.onCloseClick,
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.weight(1f),
|
||||
|
||||
) {
|
||||
AccountSummary(state.account)
|
||||
SpacerH24()
|
||||
AccountColor(state.colorsState)
|
||||
SpacerH24()
|
||||
AccountIcon(state.iconsState)
|
||||
SpacerH8()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
text = state.account.derivationInfo.text.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
enabled = state.buttonState.isButtonEnabled,
|
||||
text = state.buttonState.text.resolveReference(),
|
||||
onClick = state.buttonState.onConfirmClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountSummary(account: Account) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.action),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
AccountIcon(account)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.account_form_name),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
AutoSizeTextField(
|
||||
centered = true,
|
||||
textStyle = TangemTheme.typography.head,
|
||||
placeholder = account.inputPlaceholder,
|
||||
value = account.name,
|
||||
singleLine = true,
|
||||
onValueChange = account.onNameChange,
|
||||
)
|
||||
SpacerH(20.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountIcon(account: Account) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(88.dp)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius24))
|
||||
.background(account.portfolioIcon.color.getUiColor()),
|
||||
) {
|
||||
val icon = account.portfolioIcon.value
|
||||
val letter = account.name.firstOrNull()
|
||||
?: account.inputPlaceholder.resolveReference().first()
|
||||
when {
|
||||
icon == CryptoPortfolioIcon.Icon.Letter -> Text(
|
||||
text = letter.uppercase(),
|
||||
style = TangemTheme.typography.head,
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
)
|
||||
else -> Icon(
|
||||
modifier = Modifier.size(44.dp),
|
||||
tint = TangemTheme.colors.text.constantWhite,
|
||||
imageVector = ImageVector.vectorResource(id = icon.getResId()),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
private fun AccountColor(colorsState: AccountCreateEditUM.Colors) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.action),
|
||||
) {
|
||||
val columns = GridCells.Fixed(6)
|
||||
val contentPadding = PaddingValues(horizontal = 8.dp, vertical = 12.dp)
|
||||
LazyVerticalGrid(
|
||||
columns = columns,
|
||||
contentPadding = contentPadding,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
itemsIndexed(colorsState.list) { index, color ->
|
||||
val isSelected = color == colorsState.selected
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.clickable(onClick = { colorsState.onColorSelect(color) })
|
||||
.size(48.dp),
|
||||
) {
|
||||
if (isSelected) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(47.dp)
|
||||
.border(2.dp, color.getUiColor(), shape = CircleShape),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(color = color.getUiColor(), shape = CircleShape),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(color = color.getUiColor(), shape = CircleShape),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.fillMaxWidth()
|
||||
.background(TangemTheme.colors.background.action)
|
||||
.padding(8.dp),
|
||||
) {
|
||||
val columns = GridCells.Fixed(6)
|
||||
LazyVerticalGrid(
|
||||
columns = columns,
|
||||
) {
|
||||
itemsIndexed(iconsState.list) { index, icon ->
|
||||
val isSelected = icon == iconsState.selected
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.clickable(onClick = { iconsState.onIconSelect(icon) })
|
||||
.size(52.dp),
|
||||
) {
|
||||
if (isSelected) {
|
||||
val borderColor: Color
|
||||
val iconTint: Color
|
||||
val backgroundTint: Color
|
||||
if (index == 0) {
|
||||
borderColor = TangemTheme.colors.icon.accent
|
||||
iconTint = TangemTheme.colors.icon.accent
|
||||
backgroundTint = TangemTheme.colors.icon.accent.copy(alpha = 0.1f)
|
||||
} else {
|
||||
borderColor = TangemTheme.colors.icon.informative
|
||||
iconTint = TangemTheme.colors.icon.secondary
|
||||
backgroundTint = TangemTheme.colors.field.focused
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
.border(2.dp, borderColor, shape = CircleShape),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.background(color = backgroundTint, shape = CircleShape),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = icon.getResId()),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val iconTint: Color
|
||||
val backgroundTint: Color
|
||||
if (index == 0) {
|
||||
iconTint = TangemTheme.colors.icon.accent
|
||||
backgroundTint = TangemTheme.colors.icon.accent.copy(alpha = 0.1f)
|
||||
} else {
|
||||
iconTint = TangemTheme.colors.text.tertiary
|
||||
backgroundTint = TangemTheme.colors.field.focused
|
||||
}
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(color = backgroundTint, shape = CircleShape),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = icon.getResId()),
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountCreateEditUM) {
|
||||
TangemThemePreview {
|
||||
AccountCreateEditContent(state = params)
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountCreateEditUM>(
|
||||
buildList {
|
||||
val colors = CryptoPortfolioIcon.Color.entries.toImmutableList()
|
||||
val icons = CryptoPortfolioIcon.Icon.entries.toImmutableList()
|
||||
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
|
||||
val first = AccountCreateEditUM(
|
||||
title = stringReference("Add account"),
|
||||
onCloseClick = {},
|
||||
account = Account(
|
||||
name = "",
|
||||
portfolioIcon = portfolioIcon,
|
||||
inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account),
|
||||
onNameChange = {},
|
||||
derivationInfo = AccountCreateEditUM.DerivationInfo.Content(
|
||||
text = resourceReference(id = R.string.account_form_account_index, formatArgs = wrappedList(1)),
|
||||
index = 1,
|
||||
),
|
||||
),
|
||||
colorsState = AccountCreateEditUM.Colors(
|
||||
selected = portfolioIcon.color,
|
||||
onColorSelect = {},
|
||||
list = colors.toImmutableList(),
|
||||
),
|
||||
iconsState = AccountCreateEditUM.Icons(
|
||||
selected = portfolioIcon.value,
|
||||
onIconSelect = {},
|
||||
list = icons.toImmutableList(),
|
||||
),
|
||||
buttonState = AccountCreateEditUM.Button(
|
||||
isButtonEnabled = false,
|
||||
onConfirmClick = {},
|
||||
text = stringReference("Add account"),
|
||||
),
|
||||
)
|
||||
add(first)
|
||||
|
||||
portfolioIcon = portfolioIcon.copy(
|
||||
value = CryptoPortfolioIcon.Icon.Letter,
|
||||
color = CryptoPortfolioIcon.Color.entries.random(),
|
||||
)
|
||||
val second = AccountCreateEditUM(
|
||||
title = stringReference("Edit account"),
|
||||
onCloseClick = {},
|
||||
account = Account(
|
||||
portfolioIcon = portfolioIcon,
|
||||
name = "Main account",
|
||||
inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account),
|
||||
onNameChange = {},
|
||||
derivationInfo = AccountCreateEditUM.DerivationInfo.Content(
|
||||
text = resourceReference(id = R.string.account_form_account_index, formatArgs = wrappedList(1)),
|
||||
index = 1,
|
||||
),
|
||||
),
|
||||
colorsState = AccountCreateEditUM.Colors(
|
||||
selected = portfolioIcon.color,
|
||||
onColorSelect = {},
|
||||
list = colors.toImmutableList(),
|
||||
),
|
||||
iconsState = AccountCreateEditUM.Icons(
|
||||
selected = portfolioIcon.value,
|
||||
onIconSelect = {},
|
||||
list = icons.toImmutableList(),
|
||||
),
|
||||
buttonState = AccountCreateEditUM.Button(
|
||||
isButtonEnabled = false,
|
||||
onConfirmClick = {},
|
||||
text = stringReference("Save"),
|
||||
),
|
||||
)
|
||||
add(second)
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.features.account.details
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
import com.tangem.features.account.common.toUM
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
|
||||
import com.tangem.features.account.details.entity.AccountDetailsUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class AccountDetailsModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AccountDetailsComponent.Params>()
|
||||
|
||||
val uiState: StateFlow<AccountDetailsUM> get() = _uiState
|
||||
private val _uiState: MutableStateFlow<AccountDetailsUM> = MutableStateFlow(getInitialState())
|
||||
|
||||
private fun onEditAccountClick() {
|
||||
router.push(AppRoute.EditAccount(params.account))
|
||||
}
|
||||
|
||||
private fun onManageTokensClick() {
|
||||
// todo account add account param
|
||||
router.push(AppRoute.ManageTokens(source = AppRoute.ManageTokens.Source.SETTINGS))
|
||||
}
|
||||
|
||||
private fun onArchiveAccountClick() {
|
||||
confirmArchiveDialog()
|
||||
}
|
||||
|
||||
private fun confirmArchiveDialog() {
|
||||
val secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
)
|
||||
val firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.account_details_archive_action),
|
||||
warning = true,
|
||||
onClick = ::archiveCryptoPortfolio,
|
||||
)
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.account_details_archive),
|
||||
message = resourceReference(R.string.account_details_archive_description),
|
||||
firstActionBuilder = { firstAction },
|
||||
secondActionBuilder = { secondAction },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun archiveCryptoPortfolio() = modelScope.launch {
|
||||
archiveCryptoPortfolioUseCase(params.account.accountId)
|
||||
}
|
||||
|
||||
private fun getInitialState(): AccountDetailsUM {
|
||||
return AccountDetailsUM(
|
||||
accountName = params.account.name.value,
|
||||
accountIcon = params.account.portfolioIcon.toUM(),
|
||||
onCloseClick = { router.pop() },
|
||||
onAccountEditClick = ::onEditAccountClick,
|
||||
onManageTokensClick = ::onManageTokensClick,
|
||||
onArchiveAccountClick = ::onArchiveAccountClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.account.details
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
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.account.AccountDetailsComponent
|
||||
import com.tangem.features.account.details.ui.AccountDetailsContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultAccountDetailsComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: AccountDetailsComponent.Params,
|
||||
) : AppComponentContext by appComponentContext, AccountDetailsComponent {
|
||||
|
||||
private val model: AccountDetailsModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
AccountDetailsContent(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
BackHandler(onBack = state.onCloseClick)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AccountDetailsComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: AccountDetailsComponent.Params,
|
||||
): DefaultAccountDetailsComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.features.account.details.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
import com.tangem.features.account.details.AccountDetailsModel
|
||||
import com.tangem.features.account.details.DefaultAccountDetailsComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AccountDetailsModule {
|
||||
|
||||
@Binds
|
||||
fun bindAccountDetailsComponentFactory(
|
||||
impl: DefaultAccountDetailsComponent.Factory,
|
||||
): AccountDetailsComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AccountDetailsModel::class)
|
||||
fun bindAccountDetailsModel(model: AccountDetailsModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.features.account.details.entity
|
||||
|
||||
import com.tangem.features.account.common.CryptoPortfolioIconUM
|
||||
|
||||
data class AccountDetailsUM(
|
||||
val accountName: String,
|
||||
val accountIcon: CryptoPortfolioIconUM,
|
||||
val onCloseClick: () -> Unit,
|
||||
val onAccountEditClick: () -> Unit,
|
||||
val onManageTokensClick: () -> Unit,
|
||||
val onArchiveAccountClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
package com.tangem.features.account.details.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.account.getResId
|
||||
import com.tangem.common.ui.account.getUiColor
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.fields.AutoSizeTextField
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.features.account.common.CryptoPortfolioIconUM
|
||||
import com.tangem.features.account.common.toUM
|
||||
import com.tangem.features.account.details.entity.AccountDetailsUM
|
||||
|
||||
@Composable
|
||||
internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.secondary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
AppBarWithBackButton(
|
||||
onBackClick = state.onCloseClick,
|
||||
modifier = Modifier.height(TangemTheme.dimens.size56),
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.weight(1f),
|
||||
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing12),
|
||||
text = stringResourceSafe(R.string.account_details_title),
|
||||
style = TangemTheme.typography.h1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
SpacerH16()
|
||||
AccountRow(state)
|
||||
SpacerH16()
|
||||
ManageTokensRow(state)
|
||||
SpacerH16()
|
||||
ArchiveAccountRow(state)
|
||||
SpacerH(8.dp)
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
text = stringResourceSafe(R.string.account_details_archive_description),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ArchiveAccountRow(state: AccountDetailsUM) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.clickable(onClick = state.onArchiveAccountClick)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Icon(
|
||||
tint = TangemTheme.colors.icon.warning,
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_archive_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.account_details_archive),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ManageTokensRow(state: AccountDetailsUM) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.clickable(onClick = state.onManageTokensClick)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(id = R.drawable.ic_group_24),
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.main_manage_tokens),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountRow(state: AccountDetailsUM) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius12))
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.clickable(onClick = state.onAccountEditClick)
|
||||
.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AccountIcon(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(9.dp)),
|
||||
accountName = state.accountName,
|
||||
accountIcon = state.accountIcon,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.account_form_name),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
AutoSizeTextField(
|
||||
textStyle = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
value = state.accountName,
|
||||
singleLine = true,
|
||||
readOnly = true,
|
||||
onValueChange = {},
|
||||
)
|
||||
}
|
||||
|
||||
SecondarySmallButton(
|
||||
config = SmallButtonConfig(
|
||||
text = resourceReference(R.string.common_edit),
|
||||
onClick = state.onAccountEditClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// todo account make reusable
|
||||
@Composable
|
||||
internal fun AccountIcon(accountName: String, accountIcon: CryptoPortfolioIconUM, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.background(accountIcon.color.getUiColor()),
|
||||
) {
|
||||
val icon = accountIcon.value
|
||||
val letter = accountName.first()
|
||||
when {
|
||||
icon == CryptoPortfolioIcon.Icon.Letter -> Text(
|
||||
text = letter.uppercase(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.constantWhite,
|
||||
)
|
||||
else -> Icon(
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = TangemTheme.colors.text.constantWhite,
|
||||
imageVector = ImageVector.vectorResource(id = icon.getResId()),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::class) params: AccountDetailsUM) {
|
||||
TangemThemePreview {
|
||||
AccountDetailsContent(state = params)
|
||||
}
|
||||
}
|
||||
|
||||
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountDetailsUM>(
|
||||
buildList {
|
||||
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
|
||||
val first = AccountDetailsUM(
|
||||
onCloseClick = {},
|
||||
onAccountEditClick = {},
|
||||
onManageTokensClick = {},
|
||||
onArchiveAccountClick = {},
|
||||
accountName = "Main",
|
||||
accountIcon = portfolioIcon,
|
||||
)
|
||||
add(first)
|
||||
portfolioIcon = portfolioIcon.copy(
|
||||
value = CryptoPortfolioIcon.Icon.Letter,
|
||||
color = CryptoPortfolioIcon.Color.entries.random(),
|
||||
)
|
||||
add(first.copy(accountIcon = portfolioIcon))
|
||||
},
|
||||
)
|
||||
|
|
@ -13,6 +13,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
api(projects.features.biometry.api)
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.ui)
|
||||
|
|
|
|||
|
|
@ -13,13 +13,15 @@ import com.tangem.core.ui.message.DialogMessage
|
|||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
|
||||
import com.tangem.features.biometry.AskBiometryComponent
|
||||
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
|
|
@ -40,11 +42,13 @@ internal class AskBiometryModel @Inject constructor(
|
|||
private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AskBiometryComponent.Params>()
|
||||
|
|
@ -87,7 +91,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
|
||||
* because it will be automatically saved on UserWalletsListManager switch
|
||||
*/
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync ?: run {
|
||||
val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run {
|
||||
Timber.e("Unable to save user wallet")
|
||||
uiMessageSender.send(
|
||||
SnackbarMessage(stringReference("No selected user wallet")),
|
||||
|
|
@ -109,10 +113,18 @@ internal class AskBiometryModel @Inject constructor(
|
|||
walletsRepository.saveShouldSaveUserWallets(item = true)
|
||||
settingsRepository.setShouldSaveAccessCodes(value = true)
|
||||
|
||||
if (userWallet is UserWallet.Cold) {
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
walletsRepository.setUseBiometricAuthentication(value = true)
|
||||
setBiometryLockForAllWallets()
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(
|
||||
isBiometricsRequestPolicy = userWallet.hasAccessCode,
|
||||
isBiometricsRequestPolicy = walletsRepository.requireAccessCode().not(),
|
||||
)
|
||||
} else {
|
||||
if (userWallet is UserWallet.Cold) {
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(
|
||||
isBiometricsRequestPolicy = userWallet.hasAccessCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (_uiState.value.bottomSheetVariant) {
|
||||
|
|
@ -123,6 +135,18 @@ internal class AskBiometryModel @Inject constructor(
|
|||
params.modelCallbacks.onAllowed()
|
||||
}
|
||||
|
||||
private fun setBiometryLockForAllWallets() {
|
||||
modelScope.launch {
|
||||
userWalletsListRepository.userWalletsSync().forEach { userWallet ->
|
||||
userWalletsListRepository.setLock(
|
||||
userWalletId = userWallet.walletId,
|
||||
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
|
||||
changeUnsecured = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEnrollBiometricsDialog() {
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ dependencies {
|
|||
/** Hot Wallet Feature */
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
implementation(projects.core.analytics)
|
||||
|
|
@ -45,7 +51,6 @@ dependencies {
|
|||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/** Compose libraries */
|
||||
implementation(deps.compose.material)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.animation)
|
||||
implementation(deps.compose.foundation)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,63 @@
|
|||
package com.tangem.features.createwalletselection
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
|
||||
import com.tangem.domain.card.analytics.Shop
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.core.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class CreateWalletSelectionModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
internal val uiState: StateFlow<CreateWalletSelectionUM>
|
||||
|
|
@ -31,10 +75,110 @@ internal class CreateWalletSelectionModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
// TODO open card order web page
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
// TODO open card scanning
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
|
||||
scanCard()
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
modelScope.launch {
|
||||
setLoading(true)
|
||||
|
||||
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(
|
||||
isBiometricsRequestPolicy = shouldSaveAccessCodes,
|
||||
)
|
||||
|
||||
val analyticsSource = AnalyticsParam.ScreensSources.Intro
|
||||
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = analyticsSource,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (!showProgress) {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
} else {
|
||||
setLoading(true)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
handleScanError(error)
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
},
|
||||
onSuccess = { scanResponse ->
|
||||
proceedWithScanResponse(scanResponse)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
|
||||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("User wallet not created")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
saveWalletUseCase(userWallet).fold(
|
||||
ifLeft = {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
when (it) {
|
||||
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
|
||||
is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
walletsCount = userWalletsListManager.walletsCount.toString(),
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
uiState.update { it.copy(isScanInProgress = isLoading) }
|
||||
}
|
||||
|
||||
fun handleScanError(error: TangemError) {
|
||||
when (error) {
|
||||
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
|
||||
is TangemSdkError -> Timber.e(error, "Scan error occurred")
|
||||
else -> Timber.e(error, "Error happened")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNfcFeatureUnavailable() {
|
||||
uiMessageSender.send(
|
||||
message = DialogMessage(
|
||||
message = resourceReference(R.string.nfc_error_unavailable),
|
||||
title = resourceReference(id = R.string.common_error),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,12 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -18,6 +20,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButton
|
|||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -179,6 +182,9 @@ private fun AlreadyHaveTangemWalletBlock(
|
|||
isScanInProgress: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var buttonWidth by remember { mutableStateOf(0) }
|
||||
val density = LocalDensity.current
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -201,9 +207,17 @@ private fun AlreadyHaveTangemWalletBlock(
|
|||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
TangemButton(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth(),
|
||||
.conditional(buttonWidth > 0) {
|
||||
width(with(density) { buttonWidth.toDp() })
|
||||
}
|
||||
.onGloballyPositioned { coordinates ->
|
||||
if (buttonWidth == 0) {
|
||||
buttonWidth = coordinates.size.width
|
||||
}
|
||||
},
|
||||
text = stringResourceSafe(R.string.wallet_create_scan_title),
|
||||
onClick = onScanClick,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
|
|
|
|||
|
|
@ -118,10 +118,14 @@ internal class DetailsModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
val userWallets = getWalletsUseCase.invokeSync()
|
||||
|
||||
val scanResponse =
|
||||
getSelectedWalletSyncUseCase().getOrNull()?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY]
|
||||
?: error("Selected wallet is null")
|
||||
val selectedUserWallet = getSelectedWalletSyncUseCase().getOrNull()
|
||||
?: error("Selected wallet is null")
|
||||
|
||||
if (selectedUserWallet is UserWallet.Hot) {
|
||||
return@launch // TODO [REDACTED_TASK_KEY] [Hot Wallet] Send feedback
|
||||
}
|
||||
|
||||
val scanResponse = selectedUserWallet.requireColdWallet().scanResponse
|
||||
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return@launch
|
||||
|
||||
val feedbackType = when {
|
||||
|
|
@ -140,11 +144,13 @@ internal class DetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun openUseDesk() {
|
||||
val scanResponse =
|
||||
getSelectedWalletSyncUseCase().getOrNull()?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY]
|
||||
?: error("Selected wallet is null")
|
||||
val userWallet = getSelectedWalletSyncUseCase().getOrNull() ?: error("Selected wallet is null")
|
||||
|
||||
val cardInfo = getCardInfoUseCase(scanResponse).getOrNull() ?: return
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return // TODO [REDACTED_TASK_KEY] [Hot Wallet] UseDesk
|
||||
}
|
||||
|
||||
val cardInfo = getCardInfoUseCase(userWallet.requireColdWallet().scanResponse).getOrNull() ?: return
|
||||
|
||||
router.push(AppRoute.Usedesk(cardInfo))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
private val userWalletsFetcher = userWalletsFetcherFactory.create(
|
||||
messageSender = messageSender,
|
||||
onlyMultiCurrency = false,
|
||||
authMode = false,
|
||||
onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) },
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ 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.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.models.SaveWalletError
|
||||
import com.tangem.domain.core.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ dependencies {
|
|||
implementation(deps.compose.accompanist.permission)
|
||||
implementation(deps.compose.accompanist.webView)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.material)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.ui)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ internal class DisclaimerModel @Inject constructor(
|
|||
} else {
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
router.replaceAll(AppRoute.Home)
|
||||
router.replaceAll(AppRoute.Home())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.google.accompanist.web.WebView
|
||||
import com.google.accompanist.web.WebViewNavigator
|
||||
|
|
@ -62,9 +64,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
|
|||
) {
|
||||
TangemTopAppBar(
|
||||
title = resourceReference(R.string.disclaimer_title),
|
||||
startButton = TopAppBarButtonUM(
|
||||
iconRes = R.drawable.ic_back_24,
|
||||
onIconClicked = state.popBack,
|
||||
startButton = TopAppBarButtonUM.Back(
|
||||
onBackClicked = state.popBack,
|
||||
).takeIf { state.isTosAccepted },
|
||||
titleAlignment = Alignment.CenterHorizontally,
|
||||
textColor = textColor,
|
||||
|
|
@ -108,7 +109,11 @@ private fun DisclaimerContent(url: String) {
|
|||
state = webViewState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.testTag(DisclaimerScreenTestTags.WEB_VIEW)
|
||||
.semantics {
|
||||
contentDescription = "WebView URL: ${webViewState.content.getCurrentUrl() ?: url}"
|
||||
},
|
||||
captureBackPresses = false,
|
||||
navigator = webViewNavigator,
|
||||
onCreated = WebView::applySafeSettings,
|
||||
|
|
|
|||
1
features/home/api/.gitignore
vendored
Normal file
1
features/home/api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
17
features/home/api/build.gradle.kts
Normal file
17
features/home/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.features.home.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.routing)
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.features.home.api
|
||||
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface HomeComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val launchMode: InitScreenLaunchMode = InitScreenLaunchMode.Standard,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, HomeComponent> {
|
||||
override fun create(context: AppComponentContext, params: Params): HomeComponent
|
||||
}
|
||||
}
|
||||
1
features/home/impl/.gitignore
vendored
Normal file
1
features/home/impl/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
66
features/home/impl/build.gradle.kts
Normal file
66
features/home/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
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.home.impl"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Api */
|
||||
implementation(projects.features.home.api)
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.decompose)
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.routing)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.feedback.models)
|
||||
|
||||
/** AndroidX libraries */
|
||||
implementation(deps.androidx.activity.compose)
|
||||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/** Compose libraries */
|
||||
implementation(deps.compose.ui)
|
||||
implementation(deps.compose.ui.tooling)
|
||||
implementation(deps.compose.foundation)
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.animation)
|
||||
implementation(deps.compose.coil)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.card.android)
|
||||
implementation(tangemDeps.card.core)
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.timber)
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.tangem.features.home.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.home.api.HomeComponent
|
||||
import com.tangem.features.home.impl.model.HomeModel
|
||||
import com.tangem.features.home.impl.ui.Home
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class DefaultHomeComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: HomeComponent.Params,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : HomeComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: HomeModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
Home(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
isV2StoriesEnabled = hotWalletFeatureToggles.isHotWalletEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : HomeComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: HomeComponent.Params): DefaultHomeComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.home.impl.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.home.impl.DefaultHomeComponent
|
||||
import com.tangem.features.home.impl.model.HomeModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface ComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindComponent(factory: DefaultHomeComponent.Factory): HomeComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(HomeModel::class)
|
||||
fun provideModel(model: HomeModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
package com.tangem.features.home.impl.model
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
|
||||
import com.tangem.domain.card.analytics.Shop
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.core.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
import com.tangem.features.home.impl.ui.state.Stories
|
||||
import com.tangem.features.home.impl.ui.state.getRestrictedStories
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class HomeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val router: Router,
|
||||
private val appRouter: AppRouter,
|
||||
private val getUserCountryUseCase: GetUserCountryUseCase,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<HomeComponent.Params>()
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
HomeUM(
|
||||
scanInProgress = false,
|
||||
stories = getRestrictedStories().toImmutableList(),
|
||||
onScanClick = ::onScanClick,
|
||||
onShopClick = ::onShopClick,
|
||||
onSearchTokensClick = ::onSearchTokensClick,
|
||||
onCreateNewWalletClick = ::onCreateNewWalletClick,
|
||||
onAddExistingWalletClick = ::onAddExistingWalletClick,
|
||||
),
|
||||
)
|
||||
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(IntroductionProcess.ScreenOpened)
|
||||
observeUserCountryChanges()
|
||||
|
||||
when (params.launchMode) {
|
||||
InitScreenLaunchMode.Standard -> Unit
|
||||
InitScreenLaunchMode.WithCardScan -> scanCard()
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeUserCountryChanges() {
|
||||
getUserCountryUseCase.invoke()
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach { result ->
|
||||
val userCountry = result.getOrNull() ?: UserCountry.Other(Locale.getDefault().country)
|
||||
updateStoriesForCountry(userCountry)
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun updateStoriesForCountry(userCountry: UserCountry) {
|
||||
val stories = if (userCountry.needApplyFCARestrictions()) {
|
||||
getRestrictedStories()
|
||||
} else {
|
||||
Stories.entries
|
||||
}
|
||||
|
||||
_uiState.update {
|
||||
it.copy(stories = stories.toImmutableList())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
|
||||
scanCard()
|
||||
}
|
||||
|
||||
private fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchTokensClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList)
|
||||
router.push(AppRoute.ManageTokens(Source.STORIES))
|
||||
}
|
||||
|
||||
private fun onCreateNewWalletClick() {
|
||||
router.push(AppRoute.CreateWalletSelection)
|
||||
}
|
||||
|
||||
private fun onAddExistingWalletClick() {
|
||||
router.push(AppRoute.AddExistingWallet)
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
modelScope.launch {
|
||||
setLoading(true)
|
||||
|
||||
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(
|
||||
isBiometricsRequestPolicy = shouldSaveAccessCodes,
|
||||
)
|
||||
|
||||
val analyticsSource = AnalyticsParam.ScreensSources.Intro
|
||||
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = analyticsSource,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (!showProgress) {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
} else {
|
||||
setLoading(true)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
handleScanError(error)
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
},
|
||||
onSuccess = { scanResponse ->
|
||||
proceedWithScanResponse(scanResponse)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
|
||||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("User wallet not created")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
saveWalletUseCase(userWallet).fold(
|
||||
ifLeft = {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
when (it) {
|
||||
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
|
||||
is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
walletsCount = userWalletsListManager.walletsCount.toString(),
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
_uiState.update { it.copy(scanInProgress = isLoading) }
|
||||
}
|
||||
|
||||
fun handleScanError(error: TangemError) {
|
||||
when (error) {
|
||||
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
|
||||
is TangemSdkError -> Timber.e(error, "Scan error occurred")
|
||||
else -> Timber.e(error, "Error happened")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNfcFeatureUnavailable() {
|
||||
uiMessageSender.send(
|
||||
message = DialogMessage(
|
||||
message = resourceReference(R.string.nfc_error_unavailable),
|
||||
title = resourceReference(id = R.string.common_error),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package com.tangem.features.home.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect
|
||||
import com.tangem.features.home.impl.ui.compose.StoriesScreen
|
||||
import com.tangem.features.home.impl.ui.compose.StoriesScreenV2
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
|
||||
@Composable
|
||||
internal fun Home(state: HomeUM, isV2StoriesEnabled: Boolean, modifier: Modifier = Modifier) {
|
||||
SystemBarsIconsDisposable(darkIcons = false)
|
||||
|
||||
if (isV2StoriesEnabled) {
|
||||
StoriesScreenV2(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onCreateNewWalletButtonClick = state.onCreateNewWalletClick,
|
||||
onAddExistingWalletButtonClick = state.onAddExistingWalletClick,
|
||||
onScanButtonClick = state.onScanClick,
|
||||
)
|
||||
} else {
|
||||
StoriesScreen(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
onScanButtonClick = state.onScanClick,
|
||||
onShopButtonClick = state.onShopClick,
|
||||
onSearchTokensClick = state.onSearchTokensClick,
|
||||
)
|
||||
}
|
||||
|
||||
ChangeRootBackgroundColorEffect(TangemColorPalette.Black)
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
package com.tangem.features.home.impl.ui.compose
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.absoluteOffset
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.utils.AnimatedValue
|
||||
import com.tangem.core.ui.utils.toAnimatable
|
||||
|
||||
private const val SCALE_SWITCH_BARRIER = 1.15f
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun HorizontalSlidingImage(
|
||||
painter: Painter,
|
||||
paused: Boolean,
|
||||
duration: Int,
|
||||
itemSize: DpSize,
|
||||
startOffset: Float,
|
||||
targetOffset: Float,
|
||||
contentDescription: String,
|
||||
) {
|
||||
val translateX = AnimatedValue(startOffset * -1f, (startOffset + targetOffset) * -1f)
|
||||
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.requiredWidth(itemSize.width)
|
||||
.requiredHeight(itemSize.height)
|
||||
.graphicsLayer(
|
||||
translationX = translateX.toAnimatable(isPaused = paused, duration = duration).value,
|
||||
),
|
||||
alignment = Alignment.TopStart,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesTextAnimation(
|
||||
slideInDuration: Int = 500,
|
||||
slideInDelay: Int = 200,
|
||||
slideDistance: Dp = 60.dp,
|
||||
label: String = "",
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
val isLaunched = remember { mutableStateOf(false) }
|
||||
val transition = updateTransition(targetState = isLaunched.value, label = label)
|
||||
|
||||
val offsetY = transition.animateDp(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = slideInDuration,
|
||||
delayMillis = slideInDelay,
|
||||
easing = FastOutSlowInEasing,
|
||||
)
|
||||
},
|
||||
label = "Slide in",
|
||||
) { value -> if (value) 0.dp else slideDistance }
|
||||
|
||||
val alpha = transition.animateFloat(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = slideInDuration * 2,
|
||||
delayMillis = slideInDelay,
|
||||
easing = FastOutSlowInEasing,
|
||||
)
|
||||
},
|
||||
label = "Visibility",
|
||||
) { value -> if (value) 1f else 0f }
|
||||
|
||||
content(
|
||||
Modifier
|
||||
.absoluteOffset(y = offsetY.value)
|
||||
.alpha(alpha.value),
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) { isLaunched.value = true }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesBottomImageAnimation(
|
||||
initialScale: Float = 2.5f,
|
||||
secondStageScale: Float = SCALE_SWITCH_BARRIER,
|
||||
targetScale: Float = 1.0f,
|
||||
firstStepDuration: Int,
|
||||
totalDuration: Int,
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
val secondStepDuration = totalDuration - firstStepDuration
|
||||
|
||||
val isFirstStepLaunched = remember { mutableStateOf(false) }
|
||||
val isSecondStepLaunched = remember { mutableStateOf(false) }
|
||||
|
||||
val firstTransition = updateTransition(
|
||||
targetState = isFirstStepLaunched.value,
|
||||
label = "Image appearing",
|
||||
)
|
||||
val firstScaleStep = firstTransition.animateFloat(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = firstStepDuration,
|
||||
easing = FastOutLinearInEasing,
|
||||
)
|
||||
},
|
||||
label = "Appearing scale",
|
||||
) { value -> if (value) secondStageScale else initialScale }
|
||||
|
||||
val secondTransition = updateTransition(
|
||||
targetState = isSecondStepLaunched.value,
|
||||
label = "Image slow outgoing",
|
||||
)
|
||||
val secondScaleStep = secondTransition.animateFloat(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = secondStepDuration,
|
||||
easing = LinearEasing,
|
||||
)
|
||||
},
|
||||
label = "Outgoing scale",
|
||||
) { value -> if (value) targetScale else secondStageScale }
|
||||
|
||||
val fadeIn = firstTransition.animateFloat(
|
||||
transitionSpec = { tween(durationMillis = 400) },
|
||||
label = "Fade in on start",
|
||||
) { value -> if (value) 1f else 0f }
|
||||
|
||||
if (firstScaleStep.value == secondStageScale) {
|
||||
isSecondStepLaunched.value = true
|
||||
}
|
||||
|
||||
val modifier = if (!isSecondStepLaunched.value) {
|
||||
Modifier.scale(firstScaleStep.value)
|
||||
} else {
|
||||
Modifier.scale(secondScaleStep.value)
|
||||
}.alpha(fadeIn.value)
|
||||
|
||||
content(modifier)
|
||||
|
||||
LaunchedEffect(Unit) { isFirstStepLaunched.value = true }
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.features.home.impl.ui.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.features.home.impl.ui.compose.content.*
|
||||
import com.tangem.features.home.impl.ui.compose.views.HomeButtons
|
||||
import com.tangem.features.home.impl.ui.compose.views.SearchCurrenciesButton
|
||||
import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar
|
||||
import com.tangem.features.home.impl.ui.state.Stories
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
internal fun StoriesScreen(
|
||||
state: HomeUM,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onShopButtonClick: () -> Unit,
|
||||
onSearchTokensClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var currentStory by remember { mutableStateOf(state.firstStory) }
|
||||
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
|
||||
|
||||
val goToPreviousStory = remember(currentStory, currentStoryIndex) {
|
||||
{ currentStory = state.stories[max(0, currentStoryIndex - 1)] }
|
||||
}
|
||||
val goToNextStory = remember(currentStory, currentStoryIndex) {
|
||||
{
|
||||
currentStory = if (currentStoryIndex < state.stories.lastIndex) {
|
||||
state.stories[currentStoryIndex + 1]
|
||||
} else {
|
||||
state.firstStory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo refactor [REDACTED_TASK_KEY]
|
||||
StoriesScreenContent(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
|
||||
config = StoriesScreenContentConfig(
|
||||
storiesSize = state.stories.lastIndex,
|
||||
currentStoryIndex = currentStoryIndex,
|
||||
currentStory = currentStory,
|
||||
isScanInProgress = state.scanInProgress,
|
||||
onGoToPreviousStory = goToPreviousStory,
|
||||
onGoToNextStory = goToNextStory,
|
||||
onSearchTokensClick = onSearchTokensClick,
|
||||
onScanButtonClick = onScanButtonClick,
|
||||
onShopButtonClick = onShopButtonClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use StoriesContainer from core/ui")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) {
|
||||
var isPressed by remember { mutableStateOf(value = false) }
|
||||
|
||||
val isPaused = isPressed || config.isScanInProgress
|
||||
val currentStoryDuration = config.currentStory.duration
|
||||
|
||||
Box(
|
||||
modifier = modifier.background(Color(0xFF010101)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
isPressed = true
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < 200) config.onGoToPreviousStory()
|
||||
isPressed = false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
isPressed = true
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < 200) config.onGoToNextStory()
|
||||
isPressed = false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
StoriesProgressBar(
|
||||
steps = config.storiesSize,
|
||||
currentStep = config.currentStoryIndex,
|
||||
stepDuration = currentStoryDuration,
|
||||
paused = isPaused,
|
||||
onStepFinish = config.onGoToNextStory,
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_tangem_logo),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.height(TangemTheme.dimens.size18)
|
||||
.align(Alignment.Start),
|
||||
)
|
||||
when (config.currentStory) {
|
||||
Stories.TangemIntro -> FirstStoriesContent(
|
||||
isPaused = isPaused,
|
||||
duration = currentStoryDuration,
|
||||
)
|
||||
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
|
||||
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
|
||||
isPaused = isPaused,
|
||||
stepDuration = currentStoryDuration,
|
||||
)
|
||||
Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration)
|
||||
Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration)
|
||||
Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = TangemTheme.dimens.spacing16)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = config.currentStory == Stories.Currencies,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
SearchCurrenciesButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = config.onSearchTokensClick,
|
||||
)
|
||||
}
|
||||
|
||||
HomeButtons(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
btnScanStateInProgress = config.isScanInProgress,
|
||||
onScanButtonClick = config.onScanButtonClick,
|
||||
onShopButtonClick = config.onShopButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class StoriesScreenContentConfig(
|
||||
val storiesSize: Int,
|
||||
val currentStoryIndex: Int,
|
||||
val currentStory: Stories,
|
||||
val isScanInProgress: Boolean,
|
||||
val onGoToPreviousStory: () -> Unit = {},
|
||||
val onGoToNextStory: () -> Unit = {},
|
||||
val onSearchTokensClick: () -> Unit = {},
|
||||
val onScanButtonClick: () -> Unit = {},
|
||||
val onShopButtonClick: () -> Unit = {},
|
||||
)
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun StoriesScreenContentPreview(
|
||||
@PreviewParameter(StoriesScreenContentConfigProvider::class) config: StoriesScreenContentConfig,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
StoriesScreenContent(config = config)
|
||||
}
|
||||
}
|
||||
|
||||
private class StoriesScreenContentConfigProvider : CollectionPreviewParameterProvider<StoriesScreenContentConfig>(
|
||||
collection = listOf(
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 0,
|
||||
currentStory = Stories.TangemIntro,
|
||||
isScanInProgress = true,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 1,
|
||||
currentStory = Stories.RevolutionaryWallet,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 2,
|
||||
currentStory = Stories.UltraSecureBackup,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 3,
|
||||
currentStory = Stories.Currencies,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 4,
|
||||
currentStory = Stories.Web3,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 5,
|
||||
currentStory = Stories.WalletForEveryone,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.features.home.impl.ui.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.features.home.impl.ui.compose.content.*
|
||||
import com.tangem.features.home.impl.ui.compose.views.HomeButtonsV2
|
||||
import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar
|
||||
import com.tangem.features.home.impl.ui.state.Stories
|
||||
import kotlin.math.max
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.features.home.impl.ui.state.HomeUM
|
||||
|
||||
@Composable
|
||||
internal fun StoriesScreenV2(
|
||||
state: HomeUM,
|
||||
onCreateNewWalletButtonClick: () -> Unit,
|
||||
onAddExistingWalletButtonClick: () -> Unit,
|
||||
onScanButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var currentStory by remember { mutableStateOf(state.firstStory) }
|
||||
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
|
||||
|
||||
val goToPreviousStory = remember(currentStory, currentStoryIndex) {
|
||||
{ currentStory = state.stories[max(0, currentStoryIndex - 1)] }
|
||||
}
|
||||
val goToNextStory = remember(currentStory, currentStoryIndex) {
|
||||
{
|
||||
currentStory = if (currentStoryIndex < state.stories.lastIndex) {
|
||||
state.stories[currentStoryIndex + 1]
|
||||
} else {
|
||||
state.firstStory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo refactor [REDACTED_TASK_KEY]
|
||||
StoriesScreenContentV2(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
|
||||
config = StoriesScreenContentV2Config(
|
||||
storiesSize = state.stories.lastIndex,
|
||||
currentStoryIndex = currentStoryIndex,
|
||||
currentStory = currentStory,
|
||||
isScanInProgress = state.scanInProgress,
|
||||
onGoToPreviousStory = goToPreviousStory,
|
||||
onGoToNextStory = goToNextStory,
|
||||
onCreateNewWalletButtonClick = onCreateNewWalletButtonClick,
|
||||
onAddExistingWalletButtonClick = onAddExistingWalletButtonClick,
|
||||
onScanButtonClick = onScanButtonClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use StoriesContainer from core/ui")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun StoriesScreenContentV2(config: StoriesScreenContentV2Config, modifier: Modifier = Modifier) {
|
||||
var isPressed by remember { mutableStateOf(value = false) }
|
||||
|
||||
val isPaused = isPressed || config.isScanInProgress
|
||||
val currentStoryDuration = config.currentStory.duration
|
||||
|
||||
Box(
|
||||
modifier = modifier.background(Color(0xFF010101)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
isPressed = true
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < 200) config.onGoToPreviousStory()
|
||||
isPressed = false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
isPressed = true
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < 200) config.onGoToNextStory()
|
||||
isPressed = false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
StoriesProgressBar(
|
||||
steps = config.storiesSize,
|
||||
currentStep = config.currentStoryIndex,
|
||||
stepDuration = currentStoryDuration,
|
||||
paused = isPaused,
|
||||
onStepFinish = config.onGoToNextStory,
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_tangem_logo),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.height(TangemTheme.dimens.size18)
|
||||
.align(Alignment.Start),
|
||||
)
|
||||
when (config.currentStory) {
|
||||
Stories.TangemIntro -> FirstStoriesContent(
|
||||
isPaused = isPaused,
|
||||
duration = currentStoryDuration,
|
||||
)
|
||||
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
|
||||
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
|
||||
isPaused = isPaused,
|
||||
stepDuration = currentStoryDuration,
|
||||
)
|
||||
Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration)
|
||||
Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration)
|
||||
Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = TangemTheme.dimens.spacing16)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
HomeButtonsV2(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
btnScanStateInProgress = config.isScanInProgress,
|
||||
onScanButtonClick = config.onScanButtonClick,
|
||||
onCreateNewWalletButtonClick = config.onCreateNewWalletButtonClick,
|
||||
onAddExistingWalletButtonClick = config.onAddExistingWalletButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class StoriesScreenContentV2Config(
|
||||
val storiesSize: Int,
|
||||
val currentStoryIndex: Int,
|
||||
val currentStory: Stories,
|
||||
val isScanInProgress: Boolean,
|
||||
val onGoToPreviousStory: () -> Unit = {},
|
||||
val onGoToNextStory: () -> Unit = {},
|
||||
val onCreateNewWalletButtonClick: () -> Unit = {},
|
||||
val onAddExistingWalletButtonClick: () -> Unit = {},
|
||||
val onScanButtonClick: () -> Unit = {},
|
||||
)
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun StoriesScreenContentV2Preview(
|
||||
@PreviewParameter(StoriesScreenContentV2ConfigProvider::class) config: StoriesScreenContentV2Config,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
StoriesScreenContentV2(config = config)
|
||||
}
|
||||
}
|
||||
|
||||
private class StoriesScreenContentV2ConfigProvider : CollectionPreviewParameterProvider<StoriesScreenContentV2Config>(
|
||||
collection = listOf(
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 0,
|
||||
currentStory = Stories.TangemIntro,
|
||||
isScanInProgress = true,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 1,
|
||||
currentStory = Stories.RevolutionaryWallet,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 2,
|
||||
currentStory = Stories.UltraSecureBackup,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 3,
|
||||
currentStory = Stories.Currencies,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 4,
|
||||
currentStory = Stories.Web3,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 5,
|
||||
currentStory = Stories.WalletForEveryone,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
package com.tangem.features.home.impl.ui.compose.content
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.home.impl.ui.compose.StoriesBottomImageAnimation
|
||||
import com.tangem.features.home.impl.ui.compose.StoriesTextAnimation
|
||||
import com.tangem.core.ui.R
|
||||
|
||||
@Composable
|
||||
fun StoriesRevolutionaryWallet() {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_awe_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_awe_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH32()
|
||||
StoriesImage(
|
||||
modifier = Modifier,
|
||||
drawableResId = R.drawable.img_revolutionary_wallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_backup_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_backup_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH32()
|
||||
FloatingCardsContent(
|
||||
isPaused = isPaused,
|
||||
stepDuration = stepDuration,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_currencies_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_currencies_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH32()
|
||||
StoriesCurrenciesContent(paused = isPaused, duration = stepDuration)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_web3_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_web3_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH(TangemTheme.dimens.spacing70)
|
||||
StoriesWeb3Content(paused = isPaused, duration = stepDuration)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesWalletForEveryone(stepDuration: Int) {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_finish_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_finish_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH32()
|
||||
BoxWithGradient {
|
||||
StoriesBottomImageAnimation(
|
||||
initialScale = 2.6f,
|
||||
secondStageScale = 1.2f,
|
||||
targetScale = 1.1f,
|
||||
totalDuration = stepDuration,
|
||||
firstStepDuration = 500,
|
||||
) { modifier ->
|
||||
StoriesImage(
|
||||
modifier = modifier,
|
||||
drawableResId = R.drawable.img_tangem_for_everyone,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SplitContent(topContent: @Composable () -> Unit, bottomContent: @Composable () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
) {
|
||||
topContent()
|
||||
bottomContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopContent(titleText: String, subtitleText: String) {
|
||||
SpacerH(TangemTheme.dimens.spacing36)
|
||||
StoriesTitleText(
|
||||
text = titleText,
|
||||
)
|
||||
SpacerH16()
|
||||
StoriesSubtitleText(
|
||||
subtitleText = subtitleText,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun StoriesTitleText(text: String) {
|
||||
StoriesTextAnimation(
|
||||
slideInDuration = 500,
|
||||
slideInDelay = 150,
|
||||
) { modifier ->
|
||||
Text(
|
||||
modifier = modifier
|
||||
.padding(start = 40.dp, end = 40.dp),
|
||||
text = text,
|
||||
style = TangemTheme.typography.head,
|
||||
color = TangemColorPalette.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun StoriesSubtitleText(subtitleText: String) {
|
||||
StoriesTextAnimation(
|
||||
slideInDuration = 500,
|
||||
slideInDelay = 400,
|
||||
) { modifier ->
|
||||
Text(
|
||||
modifier = modifier
|
||||
.padding(start = 40.dp, end = 40.dp),
|
||||
text = subtitleText,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemColorPalette.Dark1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StoriesImage(@DrawableRes drawableResId: Int, modifier: Modifier = Modifier) {
|
||||
Image(
|
||||
painter = painterResource(id = drawableResId),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Inside,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RevolutionaryWalletPreview() {
|
||||
StoriesRevolutionaryWallet()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun UltraSecureBackupPreview() {
|
||||
StoriesUltraSecureBackup(
|
||||
false,
|
||||
6000,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun CurrenciesPreview() {
|
||||
StoriesCurrencies(false, 6000)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Web3Preview() {
|
||||
StoriesWeb3(false, 6000)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WalletForEveryonePreview() {
|
||||
StoriesWalletForEveryone(6000)
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
package com.tangem.features.home.impl.ui.compose.content
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.home.impl.ui.compose.HorizontalSlidingImage
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.utils.dpSize
|
||||
import com.tangem.core.ui.utils.toPx
|
||||
|
||||
@Composable
|
||||
fun StoriesCurrenciesContent(paused: Boolean, duration: Int) {
|
||||
val currencyDrawableList = remember {
|
||||
listOf(
|
||||
R.drawable.currency0,
|
||||
R.drawable.currency1,
|
||||
R.drawable.currency2,
|
||||
R.drawable.currency3,
|
||||
R.drawable.currency4,
|
||||
)
|
||||
}
|
||||
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||
val decreaseRate = remember { 1f / currencyDrawableList.size }
|
||||
val designItemHeight = remember { 82.dp }
|
||||
|
||||
BoxWithGradient {
|
||||
Column(modifier = Modifier.graphicsLayer(clip = false)) {
|
||||
currencyDrawableList.forEachIndexed { index, drawableResId ->
|
||||
val painter = painterResource(id = drawableResId)
|
||||
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
|
||||
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
|
||||
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
|
||||
|
||||
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.halfHeight()
|
||||
val animateFrom = chessOffset - moveItemToStartOfScreen
|
||||
val animateTo = 50.dp - 50.dp * index * decreaseRate
|
||||
|
||||
HorizontalSlidingImage(
|
||||
paused = paused,
|
||||
duration = duration,
|
||||
painter = painter,
|
||||
itemSize = scaledItemSize,
|
||||
startOffset = animateFrom.toPx(),
|
||||
targetOffset = animateTo.toPx(),
|
||||
contentDescription = "Currency row",
|
||||
)
|
||||
SpacerH12()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun StoriesWeb3Content(paused: Boolean, duration: Int) {
|
||||
val dappsItemList = remember {
|
||||
listOf(
|
||||
R.drawable.dapps1,
|
||||
R.drawable.dapps1,
|
||||
R.drawable.dapps2,
|
||||
R.drawable.dapps3,
|
||||
R.drawable.dapps4,
|
||||
R.drawable.dapps5,
|
||||
)
|
||||
}
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||
val decreaseRate = remember { 1f / dappsItemList.size }
|
||||
val designItemHeight = 75.dp
|
||||
|
||||
BoxWithGradient {
|
||||
Column(modifier = Modifier.graphicsLayer(clip = false)) {
|
||||
dappsItemList.forEachIndexed { index, drawableResId ->
|
||||
val painter = painterResource(id = drawableResId)
|
||||
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
|
||||
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
|
||||
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
|
||||
|
||||
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.width / 3
|
||||
val animateFrom = chessOffset - moveItemToStartOfScreen
|
||||
val animateTo = 70.dp - 70.dp * index * decreaseRate
|
||||
|
||||
HorizontalSlidingImage(
|
||||
paused = paused,
|
||||
duration = duration,
|
||||
painter = painter,
|
||||
itemSize = scaledItemSize,
|
||||
startOffset = animateFrom.toPx(),
|
||||
targetOffset = animateTo.toPx(),
|
||||
contentDescription = "Web3 row",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BoxWithGradient(content: @Composable () -> Unit) {
|
||||
val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
content()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size164 + bottomInsetsPx.dp)
|
||||
.background(BottomGradient),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scaleToDesignSize(itemSize: DpSize, designItemHeight: Dp): DpSize {
|
||||
val scaleRate = itemSize.height / designItemHeight
|
||||
return itemSize / scaleRate
|
||||
}
|
||||
|
||||
private val BottomGradient: Brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
TangemColorPalette.Black.copy(alpha = 0f),
|
||||
TangemColorPalette.Black.copy(alpha = 0.75f),
|
||||
TangemColorPalette.Black.copy(alpha = 0.95f),
|
||||
TangemColorPalette.Black,
|
||||
),
|
||||
)
|
||||
|
||||
fun DpSize.halfHeight(): Dp = this.height / 2
|
||||
|
||||
fun Int.isEven() = this and 1 == 0
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package com.tangem.features.home.impl.ui.compose.content
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.home.impl.ui.compose.StoriesTextAnimation
|
||||
import com.tangem.core.ui.R
|
||||
|
||||
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
|
||||
@Composable
|
||||
fun FirstStoriesContent(isPaused: Boolean, duration: Int) {
|
||||
val progress = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(isPaused) {
|
||||
if (isPaused) {
|
||||
progress.stop()
|
||||
} else {
|
||||
progress.animateTo(
|
||||
targetValue = 2f,
|
||||
animationSpec = tween(
|
||||
durationMillis = duration,
|
||||
easing = LinearEasing,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val style = TextStyle(
|
||||
fontSize = 46.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SpacerH(TangemTheme.dimens.spacing94)
|
||||
StoriesTextAnimation(
|
||||
slideInDuration = 500,
|
||||
slideInDelay = 150,
|
||||
) { modifier ->
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(R.string.story_meet_title),
|
||||
style = style,
|
||||
color = TangemColorPalette.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
SpacerH(TangemTheme.dimens.spacing46)
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
painter = painterResource(R.drawable.img_meet_tangem),
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = "Tangem Wallet card",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun FirstStoriesPreview() {
|
||||
FirstStoriesContent(
|
||||
false,
|
||||
8000,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.features.home.impl.ui.compose.content
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.utils.AnimatedValue
|
||||
import com.tangem.core.ui.utils.asImageBitmap
|
||||
import com.tangem.core.ui.utils.toAnimatable
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun FloatingCardsContent(isPaused: Boolean, stepDuration: Int) {
|
||||
val imageBitmap = asImageBitmap(R.drawable.img_card_placeholder_wallet_2)
|
||||
val cards = listOf(
|
||||
FloatingCard.first(),
|
||||
FloatingCard.second(),
|
||||
FloatingCard.third(),
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
cards.forEach { floatingCard ->
|
||||
FloatingCard.Item(
|
||||
isPaused = isPaused,
|
||||
imageBitmap = imageBitmap,
|
||||
cardValues = floatingCard,
|
||||
stepDuration = stepDuration,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class CardValues(
|
||||
val translateX: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val translateY: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val rotationX: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val rotationY: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val rotationZ: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val scale: AnimatedValue = AnimatedValue(1f, 1f),
|
||||
)
|
||||
|
||||
private object FloatingCard {
|
||||
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
fun Item(isPaused: Boolean, stepDuration: Int, imageBitmap: ImageBitmap, cardValues: CardValues) {
|
||||
Image(
|
||||
bitmap = imageBitmap,
|
||||
contentDescription = "Floating Tangem card",
|
||||
modifier = Modifier
|
||||
.graphicsLayer(
|
||||
translationX = cardValues.translateX.toAnimatable(isPaused, stepDuration).value,
|
||||
translationY = cardValues.translateY.toAnimatable(isPaused, stepDuration).value,
|
||||
rotationX = cardValues.rotationX.toAnimatable(isPaused, stepDuration).value,
|
||||
rotationY = cardValues.rotationY.toAnimatable(isPaused, stepDuration).value,
|
||||
rotationZ = cardValues.rotationZ.toAnimatable(isPaused, stepDuration).value,
|
||||
scaleX = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
|
||||
scaleY = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun first(): CardValues = CardValues(
|
||||
translateX = -400f to -350f,
|
||||
translateY = 30f to 32f,
|
||||
rotationX = 10f to 15f,
|
||||
rotationY = 15f to 15f,
|
||||
rotationZ = 40f to 27f,
|
||||
scale = 0.6f to 0.6f,
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun second(): CardValues = CardValues(
|
||||
translateX = 350f to 300f,
|
||||
translateY = -70f to 0f,
|
||||
rotationX = 30f to 48f,
|
||||
rotationY = 0f to 5f,
|
||||
rotationZ = -34f to -42f,
|
||||
scale = 0.47f to 0.35f,
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun third(): CardValues = CardValues(
|
||||
translateX = 320f to 250f,
|
||||
translateY = 500f to 500f,
|
||||
rotationX = 0f to 3f,
|
||||
rotationY = 10f to 10f,
|
||||
rotationZ = -45f to -30f,
|
||||
scale = 0.6f to 0.75f,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package com.tangem.features.home.impl.ui.compose.views
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.core.ui.R
|
||||
|
||||
@Composable
|
||||
internal fun HomeButtons(
|
||||
btnScanStateInProgress: Boolean,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onShopButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = modifier,
|
||||
) {
|
||||
ScanCardButton(
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f)
|
||||
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
|
||||
showProgress = btnScanStateInProgress,
|
||||
onClick = onScanButtonClick,
|
||||
)
|
||||
SpacerW12()
|
||||
OrderCardButton(
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f)
|
||||
.testTag(StoriesScreenTestTags.ORDER_BUTTON),
|
||||
onClick = onShopButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_scan),
|
||||
useDarkerColors = false,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
onClick = onClick,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_order),
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::class) state: HomeButtonsState) {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier.background(Color.Black),
|
||||
) {
|
||||
HomeButtons(
|
||||
btnScanStateInProgress = state.btnScanStateInProgress,
|
||||
onScanButtonClick = {},
|
||||
onShopButtonClick = {},
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HomeButtonsParameterProvider : CollectionPreviewParameterProvider<HomeButtonsState>(
|
||||
collection = listOf(
|
||||
HomeButtonsState(
|
||||
btnScanStateInProgress = false,
|
||||
),
|
||||
HomeButtonsState(
|
||||
btnScanStateInProgress = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private data class HomeButtonsState(
|
||||
val btnScanStateInProgress: Boolean,
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
package com.tangem.features.home.impl.ui.compose.views
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.core.ui.R
|
||||
|
||||
@Composable
|
||||
internal fun HomeButtonsV2(
|
||||
btnScanStateInProgress: Boolean,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onCreateNewWalletButtonClick: () -> Unit,
|
||||
onAddExistingWalletButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
CreateNewWalletButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.CREATE_NEW_WALLET_BUTTON),
|
||||
onClick = onCreateNewWalletButtonClick,
|
||||
)
|
||||
AddExistingWalletButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.ADD_EXISTING_WALLET_BUTTON),
|
||||
onClick = onAddExistingWalletButtonClick,
|
||||
)
|
||||
ScanCardButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
|
||||
showProgress = btnScanStateInProgress,
|
||||
onClick = onScanButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateNewWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_create_new_wallet),
|
||||
useDarkerColors = false,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddExistingWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_add_existing_wallet),
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_scan),
|
||||
useDarkerColors = true,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
onClick = onClick,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun HomeButtonsV2Preview(@PreviewParameter(HomeButtonsV2ParameterProvider::class) state: HomeButtonsV2State) {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier.background(Color.Black),
|
||||
) {
|
||||
HomeButtonsV2(
|
||||
btnScanStateInProgress = state.btnScanStateInProgress,
|
||||
onCreateNewWalletButtonClick = {},
|
||||
onAddExistingWalletButtonClick = {},
|
||||
onScanButtonClick = {},
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HomeButtonsV2ParameterProvider : CollectionPreviewParameterProvider<HomeButtonsV2State>(
|
||||
collection = listOf(
|
||||
HomeButtonsV2State(
|
||||
btnScanStateInProgress = false,
|
||||
),
|
||||
HomeButtonsV2State(
|
||||
btnScanStateInProgress = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private data class HomeButtonsV2State(
|
||||
val btnScanStateInProgress: Boolean,
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
package com.tangem.features.home.impl.ui.compose.views
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.R
|
||||
|
||||
@Composable
|
||||
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.common_search_tokens),
|
||||
icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24),
|
||||
showProgress = false,
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SearchCurrenciesButtonPreview() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(color = Color.Black)
|
||||
.padding(all = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
SearchCurrenciesButton(modifier = Modifier.fillMaxWidth(), onClick = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package com.tangem.features.home.impl.ui.compose.views
|
||||
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun StoriesButton(
|
||||
text: String,
|
||||
useDarkerColors: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
|
||||
showProgress: Boolean = false,
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = icon,
|
||||
colors = if (useDarkerColors) DarkerButtonColors else LighterButtonColors,
|
||||
showProgress = showProgress,
|
||||
enabled = true,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
iconPadding = when (icon) {
|
||||
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing4
|
||||
is TangemButtonIconPosition.End,
|
||||
is TangemButtonIconPosition.None,
|
||||
-> TangemTheme.dimens.spacing8
|
||||
},
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
private val LighterButtonColors: ButtonColors = ButtonColors(
|
||||
containerColor = TangemColorPalette.Light4,
|
||||
contentColor = TangemColorPalette.Dark6,
|
||||
disabledContainerColor = TangemColorPalette.Dark5,
|
||||
disabledContentColor = TangemColorPalette.Dark6,
|
||||
)
|
||||
|
||||
private val DarkerButtonColors: ButtonColors = ButtonColors(
|
||||
containerColor = TangemColorPalette.Dark4,
|
||||
contentColor = TangemColorPalette.White,
|
||||
disabledContainerColor = TangemColorPalette.Dark4,
|
||||
disabledContentColor = TangemColorPalette.White,
|
||||
)
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
package com.tangem.features.home.impl.ui.compose.views
|
||||
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L
|
||||
|
||||
@Composable
|
||||
fun StoriesProgressBar(
|
||||
steps: Int,
|
||||
currentStep: Int,
|
||||
paused: Boolean = false,
|
||||
stepDuration: Int = 8_000,
|
||||
onStepFinish: () -> Unit = {},
|
||||
) {
|
||||
val progress = remember(currentStep) { Animatable(initialValue = 0f) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val animatorSpeed = Settings.Global.getFloat(
|
||||
context.contentResolver,
|
||||
Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||
1f,
|
||||
)
|
||||
|
||||
LaunchedEffect(paused, currentStep, animatorSpeed) {
|
||||
if (paused) {
|
||||
progress.stop()
|
||||
} else {
|
||||
if (animatorSpeed == 0f) {
|
||||
progress.snapTo(1f)
|
||||
delay(STORIES_ANIMATION_SPEED_ZERO_DURATION)
|
||||
} else {
|
||||
progress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
|
||||
easing = LinearEasing,
|
||||
),
|
||||
)
|
||||
progress.snapTo(0f)
|
||||
}
|
||||
onStepFinish()
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
for (index in 0..steps) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size2)
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
|
||||
.background(TangemColorPalette.White.copy(alpha = .2f)),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
|
||||
.background(TangemColorPalette.White)
|
||||
.fillMaxHeight()
|
||||
.let {
|
||||
when (index) {
|
||||
currentStep -> it.fillMaxWidth(progress.value)
|
||||
in 0..currentStep -> it.fillMaxWidth(fraction = 1f)
|
||||
else -> it
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (index != steps) {
|
||||
SpacerW4()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun StoriesProgressBarPreview() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.background(TangemColorPalette.Black)
|
||||
.padding(vertical = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
StoriesProgressBar(steps = 5, currentStep = 3, paused = false)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.features.home.impl.ui.state
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class HomeUM(
|
||||
val scanInProgress: Boolean,
|
||||
val stories: ImmutableList<Stories>,
|
||||
val onScanClick: () -> Unit,
|
||||
val onShopClick: () -> Unit,
|
||||
val onSearchTokensClick: () -> Unit,
|
||||
val onCreateNewWalletClick: () -> Unit,
|
||||
val onAddExistingWalletClick: () -> Unit,
|
||||
) {
|
||||
val firstStory: Stories get() = stories[0]
|
||||
|
||||
fun stepOf(story: Stories): Int = stories.indexOf(story)
|
||||
}
|
||||
|
||||
enum class Stories(val duration: Int = 6000) {
|
||||
TangemIntro,
|
||||
RevolutionaryWallet,
|
||||
UltraSecureBackup,
|
||||
Currencies,
|
||||
Web3,
|
||||
WalletForEveryone,
|
||||
}
|
||||
|
||||
/**
|
||||
* For FCA restriction stories
|
||||
*/
|
||||
fun getRestrictedStories(): List<Stories> {
|
||||
return Stories.entries.filterNot { it == Stories.Currencies }
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ dependencies {
|
|||
|
||||
/* Project - Domain */
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
||||
/* Project - Core */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.hotwallet
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface CreateWalletBackupComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, CreateWalletBackupComponent>
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.hotwallet
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
|
||||
interface HotAccessCodeRequestComponent : ComposableContentComponent, HotWalletPasswordRequester {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.features.hotwallet
|
||||
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
|
||||
interface HotWalletPasswordRequester {
|
||||
|
||||
suspend fun wrongPassword()
|
||||
|
||||
suspend fun requestPassword(hasBiometry: Boolean): Result
|
||||
|
||||
suspend fun dismiss()
|
||||
|
||||
sealed class Result {
|
||||
data object UseBiometry : Result()
|
||||
data object Dismiss : Result()
|
||||
data class EnteredPassword(val password: HotAuth.Password) : Result()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.features.hotwallet
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface UpdateAccessCodeComponent : ComposableContentComponent {
|
||||
data class Params(val userWalletId: UserWalletId)
|
||||
interface Factory : ComponentFactory<Params, UpdateAccessCodeComponent>
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.hotwallet
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
||||
interface WalletActivationComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, WalletActivationComponent>
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ dependencies {
|
|||
implementation(projects.core.datasource)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.card)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.wallets.models)
|
||||
|
|
@ -52,7 +53,6 @@ dependencies {
|
|||
implementation(deps.lifecycle.runtime.ktx)
|
||||
|
||||
/** Compose libraries */
|
||||
implementation(deps.compose.material) // to use buttons and text field in MultiWalletSeedPhraseImport.kt
|
||||
implementation(deps.compose.material3)
|
||||
implementation(deps.compose.animation)
|
||||
implementation(deps.compose.foundation)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode
|
||||
package com.tangem.features.hotwallet.accesscode
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -8,36 +8,45 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
|
||||
import com.tangem.features.hotwallet.setaccesscode.ui.SetAccessCodeContent
|
||||
import com.tangem.features.hotwallet.accesscode.ui.AccessCode
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
internal class SetAccessCodeComponent @AssistedInject constructor(
|
||||
internal class AccessCodeComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Params,
|
||||
) : ComposableContentComponent, AppComponentContext by context {
|
||||
|
||||
private val model: SetAccessCodeModel = getOrCreateModel(params)
|
||||
private val model: AccessCodeModel = getOrCreateModel(params)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
SetAccessCodeContent(
|
||||
state = state,
|
||||
onBack = { model.onBack() },
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
DisableScreenshotsDisposableEffect()
|
||||
|
||||
AccessCode(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onBackClick()
|
||||
fun onAccessCodeSet()
|
||||
fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String)
|
||||
fun onAccessCodeConfirmed(userWalletId: UserWalletId)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val isConfirmMode: Boolean,
|
||||
val accessCodeToConfirm: String? = null,
|
||||
val userWalletId: UserWalletId,
|
||||
val callbacks: ModelCallbacks,
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(context: AppComponentContext, params: Params): AccessCodeComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.tangem.features.hotwallet.accesscode
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.UnlockHotWallet
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class AccessCodeModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AccessCodeComponent.Params>()
|
||||
|
||||
internal val uiState: StateFlow<AccessCodeUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
private fun getInitialState() = AccessCodeUM(
|
||||
accessCode = "",
|
||||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
isConfirmMode = params.isConfirmMode,
|
||||
buttonEnabled = false,
|
||||
buttonInProgress = false,
|
||||
onButtonClick = ::onButtonClick,
|
||||
)
|
||||
|
||||
private fun onAccessCodeChange(value: String) {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
accessCode = value,
|
||||
buttonEnabled = if (params.isConfirmMode) {
|
||||
value == params.accessCodeToConfirm
|
||||
} else {
|
||||
value.length == uiState.value.accessCodeLength
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onButtonClick() {
|
||||
if (!params.isConfirmMode) {
|
||||
params.callbacks.onAccessCodeSet(params.userWalletId, uiState.value.accessCode)
|
||||
} else {
|
||||
params.accessCodeToConfirm?.let {
|
||||
setCode(params.userWalletId, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setCode(userWalletId: UserWalletId, accessCode: String) {
|
||||
modelScope.launch {
|
||||
uiState.update {
|
||||
it.copy(buttonInProgress = true)
|
||||
}
|
||||
|
||||
runCatching {
|
||||
val userWallet = getUserWalletUseCase(userWalletId)
|
||||
.getOrElse { error("User wallet with id $userWalletId not found") }
|
||||
if (userWallet !is UserWallet.Hot) return@launch
|
||||
|
||||
val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth)
|
||||
var updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
auth = HotAuth.Password(accessCode.toCharArray()),
|
||||
)
|
||||
|
||||
if (walletsRepository.requireAccessCode().not()) {
|
||||
updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = updatedHotWalletId,
|
||||
auth = HotAuth.Password(accessCode.toCharArray()),
|
||||
),
|
||||
auth = HotAuth.Biometry,
|
||||
)
|
||||
}
|
||||
|
||||
userWalletsListRepository.saveWithoutLock(
|
||||
userWallet.copy(
|
||||
hotWalletId = updatedHotWalletId,
|
||||
backedUp = true,
|
||||
),
|
||||
canOverride = true,
|
||||
)
|
||||
|
||||
userWalletsListRepository.setLock(
|
||||
userWallet.walletId,
|
||||
UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()),
|
||||
)
|
||||
|
||||
if (walletsRepository.useBiometricAuthentication()) {
|
||||
userWalletsListRepository.setLock(
|
||||
userWallet.walletId,
|
||||
UserWalletsListRepository.LockMethod.Biometric,
|
||||
)
|
||||
}
|
||||
|
||||
params.callbacks.onAccessCodeConfirmed(params.userWalletId)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
|
||||
uiState.update {
|
||||
it.copy(buttonInProgress = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package com.tangem.features.hotwallet.accesscode
|
||||
|
||||
const val ACCESS_CODE_LENGTH = 6
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.features.hotwallet.setaccesscode.di
|
||||
package com.tangem.features.hotwallet.accesscode.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel
|
||||
import com.tangem.features.hotwallet.accesscode.AccessCodeModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -11,10 +11,10 @@ import dagger.multibindings.IntoMap
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface SetAccessCodeModule {
|
||||
internal interface AccessCodeModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(SetAccessCodeModel::class)
|
||||
fun bindSetAccessCodeModel(model: SetAccessCodeModel): Model
|
||||
@ClassKey(AccessCodeModel::class)
|
||||
fun bindAccessCodeModel(model: AccessCodeModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.features.hotwallet.accesscode.entity
|
||||
|
||||
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
|
||||
|
||||
internal data class AccessCodeUM(
|
||||
val accessCode: String,
|
||||
val onAccessCodeChange: (String) -> Unit,
|
||||
val isConfirmMode: Boolean,
|
||||
val buttonEnabled: Boolean,
|
||||
val buttonInProgress: Boolean,
|
||||
val onButtonClick: () -> Unit,
|
||||
) {
|
||||
val accessCodeLength: Int = ACCESS_CODE_LENGTH
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package com.tangem.features.hotwallet.accesscode.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.res.R
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.fields.PinTextColor
|
||||
import com.tangem.core.ui.components.fields.PinTextField
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
|
||||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(top = 16.dp)
|
||||
.weight(1f)
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(horizontal = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(top = 56.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = if (state.isConfirmMode) {
|
||||
stringResourceSafe(R.string.access_code_confirm_title)
|
||||
} else {
|
||||
stringResourceSafe(R.string.access_code_create_title)
|
||||
},
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = if (state.isConfirmMode) {
|
||||
stringResourceSafe(R.string.access_code_confirm_description)
|
||||
} else {
|
||||
stringResourceSafe(
|
||||
R.string.access_code_create_description,
|
||||
state.accessCodeLength,
|
||||
)
|
||||
},
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
PinTextField(
|
||||
length = state.accessCodeLength,
|
||||
isPasswordVisual = !state.isConfirmMode,
|
||||
value = state.accessCode,
|
||||
pinTextColor = PinTextColor.Primary,
|
||||
onValueChange = state.onAccessCodeChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.imePadding(),
|
||||
text = stringResourceSafe(
|
||||
if (state.isConfirmMode) {
|
||||
R.string.common_confirm
|
||||
} else {
|
||||
R.string.common_continue
|
||||
},
|
||||
),
|
||||
onClick = state.onButtonClick,
|
||||
enabled = state.buttonEnabled,
|
||||
showProgress = state.buttonInProgress,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewSet() {
|
||||
TangemThemePreview {
|
||||
AccessCode(
|
||||
state = AccessCodeUM(
|
||||
accessCode = "",
|
||||
onAccessCodeChange = {},
|
||||
isConfirmMode = false,
|
||||
buttonEnabled = false,
|
||||
buttonInProgress = false,
|
||||
onButtonClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun PreviewConfirm() {
|
||||
TangemThemePreview {
|
||||
AccessCode(
|
||||
state = AccessCodeUM(
|
||||
accessCode = "123456",
|
||||
onAccessCodeChange = {},
|
||||
isConfirmMode = true,
|
||||
buttonEnabled = true,
|
||||
buttonInProgress = false,
|
||||
onButtonClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
package com.tangem.features.hotwallet.accesscoderequest
|
||||
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.runtime.*
|
||||
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.core.ui.components.FullScreen
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
|
||||
import com.tangem.features.hotwallet.HotWalletPasswordRequester
|
||||
import com.tangem.features.hotwallet.accesscoderequest.ui.HotAccessCodeRequestFullScreenContent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
|
@ -26,8 +25,14 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor(
|
|||
model.wrongAccessCode()
|
||||
}
|
||||
|
||||
override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result {
|
||||
model.show(hasBiometry)
|
||||
override suspend fun successfulAuthentication() {
|
||||
model.successfulAuthentication()
|
||||
}
|
||||
|
||||
override suspend fun requestPassword(
|
||||
attemptRequest: HotWalletPasswordRequester.AttemptRequest,
|
||||
): HotWalletPasswordRequester.Result {
|
||||
model.show(attemptRequest)
|
||||
return model.waitResult()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,36 +2,64 @@ package com.tangem.features.hotwallet.accesscoderequest
|
|||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.hotwallet.HotWalletPasswordRequester
|
||||
import com.tangem.core.ui.components.fields.PinTextColor
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.core.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
|
||||
import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class HotAccessCodeRequestModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
) : Model() {
|
||||
|
||||
private val result = MutableStateFlow<HotWalletPasswordRequester.Result?>(null)
|
||||
private val currentRequest = MutableStateFlow<HotWalletPasswordRequester.AttemptRequest?>(null)
|
||||
private val attemptsRequestJobHolder = JobHolder()
|
||||
|
||||
private val HotWalletPasswordRequester.AttemptRequest.attemptId
|
||||
get() = HotWalletAccessCodeAttemptsRepository.AttemptId(
|
||||
hotWalletId = hotWalletId,
|
||||
auth = authMode,
|
||||
)
|
||||
|
||||
val uiState: StateFlow<HotAccessCodeRequestUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
fun dismiss() {
|
||||
result.value = HotWalletPasswordRequester.Result.Dismiss
|
||||
dismissState()
|
||||
}
|
||||
suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) {
|
||||
if (userWalletExists(attemptRequest.hotWalletId).not()) {
|
||||
Timber.e("User wallet with id ${attemptRequest.hotWalletId} does not exist")
|
||||
result.value = HotWalletPasswordRequester.Result.Dismiss
|
||||
return
|
||||
}
|
||||
|
||||
fun show(hasBiometry: Boolean) {
|
||||
currentRequest.value = attemptRequest
|
||||
result.value = null // Reset the result when showing the dialog
|
||||
subscribeToAttempts(id = attemptRequest.attemptId)
|
||||
uiState.update {
|
||||
it.copy(
|
||||
isShown = true,
|
||||
accessCode = "",
|
||||
useBiometricVisible = hasBiometry,
|
||||
useBiometricVisible = attemptRequest.hasBiometry,
|
||||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
)
|
||||
}
|
||||
|
|
@ -41,16 +69,36 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
return result.filterNotNull().first().also { result.value = null }
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
result.value = HotWalletPasswordRequester.Result.Dismiss
|
||||
attemptsRequestJobHolder.cancel()
|
||||
dismissState()
|
||||
}
|
||||
|
||||
suspend fun wrongAccessCode() {
|
||||
val currentRequest = currentRequest.value ?: return
|
||||
hotAccessCodeAttemptsRepository.incrementAttempts(currentRequest.attemptId)
|
||||
uiState.update {
|
||||
it.copy(
|
||||
wrongAccessCode = true,
|
||||
accessCodeColor = PinTextColor.WrongCode,
|
||||
onAccessCodeChange = {},
|
||||
)
|
||||
}
|
||||
delay(timeMillis = 500) // Delay to show the wrong access code state
|
||||
}
|
||||
|
||||
suspend fun successfulAuthentication() {
|
||||
val currentRequest = currentRequest.value ?: return
|
||||
hotAccessCodeAttemptsRepository.resetAttempts(currentRequest.hotWalletId)
|
||||
uiState.update {
|
||||
it.copy(
|
||||
accessCodeColor = PinTextColor.Success,
|
||||
onAccessCodeChange = {},
|
||||
)
|
||||
}
|
||||
delay(timeMillis = 200) // Delay to show the success state
|
||||
}
|
||||
|
||||
private fun getInitialState() = HotAccessCodeRequestUM(
|
||||
onDismiss = ::dismiss,
|
||||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
|
|
@ -65,7 +113,10 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
if (accessCode.length > ACCESS_CODE_LENGTH) return
|
||||
|
||||
uiState.update {
|
||||
it.copy(accessCode = accessCode, wrongAccessCode = false)
|
||||
it.copy(
|
||||
accessCode = accessCode,
|
||||
accessCodeColor = PinTextColor.Primary,
|
||||
)
|
||||
}
|
||||
|
||||
if (accessCode.length == ACCESS_CODE_LENGTH) {
|
||||
|
|
@ -77,13 +128,71 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun subscribeToAttempts(id: HotWalletAccessCodeAttemptsRepository.AttemptId) {
|
||||
fun remainingSecondsToText(remainingSeconds: Int): TextReference? {
|
||||
return if (remainingSeconds > 0) {
|
||||
resourceReference(
|
||||
R.string.access_code_check_warining_wait,
|
||||
wrappedList(remainingSeconds),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun collectAttempts(attempts: Attempts) {
|
||||
when (attempts) {
|
||||
is Attempts.FastForward -> {
|
||||
/** ignore */
|
||||
}
|
||||
is Attempts.WithDelay -> {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds),
|
||||
onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 }
|
||||
?: {},
|
||||
)
|
||||
}
|
||||
}
|
||||
is Attempts.BeforeDeletion -> {
|
||||
uiState.update {
|
||||
it.copy(
|
||||
wrongAccessCodeText = remainingSecondsToText(attempts.remainingSeconds)
|
||||
?: resourceReference(
|
||||
R.string.access_code_check_warining_delete,
|
||||
wrappedList(attempts.remainingAttemptsCountBeforeDeletion),
|
||||
),
|
||||
onAccessCodeChange = ::onAccessCodeChange.takeIf { attempts.remainingSeconds <= 0 }
|
||||
?: {},
|
||||
)
|
||||
}
|
||||
}
|
||||
Attempts.Deletion -> deleteUserWallet()
|
||||
}
|
||||
}
|
||||
|
||||
modelScope.launch {
|
||||
hotAccessCodeAttemptsRepository.getAttempts(id)
|
||||
.collectLatest { attempts -> collectAttempts(attempts) }
|
||||
}.saveIn(attemptsRequestJobHolder)
|
||||
}
|
||||
|
||||
private suspend fun userWalletExists(id: HotWalletId): Boolean {
|
||||
return userWalletsListRepository.userWalletsSync()
|
||||
.any { it is UserWallet.Hot && it.hotWalletId == id }
|
||||
}
|
||||
|
||||
private suspend fun deleteUserWallet() {
|
||||
val currentRequest = currentRequest.value ?: return
|
||||
val userWallet = userWalletsListRepository.userWalletsSync()
|
||||
.firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return
|
||||
userWalletsListRepository.delete(listOf(userWallet.walletId))
|
||||
dismiss()
|
||||
}
|
||||
|
||||
private fun dismissState() {
|
||||
uiState.update {
|
||||
it.copy(isShown = false)
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ACCESS_CODE_LENGTH = 6
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.features.hotwallet.accesscoderequest.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
|
||||
import com.tangem.features.hotwallet.HotWalletPasswordRequester
|
||||
import com.tangem.features.hotwallet.accesscoderequest.DefaultHotAccessCodeRequestComponent
|
||||
import com.tangem.features.hotwallet.accesscoderequest.HotAccessCodeRequestModel
|
||||
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package com.tangem.features.hotwallet.accesscoderequest.entity
|
||||
|
||||
import com.tangem.core.ui.components.fields.PinTextColor
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class HotAccessCodeRequestUM(
|
||||
val isShown: Boolean = false,
|
||||
val accessCode: String = "",
|
||||
val wrongAccessCode: Boolean = false,
|
||||
val accessCodeColor: PinTextColor = PinTextColor.Primary,
|
||||
val wrongAccessCodeText: TextReference? = null,
|
||||
val useBiometricVisible: Boolean = true,
|
||||
val useBiometricClick: () -> Unit = {},
|
||||
val onAccessCodeChange: (String) -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.hotwallet.accesscoderequest.proxy
|
||||
|
||||
import com.tangem.features.hotwallet.HotWalletPasswordRequester
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -13,16 +13,15 @@ class HotWalletPasswordRequesterProxy @Inject constructor() : HotWalletPasswordR
|
|||
|
||||
val componentRequester = MutableStateFlow<HotWalletPasswordRequester?>(null)
|
||||
|
||||
override suspend fun wrongPassword() {
|
||||
call { wrongPassword() }
|
||||
}
|
||||
override suspend fun wrongPassword() = call { wrongPassword() }
|
||||
|
||||
override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result =
|
||||
call { requestPassword(hasBiometry) }
|
||||
override suspend fun successfulAuthentication() = call { successfulAuthentication() }
|
||||
|
||||
override suspend fun dismiss() {
|
||||
call { dismiss() }
|
||||
}
|
||||
override suspend fun requestPassword(
|
||||
attemptRequest: HotWalletPasswordRequester.AttemptRequest,
|
||||
): HotWalletPasswordRequester.Result = call { requestPassword(attemptRequest) }
|
||||
|
||||
override suspend fun dismiss() = call { dismiss() }
|
||||
|
||||
private suspend fun <T> call(block: suspend HotWalletPasswordRequester.() -> T): T {
|
||||
return withTimeout(timeMillis = 1000) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.LineBreak
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
|
|
@ -20,7 +22,10 @@ import com.tangem.core.ui.components.SpacerH
|
|||
import com.tangem.core.ui.components.SpacerH24
|
||||
import com.tangem.core.ui.components.appbar.TangemTopAppBar
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.fields.PinTextColor
|
||||
import com.tangem.core.ui.components.fields.PinTextField
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.haptic.TangemHapticEffect
|
||||
import com.tangem.core.ui.res.LocalHapticManager
|
||||
|
|
@ -85,9 +90,36 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM
|
|||
length = 6,
|
||||
isPasswordVisual = true,
|
||||
value = state.accessCode,
|
||||
wrongCode = state.wrongAccessCode,
|
||||
pinTextColor = state.accessCodeColor,
|
||||
onValueChange = state.onAccessCodeChange,
|
||||
)
|
||||
|
||||
SpacerH(20.dp)
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.animateEnterExit(
|
||||
enter = slideInVertically(
|
||||
tween(),
|
||||
initialOffsetY = { it + 200 },
|
||||
) + fadeIn(tween()),
|
||||
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
|
||||
),
|
||||
visible = state.wrongAccessCodeText != null,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
val wrongAccessCodeText =
|
||||
state.wrongAccessCodeText ?: return@AnimatedVisibility
|
||||
|
||||
Text(
|
||||
text = wrongAccessCodeText.resolveReference(),
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.caption2.copy(
|
||||
lineBreak = LineBreak.Heading,
|
||||
),
|
||||
color = TangemTheme.colors.text.warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.useBiometricVisible) {
|
||||
|
|
@ -97,7 +129,10 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM
|
|||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.imePadding(),
|
||||
text = "Use biometric",
|
||||
text = stringResourceSafe(
|
||||
id = R.string.welcome_unlock,
|
||||
stringResourceSafe(R.string.common_biometrics),
|
||||
),
|
||||
onClick = state.useBiometricClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -106,9 +141,15 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM
|
|||
|
||||
val hapticManager = LocalHapticManager.current
|
||||
|
||||
LaunchedEffect(state.wrongAccessCode) {
|
||||
if (state.wrongAccessCode) {
|
||||
hapticManager.perform(TangemHapticEffect.View.Reject)
|
||||
LaunchedEffect(state.accessCodeColor) {
|
||||
when (state.accessCodeColor) {
|
||||
PinTextColor.WrongCode -> {
|
||||
hapticManager.perform(TangemHapticEffect.View.Reject)
|
||||
}
|
||||
PinTextColor.Success -> {
|
||||
hapticManager.perform(TangemHapticEffect.View.Confirm)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,7 +166,10 @@ private fun Preview() {
|
|||
var isShown by remember { mutableStateOf(true) }
|
||||
|
||||
HotAccessCodeRequestFullScreenContent(
|
||||
state = HotAccessCodeRequestUM(isShown = isShown),
|
||||
state = HotAccessCodeRequestUM(
|
||||
isShown = isShown,
|
||||
wrongAccessCodeText = stringReference("Wrong access code"),
|
||||
),
|
||||
modifier = Modifier,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.entry
|
||||
|
||||
import com.arkivanov.decompose.router.stack.*
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.settings.ShouldAskPermissionUseCase
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
|
||||
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
|
||||
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
|
||||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class AddExistingWalletModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
||||
) : Model() {
|
||||
|
||||
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
|
||||
val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks()
|
||||
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
|
||||
val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks()
|
||||
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
|
||||
val pushNotificationsCallbacks = PushNotificationsCallbacks()
|
||||
val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks()
|
||||
|
||||
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
|
||||
val startRoute = AddExistingWalletRoute.Start
|
||||
val currentRoute: MutableStateFlow<AddExistingWalletRoute> = MutableStateFlow(startRoute)
|
||||
|
||||
fun onChildBack() {
|
||||
when (currentRoute.value) {
|
||||
is AddExistingWalletRoute.Start -> router.pop()
|
||||
is AddExistingWalletRoute.Import -> stackNavigation.pop()
|
||||
is AddExistingWalletRoute.BackupCompleted -> Unit
|
||||
is AddExistingWalletRoute.SetAccessCode -> Unit
|
||||
is AddExistingWalletRoute.ConfirmAccessCode -> stackNavigation.pop()
|
||||
is AddExistingWalletRoute.PushNotifications -> Unit
|
||||
is AddExistingWalletRoute.SetupFinished -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToPushNotificationsOrNext() {
|
||||
modelScope.launch {
|
||||
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
|
||||
if (shouldRequestPush) {
|
||||
// is yet blocked by [REDACTED_TASK_KEY]
|
||||
// stackNavigation.replaceAll(AddExistingWalletRoute.PushNotifications)
|
||||
stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished)
|
||||
} else {
|
||||
stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToSetupFinished() {
|
||||
stackNavigation.replaceAll(AddExistingWalletRoute.SetupFinished)
|
||||
}
|
||||
|
||||
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
|
||||
override fun onBackClick() {
|
||||
onChildBack()
|
||||
}
|
||||
|
||||
override fun onSkipClick() {
|
||||
navigateToPushNotificationsOrNext()
|
||||
}
|
||||
}
|
||||
|
||||
inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks {
|
||||
override fun onBackClick() {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
override fun onImportPhraseClick() {
|
||||
stackNavigation.push(AddExistingWalletRoute.Import)
|
||||
}
|
||||
}
|
||||
|
||||
inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks {
|
||||
override fun onWalletImported(userWalletId: UserWalletId) {
|
||||
stackNavigation.replaceAll(AddExistingWalletRoute.BackupCompleted(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks {
|
||||
override fun onContinueClick(userWalletId: UserWalletId) {
|
||||
stackNavigation.replaceAll(AddExistingWalletRoute.SetAccessCode(userWalletId))
|
||||
}
|
||||
}
|
||||
|
||||
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
|
||||
override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) {
|
||||
stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode))
|
||||
}
|
||||
|
||||
override fun onAccessCodeConfirmed(userWalletId: UserWalletId) {
|
||||
navigateToPushNotificationsOrNext()
|
||||
}
|
||||
}
|
||||
|
||||
inner class PushNotificationsCallbacks : PushNotificationsModelCallbacks {
|
||||
override fun onAllowSystemPermission() {
|
||||
navigateToSetupFinished()
|
||||
}
|
||||
|
||||
override fun onDenySystemPermission() {
|
||||
navigateToSetupFinished()
|
||||
}
|
||||
|
||||
override fun onDismiss() {
|
||||
navigateToSetupFinished()
|
||||
}
|
||||
}
|
||||
|
||||
inner class MobileWalletSetupFinishedComponentModelCallbacks :
|
||||
MobileWalletSetupFinishedComponent.ModelCallbacks {
|
||||
override fun onContinueClick() {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.entry
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
|
||||
internal class AddExistingWalletStepperStateManager {
|
||||
|
||||
fun getStepperState(route: AddExistingWalletRoute): HotWalletStepperComponent.StepperUM? {
|
||||
return when (route) {
|
||||
is AddExistingWalletRoute.Start -> null
|
||||
|
||||
is AddExistingWalletRoute.Import -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_IMPORT,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.wallet_import_seed_navtitle),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_BACKUP,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_ACCESS_CODE,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = false,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_ACCESS_CODE,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = true,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.PushNotifications -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_NOTIFICATIONS,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.onboarding_title_notifications),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.SetupFinished -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_DONE,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.common_done),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STEPS_COUNT = 5
|
||||
|
||||
private const val STEP_IMPORT = 1
|
||||
private const val STEP_BACKUP = 2
|
||||
private const val STEP_ACCESS_CODE = 3
|
||||
private const val STEP_NOTIFICATIONS = 4
|
||||
private const val STEP_DONE = 5
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.root
|
||||
package com.tangem.features.hotwallet.addexistingwallet.entry
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -6,33 +6,36 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.value.ObserveLifecycleMode
|
||||
import com.arkivanov.decompose.value.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.hotwallet.AddExistingWalletComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletChildFactory
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.ui.AddExistingWalletContent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletChildFactory
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.ui.AddExistingWalletContent
|
||||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class DefaultAddExistingWalletComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: Unit,
|
||||
private val stepperStateManager: AddExistingWalletStepperStateManager,
|
||||
addExistingWalletChildFactory: AddExistingWalletChildFactory,
|
||||
stepperComponentFactory: DefaultHotWalletStepperComponent.Factory,
|
||||
) : AddExistingWalletComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: AddExistingWalletModel = getOrCreateModel(params)
|
||||
|
||||
private val startRoute = AddExistingWalletRoute.Start
|
||||
|
||||
private val innerStack = childStack(
|
||||
key = "addExistingWalletInnerStack",
|
||||
source = model.stackNavigation,
|
||||
serializer = null,
|
||||
initialConfiguration = startRoute,
|
||||
initialConfiguration = model.startRoute,
|
||||
handleBackButton = true,
|
||||
childFactory = { configuration, factoryContext ->
|
||||
addExistingWalletChildFactory.createChild(
|
||||
|
|
@ -43,27 +46,41 @@ internal class DefaultAddExistingWalletComponent @AssistedInject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
private val stepperComponent = stepperComponentFactory.create(
|
||||
context = this,
|
||||
params = HotWalletStepperComponent.Params(
|
||||
initState = HotWalletStepperComponent.StepperUM.initialState(),
|
||||
callback = model.hotWalletStepperComponentModelCallback,
|
||||
),
|
||||
)
|
||||
|
||||
init {
|
||||
innerStack.subscribe(
|
||||
lifecycle = lifecycle,
|
||||
mode = ObserveLifecycleMode.CREATE_DESTROY,
|
||||
) { stack ->
|
||||
componentScope.launch {
|
||||
model.currentRoute.emit(stack.active.configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val stackState by innerStack.subscribeAsState()
|
||||
val currentRoute = stackState.active.configuration
|
||||
|
||||
BackHandler(onBack = model::onChildBack)
|
||||
|
||||
val stepperState = stepperStateManager.getStepperState(currentRoute)
|
||||
stepperState?.let { stepperComponent.updateState(it) }
|
||||
|
||||
BackHandler(onBack = ::onChildBack)
|
||||
AddExistingWalletContent(
|
||||
stackState = stackState,
|
||||
stepperComponent = stepperComponent.takeIf { stepperState != null },
|
||||
)
|
||||
}
|
||||
|
||||
private fun onChildBack() {
|
||||
val isEmptyStack = innerStack.value.backStack.isEmpty()
|
||||
|
||||
if (isEmptyStack) {
|
||||
router.pop()
|
||||
} else {
|
||||
val currentRoute = innerStack.value.active.configuration
|
||||
model.onChildBack(currentRoute)
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : AddExistingWalletComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Unit): DefaultAddExistingWalletComponent
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.entry.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.hotwallet.AddExistingWalletComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletStepperStateManager
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.DefaultAddExistingWalletComponent
|
||||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
import com.tangem.features.hotwallet.stepper.impl.DefaultHotWalletStepperComponent
|
||||
import com.tangem.features.hotwallet.stepper.impl.HotWalletStepperModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AddExistingWalletModuleBinds {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAddExistingWalletComponentFactory(
|
||||
impl: DefaultAddExistingWalletComponent.Factory,
|
||||
): AddExistingWalletComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AddExistingWalletModel::class)
|
||||
fun bindAddExistingWalletModel(model: AddExistingWalletModel): Model
|
||||
|
||||
@Binds
|
||||
fun bindFactory(impl: DefaultHotWalletStepperComponent.Factory): HotWalletStepperComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(HotWalletStepperModel::class)
|
||||
fun bindHotWalletStepperModel(model: HotWalletStepperModel): Model
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object AddExistingWalletModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAddExistingWalletStepperStateManager(): AddExistingWalletStepperStateManager {
|
||||
return AddExistingWalletStepperStateManager()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.root.entity
|
||||
package com.tangem.features.hotwallet.addexistingwallet.entry.entity
|
||||
|
||||
internal data class AddExistingWalletUM(
|
||||
val onBackClick: () -> Unit,
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.root.routing
|
||||
package com.tangem.features.hotwallet.addexistingwallet.entry.routing
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.AddExistingWalletModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
|
||||
import com.tangem.features.hotwallet.accesscode.AccessCodeComponent
|
||||
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsParams
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AddExistingWalletChildFactory @Inject constructor(
|
||||
private val pushNotificationsComponent: PushNotificationsComponent.Factory,
|
||||
private val accessCodeComponentFactory: AccessCodeComponent.Factory,
|
||||
) {
|
||||
|
||||
fun createChild(
|
||||
|
|
@ -39,23 +39,35 @@ internal class AddExistingWalletChildFactory @Inject constructor(
|
|||
is AddExistingWalletRoute.BackupCompleted -> ManualBackupCompletedComponent(
|
||||
context = childContext,
|
||||
params = ManualBackupCompletedComponent.Params(
|
||||
userWalletId = route.userWalletId,
|
||||
callbacks = model.manualBackupCompletedComponentModelCallbacks,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.AccessCode -> SetAccessCodeComponent(
|
||||
is AddExistingWalletRoute.SetAccessCode -> accessCodeComponentFactory.create(
|
||||
context = childContext,
|
||||
params = SetAccessCodeComponent.Params(
|
||||
params = AccessCodeComponent.Params(
|
||||
isConfirmMode = false,
|
||||
userWalletId = route.userWalletId,
|
||||
callbacks = model.accessCodeModelCallbacks,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.ConfirmAccessCode -> accessCodeComponentFactory.create(
|
||||
context = childContext,
|
||||
params = AccessCodeComponent.Params(
|
||||
isConfirmMode = true,
|
||||
accessCodeToConfirm = route.accessCode,
|
||||
userWalletId = route.userWalletId,
|
||||
callbacks = model.accessCodeModelCallbacks,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create(
|
||||
context = childContext,
|
||||
params = PushNotificationsParams(
|
||||
modelCallbacks = PushNotificationsModelCallbacksStub(),
|
||||
modelCallbacks = model.pushNotificationsCallbacks,
|
||||
source = AppRoute.PushNotification.Source.Onboarding,
|
||||
),
|
||||
)
|
||||
AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent(
|
||||
is AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent(
|
||||
context = childContext,
|
||||
params = MobileWalletSetupFinishedComponent.Params(
|
||||
callbacks = model.mobileWalletSetupFinishedComponentModelCallbacks,
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.root.routing
|
||||
package com.tangem.features.hotwallet.addexistingwallet.entry.routing
|
||||
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
internal sealed class AddExistingWalletRoute : Route {
|
||||
|
|
@ -12,10 +13,13 @@ internal sealed class AddExistingWalletRoute : Route {
|
|||
object Import : AddExistingWalletRoute()
|
||||
|
||||
@Serializable
|
||||
object BackupCompleted : AddExistingWalletRoute()
|
||||
data class BackupCompleted(val userWalletId: UserWalletId) : AddExistingWalletRoute()
|
||||
|
||||
@Serializable
|
||||
object AccessCode : AddExistingWalletRoute()
|
||||
data class SetAccessCode(val userWalletId: UserWalletId) : AddExistingWalletRoute()
|
||||
|
||||
@Serializable
|
||||
data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : AddExistingWalletRoute()
|
||||
|
||||
@Serializable
|
||||
object PushNotifications : AddExistingWalletRoute()
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.root.ui
|
||||
package com.tangem.features.hotwallet.addexistingwallet.entry.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
|
|
@ -12,19 +13,29 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
|
|||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
|
||||
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
|
||||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
|
||||
@Composable
|
||||
internal fun AddExistingWalletContent(stackState: ChildStack<AddExistingWalletRoute, ComposableContentComponent>) {
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation(slide()),
|
||||
internal fun AddExistingWalletContent(
|
||||
stackState: ChildStack<AddExistingWalletRoute, ComposableContentComponent>,
|
||||
stepperComponent: HotWalletStepperComponent?,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
) {
|
||||
it.instance.Content(Modifier.fillMaxSize())
|
||||
stepperComponent?.Content(Modifier)
|
||||
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation(slide()),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
it.instance.Content(Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -28,7 +29,7 @@ internal class AddExistingWalletImportComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
interface ModelCallbacks {
|
||||
fun onWalletImported()
|
||||
fun onWalletImported(userWalletId: UserWalletId)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.im.port.entity
|
||||
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -13,11 +12,10 @@ internal data class AddExistingWalletImportUM(
|
|||
val onPassphraseInfoClick: () -> Unit,
|
||||
val wordsErrorText: TextReference?,
|
||||
val invalidWords: ImmutableList<String>,
|
||||
val createWalletEnabled: Boolean,
|
||||
val createWalletProgress: Boolean,
|
||||
val createWalletClick: () -> Unit,
|
||||
val importWalletEnabled: Boolean,
|
||||
val importWalletProgress: Boolean,
|
||||
val importWalletClick: () -> Unit,
|
||||
val suggestionsList: ImmutableList<String>,
|
||||
val onSuggestionClick: (String) -> Unit,
|
||||
val infoBottomSheetConfig: TangemBottomSheetConfig,
|
||||
val readyToImport: Boolean,
|
||||
)
|
||||
|
|
@ -1,29 +1,68 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.im.port.model
|
||||
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2
|
||||
import com.tangem.core.ui.components.bottomsheets.message.icon
|
||||
import com.tangem.core.ui.components.bottomsheets.message.infoBlock
|
||||
import com.tangem.core.ui.components.bottomsheets.message.onClick
|
||||
import com.tangem.core.ui.components.bottomsheets.message.secondaryButton
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.domain.core.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.hotwallet.MnemonicRepository
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class AddExistingWalletImportModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val mnemonicRepository: MnemonicRepository,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
|
||||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val params: AddExistingWalletImportComponent.Params = paramsContainer.require()
|
||||
|
||||
private val importSeedPhraseUiStateBuilder: ImportSeedPhraseUiStateBuilder
|
||||
|
||||
private val passphraseInfoAlertBS
|
||||
get() = bottomSheetMessage {
|
||||
infoBlock {
|
||||
icon(R.drawable.ic_passcode_lock_56) {
|
||||
type = MessageBottomSheetUMV2.Icon.Type.Accent
|
||||
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
|
||||
}
|
||||
title = resourceReference(R.string.common_passphrase)
|
||||
body = resourceReference(R.string.onboarding_bottom_sheet_passphrase_description)
|
||||
}
|
||||
secondaryButton {
|
||||
text = resourceReference(R.string.common_got_it)
|
||||
onClick { closeBs() }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder(
|
||||
modelScope = modelScope,
|
||||
|
|
@ -36,6 +75,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
passphrase = passphrase,
|
||||
)
|
||||
},
|
||||
onPassphraseInfoClick = ::onPassphraseInfoClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +84,43 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {
|
||||
// TODO implement importing seed phrase
|
||||
params.callbacks.onWalletImported()
|
||||
modelScope.launch {
|
||||
setImportProgress(true)
|
||||
|
||||
runCatching {
|
||||
val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth)
|
||||
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
|
||||
val userWallet = hotUserWalletBuilder.build()
|
||||
saveUserWalletUseCase.invoke(userWallet.copy(backedUp = true))
|
||||
.onLeft {
|
||||
setImportProgress(false)
|
||||
when (it) {
|
||||
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
|
||||
is SaveWalletError.WalletAlreadySaved -> {
|
||||
uiMessageSender.send(
|
||||
SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onRight {
|
||||
setImportProgress(false)
|
||||
params.callbacks.onWalletImported(userWallet.walletId)
|
||||
}
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
setImportProgress(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setImportProgress(progress: Boolean) {
|
||||
uiState.update {
|
||||
it.copy(importWalletProgress = progress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPassphraseInfoClick() {
|
||||
uiMessageSender.send(passphraseInfoAlertBS)
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import androidx.compose.ui.text.TextRange
|
|||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.crypto.bip39.MnemonicErrorResult
|
||||
|
|
@ -24,6 +23,7 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
private val readyToImport: (Boolean) -> Unit,
|
||||
private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit,
|
||||
private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit,
|
||||
private val onPassphraseInfoClick: () -> Unit,
|
||||
) {
|
||||
private val wordsCheckJobHolder = JobHolder()
|
||||
private var importedMnemonic: Mnemonic? = null
|
||||
|
|
@ -35,8 +35,8 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
passPhrase = TextFieldValue(""),
|
||||
wordsErrorText = null,
|
||||
invalidWords = persistentListOf(),
|
||||
createWalletEnabled = false,
|
||||
createWalletProgress = false,
|
||||
importWalletEnabled = false,
|
||||
importWalletProgress = false,
|
||||
suggestionsList = persistentListOf(),
|
||||
wordsChange = {
|
||||
launchInterceptWords(wordsField = it)
|
||||
|
|
@ -49,11 +49,10 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
passphrase = it.text
|
||||
updateUiState { state -> state.copy(passPhrase = it) }
|
||||
},
|
||||
onPassphraseInfoClick = ::showInfoBS,
|
||||
createWalletClick = ::onCreateWallet,
|
||||
onPassphraseInfoClick = onPassphraseInfoClick,
|
||||
importWalletClick = ::onCreateWallet,
|
||||
onSuggestionClick = { word -> addSuggestedWord(word) },
|
||||
readyToImport = false,
|
||||
infoBottomSheetConfig = TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -115,7 +114,7 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
|
||||
updateUiState {
|
||||
it.copy(
|
||||
createWalletEnabled = false,
|
||||
importWalletEnabled = false,
|
||||
wordsErrorText = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -130,7 +129,7 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
it.copy(
|
||||
invalidWords = invalidWords.toImmutableList(),
|
||||
wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_wrong_words),
|
||||
createWalletEnabled = false,
|
||||
importWalletEnabled = false,
|
||||
)
|
||||
}
|
||||
return
|
||||
|
|
@ -143,7 +142,7 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
it.copy(
|
||||
invalidWords = emptyList<String>().toImmutableList(),
|
||||
wordsErrorText = null,
|
||||
createWalletEnabled = true,
|
||||
importWalletEnabled = true,
|
||||
)
|
||||
}
|
||||
readyToImport(true)
|
||||
|
|
@ -154,13 +153,13 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
updateUiState {
|
||||
it.copy(
|
||||
wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_invalid_checksum),
|
||||
createWalletEnabled = false,
|
||||
importWalletEnabled = false,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
updateUiState {
|
||||
it.copy(
|
||||
createWalletEnabled = false,
|
||||
importWalletEnabled = false,
|
||||
wordsErrorText = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -168,19 +167,6 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
private fun showInfoBS() {
|
||||
updateUiState { state ->
|
||||
state.copy(
|
||||
infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty.copy(
|
||||
isShown = true,
|
||||
onDismissRequest = {
|
||||
updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty) }
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MINIMUM_WORD_LENGTH = 2
|
||||
private const val WORDS_INTERCEPT_DELAY_MS = 500L
|
||||
|
|
|
|||
|
|
@ -35,9 +35,8 @@ import com.tangem.core.ui.R
|
|||
import com.tangem.core.ui.components.Keyboard
|
||||
import com.tangem.core.ui.components.Notifier
|
||||
import com.tangem.core.ui.components.OutlineTextFieldWithIcon
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.TangemTextFieldsDefault
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.keyboardAsState
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation
|
||||
|
|
@ -91,15 +90,14 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo
|
|||
)
|
||||
}
|
||||
|
||||
PrimaryButtonIconEnd(
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_import),
|
||||
iconResId = R.drawable.ic_tangem_24,
|
||||
enabled = state.createWalletEnabled,
|
||||
showProgress = state.createWalletProgress,
|
||||
onClick = state.createWalletClick,
|
||||
enabled = state.importWalletEnabled,
|
||||
showProgress = state.importWalletProgress,
|
||||
onClick = state.importWalletClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -114,8 +112,6 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
PassphraseInfoBottomSheet(state.infoBottomSheetConfig)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -229,12 +225,11 @@ private fun PreviewAddExistingWalletImportContent() {
|
|||
onPassphraseInfoClick = {},
|
||||
wordsErrorText = null,
|
||||
invalidWords = persistentListOf(),
|
||||
createWalletEnabled = false,
|
||||
createWalletProgress = false,
|
||||
createWalletClick = {},
|
||||
importWalletEnabled = false,
|
||||
importWalletProgress = false,
|
||||
importWalletClick = {},
|
||||
suggestionsList = persistentListOf(),
|
||||
onSuggestionClick = {},
|
||||
infoBottomSheetConfig = TangemBottomSheetConfig.Empty,
|
||||
readyToImport = false,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.im.port.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) {
|
||||
TangemBottomSheet(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
) { _: TangemBottomSheetConfigContent.Empty ->
|
||||
PassphraseInfoBottomSheetContent(config.onDismissRequest)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(top = TangemTheme.dimens.size40)
|
||||
.size(TangemTheme.dimens.size48),
|
||||
painter = painterResource(id = R.drawable.ic_information_24),
|
||||
tint = TangemTheme.colors.icon.accent,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.common_passphrase),
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.size40)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description),
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.size16)
|
||||
.padding(horizontal = TangemTheme.dimens.size24)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.size16)
|
||||
.padding(top = TangemTheme.dimens.size40)
|
||||
.padding(bottom = TangemTheme.dimens.size32)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(id = R.string.common_ok),
|
||||
onClick = onDismiss,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PassphraseInfoBottomSheetContentPreview() {
|
||||
TangemThemePreview {
|
||||
PassphraseInfoBottomSheetContent({ })
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.root
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.push
|
||||
import com.arkivanov.decompose.router.stack.replaceCurrent
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.domain.settings.ShouldAskPermissionUseCase
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
|
||||
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
|
||||
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class AddExistingWalletModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
|
||||
) : Model() {
|
||||
|
||||
val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks()
|
||||
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
|
||||
val manualBackupCompletedComponentModelCallbacks = ManualBackupCompletedComponentModelCallbacks()
|
||||
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
|
||||
val mobileWalletSetupFinishedComponentModelCallbacks = MobileWalletSetupFinishedComponentModelCallbacks()
|
||||
|
||||
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
|
||||
|
||||
fun onChildBack(currentRoute: AddExistingWalletRoute) {
|
||||
when (currentRoute) {
|
||||
AddExistingWalletRoute.Import -> stackNavigation.pop()
|
||||
AddExistingWalletRoute.BackupCompleted -> Unit
|
||||
AddExistingWalletRoute.AccessCode -> stackNavigation.pop()
|
||||
AddExistingWalletRoute.PushNotifications -> Unit
|
||||
AddExistingWalletRoute.SetupFinished -> Unit
|
||||
AddExistingWalletRoute.Start -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
inner class AddExistingWalletStartModelCallbacks : AddExistingWalletStartComponent.ModelCallbacks {
|
||||
override fun onBackClick() {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
override fun onImportPhraseClick() {
|
||||
stackNavigation.push(AddExistingWalletRoute.Import)
|
||||
}
|
||||
}
|
||||
|
||||
inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks {
|
||||
override fun onWalletImported() {
|
||||
stackNavigation.replaceCurrent(AddExistingWalletRoute.BackupCompleted)
|
||||
}
|
||||
}
|
||||
|
||||
inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks {
|
||||
override fun onContinueClick() {
|
||||
stackNavigation.push(AddExistingWalletRoute.AccessCode)
|
||||
}
|
||||
}
|
||||
|
||||
inner class AccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks {
|
||||
override fun onBackClick() {
|
||||
stackNavigation.pop()
|
||||
}
|
||||
|
||||
override fun onAccessCodeSet() {
|
||||
modelScope.launch {
|
||||
val shouldRequestPush = shouldAskPermissionUseCase(PUSH_PERMISSION)
|
||||
if (shouldRequestPush) {
|
||||
// is yet blocked by [REDACTED_TASK_KEY]
|
||||
// stackNavigation.replaceCurrent(AddExistingWalletRoute.PushNotifications)
|
||||
stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished)
|
||||
} else {
|
||||
stackNavigation.replaceCurrent(AddExistingWalletRoute.SetupFinished)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class MobileWalletSetupFinishedComponentModelCallbacks : MobileWalletSetupFinishedComponent.ModelCallbacks {
|
||||
override fun onContinueClick() {
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.root.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.hotwallet.AddExistingWalletComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
|
||||
import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface AddExistingWalletModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindAddExistingWalletComponentFactory(
|
||||
impl: DefaultAddExistingWalletComponent.Factory,
|
||||
): AddExistingWalletComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(AddExistingWalletModel::class)
|
||||
fun bindAddExistingWalletModel(model: AddExistingWalletModel): Model
|
||||
}
|
||||
|
|
@ -1,18 +1,63 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.start
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
|
||||
import com.tangem.domain.card.analytics.Shop
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.core.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class AddExistingWalletStartModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val params: AddExistingWalletStartComponent.Params = paramsContainer.require()
|
||||
|
|
@ -20,10 +65,119 @@ internal class AddExistingWalletStartModel @Inject constructor(
|
|||
internal val uiState: StateFlow<AddExistingWalletStartUM>
|
||||
field = MutableStateFlow(
|
||||
AddExistingWalletStartUM(
|
||||
isScanInProgress = false,
|
||||
onBackClick = params.callbacks::onBackClick,
|
||||
onImportPhraseClick = params.callbacks::onImportPhraseClick,
|
||||
onScanCardClick = { /* [REDACTED_TODO_COMMENT] */ },
|
||||
onBuyCardClick = { /* [REDACTED_TODO_COMMENT] */ },
|
||||
onScanCardClick = ::onScanClick,
|
||||
onBuyCardClick = ::onShopClick,
|
||||
),
|
||||
)
|
||||
|
||||
private fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
|
||||
scanCard()
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
modelScope.launch {
|
||||
setLoading(true)
|
||||
|
||||
val shouldSaveAccessCodes = settingsRepository.shouldSaveAccessCodes()
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(
|
||||
isBiometricsRequestPolicy = shouldSaveAccessCodes,
|
||||
)
|
||||
|
||||
val analyticsSource = AnalyticsParam.ScreensSources.Intro
|
||||
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = analyticsSource,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (!showProgress) {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
} else {
|
||||
setLoading(true)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
handleScanError(error)
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
},
|
||||
onSuccess = { scanResponse ->
|
||||
proceedWithScanResponse(scanResponse)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
|
||||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("User wallet not created")
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
saveWalletUseCase(userWallet).fold(
|
||||
ifLeft = {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
when (it) {
|
||||
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
|
||||
is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet)
|
||||
}
|
||||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
walletsCount = userWalletsListManager.walletsCount.toString(),
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
uiState.update { it.copy(isScanInProgress = isLoading) }
|
||||
}
|
||||
|
||||
fun handleScanError(error: TangemError) {
|
||||
when (error) {
|
||||
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
|
||||
is TangemSdkError -> Timber.e(error, "Scan error occurred")
|
||||
else -> Timber.e(error, "Error happened")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNfcFeatureUnavailable() {
|
||||
uiMessageSender.send(
|
||||
message = DialogMessage(
|
||||
message = resourceReference(R.string.nfc_error_unavailable),
|
||||
title = resourceReference(id = R.string.common_error),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.start.entity
|
||||
|
||||
internal data class AddExistingWalletStartUM(
|
||||
val isScanInProgress: Boolean,
|
||||
val onBackClick: () -> Unit,
|
||||
val onImportPhraseClick: () -> Unit,
|
||||
val onScanCardClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.addexistingwallet.start.ui
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -74,14 +75,25 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
|
|||
title = stringResourceSafe(R.string.wallet_import_scan_title),
|
||||
description = stringResourceSafe(R.string.wallet_import_scan_description),
|
||||
badge = {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp)
|
||||
.size(20.dp),
|
||||
painter = painterResource(R.drawable.ic_tangem_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
if (state.isScanInProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp)
|
||||
.size(20.dp)
|
||||
.padding(2.dp),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(top = 2.dp)
|
||||
.size(20.dp),
|
||||
painter = painterResource(R.drawable.ic_tangem_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
}
|
||||
},
|
||||
onClick = state.onScanCardClick,
|
||||
enabled = true,
|
||||
|
|
@ -160,6 +172,7 @@ private fun PreviewCreateWalletContent() {
|
|||
TangemThemePreview {
|
||||
AddExistingWalletStartContent(
|
||||
state = AddExistingWalletStartUM(
|
||||
isScanInProgress = true,
|
||||
onBackClick = {},
|
||||
onImportPhraseClick = {},
|
||||
onScanCardClick = {},
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.tangem.core.ui.extensions.clickableSingle
|
|||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
internal const val DISABLED_COLORS_ALPHA = 0.5f
|
||||
private const val DISABLED_COLORS_ALPHA = 0.5f
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -45,10 +45,9 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
runCatching {
|
||||
val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12)
|
||||
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
|
||||
saveUserWalletUseCase(
|
||||
hotUserWalletBuilder.build(),
|
||||
)
|
||||
router.push(AppRoute.Wallet)
|
||||
val userWallet = hotUserWalletBuilder.build()
|
||||
saveUserWalletUseCase(userWallet)
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
}.onFailure {
|
||||
Timber.e(it)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
package com.tangem.features.hotwallet.createwalletbackup
|
||||
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.pop
|
||||
import com.arkivanov.decompose.router.stack.push
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
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.models.wallet.UserWalletId
|
||||
import com.tangem.features.hotwallet.CreateWalletBackupComponent
|
||||
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
|
||||
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.completed.ManualBackupCompletedComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
|
||||
import com.tangem.features.hotwallet.manualbackup.start.ManualBackupStartComponent
|
||||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class CreateWalletBackupModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
) : Model() {
|
||||
|
||||
val params = paramsContainer.require<CreateWalletBackupComponent.Params>()
|
||||
|
||||
val hotWalletStepperComponentModelCallback = HotWalletStepperComponentModelCallback()
|
||||
val manualBackupStartModelCallbacks = ManualBackupStartModelCallbacks()
|
||||
val manualBackupPhraseModelCallbacks = ManualBackupPhraseModelCallbacks()
|
||||
val manualBackupCheckModelCallbacks = ManualBackupCheckModelCallbacks()
|
||||
val manualBackupCompletedModelCallbacks = ManualBackupCompletedModelCallbacks()
|
||||
|
||||
val stackNavigation = StackNavigation<CreateWalletBackupRoute>()
|
||||
val startRoute = CreateWalletBackupRoute.RecoveryPhraseStart
|
||||
val currentRoute: MutableStateFlow<CreateWalletBackupRoute> = MutableStateFlow(startRoute)
|
||||
|
||||
fun onBack() {
|
||||
when (currentRoute.value) {
|
||||
is CreateWalletBackupRoute.RecoveryPhraseStart -> router.pop()
|
||||
is CreateWalletBackupRoute.RecoveryPhrase -> stackNavigation.pop()
|
||||
is CreateWalletBackupRoute.ConfirmBackup -> stackNavigation.pop()
|
||||
is CreateWalletBackupRoute.BackupCompleted -> router.pop()
|
||||
}
|
||||
}
|
||||
|
||||
fun onManualBackupStarted() {
|
||||
stackNavigation.push(CreateWalletBackupRoute.RecoveryPhrase)
|
||||
}
|
||||
|
||||
fun onManualBackupPhraseShown() {
|
||||
stackNavigation.push(CreateWalletBackupRoute.ConfirmBackup)
|
||||
}
|
||||
|
||||
fun onManualBackupChecked() {
|
||||
stackNavigation.push(CreateWalletBackupRoute.BackupCompleted)
|
||||
}
|
||||
|
||||
fun onManualBackupCompleted() {
|
||||
router.pop()
|
||||
}
|
||||
|
||||
inner class HotWalletStepperComponentModelCallback : HotWalletStepperComponent.ModelCallback {
|
||||
override fun onBackClick() {
|
||||
onBack()
|
||||
}
|
||||
|
||||
override fun onSkipClick() = Unit
|
||||
}
|
||||
|
||||
inner class ManualBackupStartModelCallbacks : ManualBackupStartComponent.ModelCallbacks {
|
||||
override fun onContinueClick() {
|
||||
onManualBackupStarted()
|
||||
}
|
||||
}
|
||||
|
||||
inner class ManualBackupPhraseModelCallbacks : ManualBackupPhraseComponent.ModelCallbacks {
|
||||
override fun onContinueClick() {
|
||||
onManualBackupPhraseShown()
|
||||
}
|
||||
}
|
||||
|
||||
inner class ManualBackupCheckModelCallbacks : ManualBackupCheckComponent.ModelCallbacks {
|
||||
override fun onCompleteClick() {
|
||||
onManualBackupChecked()
|
||||
}
|
||||
}
|
||||
|
||||
inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks {
|
||||
override fun onContinueClick(userWalletId: UserWalletId) {
|
||||
onManualBackupCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.features.hotwallet.createwalletbackup
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupRoute
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class CreateWalletBackupStepperStateManager @Inject constructor() {
|
||||
|
||||
fun getStepperState(route: CreateWalletBackupRoute): HotWalletStepperComponent.StepperUM? {
|
||||
return when (route) {
|
||||
is CreateWalletBackupRoute.RecoveryPhraseStart -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_START,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is CreateWalletBackupRoute.RecoveryPhrase -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_PHRASE,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is CreateWalletBackupRoute.ConfirmBackup -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_CONFIRM,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is CreateWalletBackupRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_COMPLETED,
|
||||
steps = STEPS_COUNT,
|
||||
title = resourceReference(R.string.common_done),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STEPS_COUNT = 4
|
||||
|
||||
private const val STEP_START = 1
|
||||
private const val STEP_PHRASE = 2
|
||||
private const val STEP_CONFIRM = 3
|
||||
private const val STEP_COMPLETED = 4
|
||||
}
|
||||
}
|
||||
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