Updated on 2026-08-14
This commit is contained in:
commit
e4754fed3b
483 changed files with 9315 additions and 4084 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ dependencies {
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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.accountName.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.common.ui.account.CryptoPortfolioIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
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: TextReference,
|
||||
val accountIconUM: CryptoPortfolioIconUM,
|
||||
val tokensInfo: TextReference,
|
||||
val onClick: (accountId: String) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
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.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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.account.AccountRow
|
||||
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.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.features.account.archived.entity.AccountArchivedUM
|
||||
import com.tangem.features.account.archived.entity.ArchivedAccountUM
|
||||
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),
|
||||
) {
|
||||
AccountRow(
|
||||
title = item.accountName,
|
||||
subtitle = item.tokensInfo,
|
||||
icon = item.accountIconUM,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
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() = AccountIconPreviewData.randomAccountIcon()
|
||||
val accountName = stringReference("Account name")
|
||||
|
||||
val firstList = List(10) {
|
||||
ArchivedAccountUM(
|
||||
accountId = it.toString(),
|
||||
accountName = accountName,
|
||||
accountIconUM = 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({}, {}))
|
||||
},
|
||||
)
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.features.account.createedit
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.common.ui.account.toDomain
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -9,27 +12,34 @@ 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,
|
||||
|
|
@ -37,13 +47,21 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
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> get() = _uiState
|
||||
private val _uiState = MutableStateFlow(value = getInitialState())
|
||||
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(
|
||||
|
|
@ -74,13 +92,16 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
|
||||
private suspend fun createNewCryptoPortfolio(params: AccountCreateEditComponent.Params.Create) {
|
||||
val state = uiState.value
|
||||
val name = AccountName(state.account.name).getOrNull() ?: return
|
||||
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.Main, // todo account
|
||||
derivationIndex = derivationIndex,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +109,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
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 isNewName = name != params.account.accountName
|
||||
val isNewIcon = icon != params.account.portfolioIcon
|
||||
updateCryptoPortfolioUseCase(
|
||||
icon = if (isNewIcon) icon else null,
|
||||
|
|
@ -100,19 +121,19 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
private fun onCloseClick() = unsaveChangeDialog()
|
||||
|
||||
private fun onIconSelect(icon: CryptoPortfolioIcon.Icon) {
|
||||
_uiState.value = uiState.value
|
||||
uiState.value = uiState.value
|
||||
.updateIconSelect(icon)
|
||||
.validateNewState()
|
||||
}
|
||||
|
||||
private fun onColorSelect(color: CryptoPortfolioIcon.Color) {
|
||||
_uiState.value = uiState.value
|
||||
uiState.value = uiState.value
|
||||
.updateColorSelect(color)
|
||||
.validateNewState()
|
||||
}
|
||||
|
||||
private fun onNameChange(name: String) {
|
||||
_uiState.value = uiState.value
|
||||
uiState.value = uiState.value
|
||||
.updateName(name)
|
||||
.validateNewState()
|
||||
}
|
||||
|
|
@ -122,7 +143,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val isAvailableForConfirm = when (params) {
|
||||
is AccountCreateEditComponent.Params.Create -> isValidName
|
||||
is AccountCreateEditComponent.Params.Edit -> {
|
||||
val isNewName = this.account.name != params.account.name.value
|
||||
val isNewName = this.account.name != params.account.accountName.value
|
||||
val isNewIcon = this.account.portfolioIcon != params.account.portfolioIcon
|
||||
isValidName && (isNewName || isNewIcon)
|
||||
}
|
||||
|
|
@ -140,4 +161,35 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.features.account.createedit.entity
|
||||
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
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(
|
||||
|
|
@ -17,11 +17,23 @@ data class AccountCreateEditUM(
|
|||
data class Account(
|
||||
val name: String,
|
||||
val portfolioIcon: CryptoPortfolioIconUM,
|
||||
val derivationInfo: TextReference,
|
||||
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>,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
package com.tangem.features.account.createedit.entity
|
||||
|
||||
import com.tangem.common.ui.account.toUM
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class AccountCreateEditUMBuilder @Inject constructor(
|
||||
val params: AccountCreateEditComponent.Params,
|
||||
internal class AccountCreateEditUMBuilder(
|
||||
private val params: AccountCreateEditComponent.Params,
|
||||
) {
|
||||
|
||||
private val accountColors = CryptoPortfolioIcon.Color.entries.toImmutableList()
|
||||
|
|
@ -29,14 +29,16 @@ internal class AccountCreateEditUMBuilder @Inject constructor(
|
|||
is AccountCreateEditComponent.Params.Create -> AccountCreateEditUM.Account(
|
||||
name = "",
|
||||
portfolioIcon = createIcon,
|
||||
derivationInfo = TextReference.EMPTY,
|
||||
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,
|
||||
name = params.account.accountName.value,
|
||||
portfolioIcon = params.account.portfolioIcon.toUM(),
|
||||
derivationInfo = TextReference.EMPTY, // todo account use Account.CryptoPortfolio.derivationIndex ?
|
||||
derivationInfo = createAccountDerivationInfo(
|
||||
index = (params.account as Account.CryptoPortfolio).derivationIndex.value,
|
||||
),
|
||||
inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account),
|
||||
onNameChange = onNameChange,
|
||||
)
|
||||
|
|
@ -113,5 +115,25 @@ internal class AccountCreateEditUMBuilder @Inject constructor(
|
|||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,9 @@ 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.AccountIcon
|
||||
import com.tangem.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.account.AccountIconSize
|
||||
import com.tangem.common.ui.account.getResId
|
||||
import com.tangem.common.ui.account.getUiColor
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
|
|
@ -32,14 +35,10 @@ 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.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.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
|
||||
|
|
@ -72,11 +71,11 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi
|
|||
SpacerH24()
|
||||
AccountColor(state.colorsState)
|
||||
SpacerH24()
|
||||
AccountIcon(state.iconsState)
|
||||
AccountIcons(state.iconsState)
|
||||
SpacerH8()
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
text = state.account.derivationInfo.resolveReference(),
|
||||
text = state.account.derivationInfo.text.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
|
@ -93,7 +92,7 @@ internal fun AccountCreateEditContent(state: AccountCreateEditUM, modifier: Modi
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountSummary(account: AccountCreateEditUM.Account) {
|
||||
private fun AccountSummary(account: Account) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
|
|
@ -103,7 +102,11 @@ private fun AccountSummary(account: AccountCreateEditUM.Account) {
|
|||
) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
AccountIcon(account)
|
||||
AccountIcon(
|
||||
name = stringReference(account.name),
|
||||
icon = account.portfolioIcon,
|
||||
size = AccountIconSize.Large,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(
|
||||
|
|
@ -125,34 +128,6 @@ private fun AccountSummary(account: AccountCreateEditUM.Account) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AccountIcon(account: AccountCreateEditUM.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) {
|
||||
|
|
@ -204,7 +179,7 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) {
|
|||
|
||||
@Suppress("LongMethod", "MagicNumber")
|
||||
@Composable
|
||||
private fun AccountIcon(iconsState: AccountCreateEditUM.Icons) {
|
||||
private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) {
|
||||
Box(
|
||||
Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
|
|
@ -299,7 +274,7 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountC
|
|||
buildList {
|
||||
val colors = CryptoPortfolioIcon.Color.entries.toImmutableList()
|
||||
val icons = CryptoPortfolioIcon.Icon.entries.toImmutableList()
|
||||
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
|
||||
var portfolioIcon = AccountIconPreviewData.randomAccountIcon()
|
||||
val first = AccountCreateEditUM(
|
||||
title = stringReference("Add account"),
|
||||
onCloseClick = {},
|
||||
|
|
@ -308,7 +283,10 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountC
|
|||
portfolioIcon = portfolioIcon,
|
||||
inputPlaceholder = resourceReference(R.string.account_form_placeholder_new_account),
|
||||
onNameChange = {},
|
||||
derivationInfo = stringReference("Account #03 — used for address derivation."),
|
||||
derivationInfo = AccountCreateEditUM.DerivationInfo.Content(
|
||||
text = resourceReference(id = R.string.account_form_account_index, formatArgs = wrappedList(1)),
|
||||
index = 1,
|
||||
),
|
||||
),
|
||||
colorsState = AccountCreateEditUM.Colors(
|
||||
selected = portfolioIcon.color,
|
||||
|
|
@ -328,19 +306,20 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountC
|
|||
)
|
||||
add(first)
|
||||
|
||||
portfolioIcon = portfolioIcon.copy(
|
||||
value = CryptoPortfolioIcon.Icon.Letter,
|
||||
color = CryptoPortfolioIcon.Color.entries.random(),
|
||||
)
|
||||
portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true)
|
||||
val accountName = "Main account"
|
||||
val second = AccountCreateEditUM(
|
||||
title = stringReference("Edit account"),
|
||||
onCloseClick = {},
|
||||
account = Account(
|
||||
portfolioIcon = portfolioIcon,
|
||||
name = "Main account",
|
||||
name = accountName,
|
||||
inputPlaceholder = resourceReference(R.string.account_form_placeholder_edit_account),
|
||||
onNameChange = {},
|
||||
derivationInfo = stringReference("Account #03 — used for address derivation."),
|
||||
derivationInfo = AccountCreateEditUM.DerivationInfo.Content(
|
||||
text = resourceReference(id = R.string.account_form_account_index, formatArgs = wrappedList(1)),
|
||||
index = 1,
|
||||
),
|
||||
),
|
||||
colorsState = AccountCreateEditUM.Colors(
|
||||
selected = portfolioIcon.color,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.account.details
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -12,7 +13,6 @@ 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
|
||||
|
|
@ -74,7 +74,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
|
||||
private fun getInitialState(): AccountDetailsUM {
|
||||
return AccountDetailsUM(
|
||||
accountName = params.account.name.value,
|
||||
accountName = params.account.accountName.value,
|
||||
accountIcon = params.account.portfolioIcon.toUM(),
|
||||
onCloseClick = { router.pop() },
|
||||
onAccountEditClick = ::onEditAccountClick,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.account.details.entity
|
||||
|
||||
import com.tangem.features.account.common.CryptoPortfolioIconUM
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconUM
|
||||
|
||||
data class AccountDetailsUM(
|
||||
val accountName: String,
|
||||
|
|
|
|||
|
|
@ -19,21 +19,18 @@ 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.common.ui.account.AccountIconPreviewData
|
||||
import com.tangem.common.ui.account.AccountRow
|
||||
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.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.common.CryptoPortfolioIconUM
|
||||
import com.tangem.features.account.common.toUM
|
||||
import com.tangem.features.account.details.entity.AccountDetailsUM
|
||||
|
||||
@Composable
|
||||
|
|
@ -145,32 +142,13 @@ private fun AccountRow(state: AccountDetailsUM) {
|
|||
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,
|
||||
AccountRow(
|
||||
title = stringReference(state.accountName),
|
||||
subtitle = resourceReference(R.string.account_form_name),
|
||||
icon = state.accountIcon,
|
||||
modifier = Modifier.weight(1f),
|
||||
isReverse = true,
|
||||
)
|
||||
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(
|
||||
|
|
@ -181,31 +159,6 @@ private fun AccountRow(state: AccountDetailsUM) {
|
|||
}
|
||||
}
|
||||
|
||||
// todo account make reusable
|
||||
@Composable
|
||||
private 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
|
||||
|
|
@ -217,20 +170,18 @@ private fun WcConnectionsContentPreview(@PreviewParameter(PreviewStateProvider::
|
|||
|
||||
private class PreviewStateProvider : CollectionPreviewParameterProvider<AccountDetailsUM>(
|
||||
buildList {
|
||||
var portfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount().toUM()
|
||||
val accountName = "Main"
|
||||
var portfolioIcon = AccountIconPreviewData.randomAccountIcon()
|
||||
val first = AccountDetailsUM(
|
||||
onCloseClick = {},
|
||||
onAccountEditClick = {},
|
||||
onManageTokensClick = {},
|
||||
onArchiveAccountClick = {},
|
||||
accountName = "Main",
|
||||
accountName = accountName,
|
||||
accountIcon = portfolioIcon,
|
||||
)
|
||||
add(first)
|
||||
portfolioIcon = portfolioIcon.copy(
|
||||
value = CryptoPortfolioIcon.Icon.Letter,
|
||||
color = CryptoPortfolioIcon.Color.entries.random(),
|
||||
)
|
||||
portfolioIcon = AccountIconPreviewData.randomAccountIcon(letter = true)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ dependencies {
|
|||
implementation(projects.features.wallet.api)
|
||||
implementation(projects.features.disclaimer.api)
|
||||
implementation(projects.features.tester.api)
|
||||
implementation(projects.features.createWalletSelection.api)
|
||||
implementation(projects.features.hotWallet.api)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.details.entity
|
|||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
|
|
@ -11,4 +12,5 @@ internal data class UserWalletListUM(
|
|||
val isWalletSavingInProgress: Boolean,
|
||||
val addNewWalletText: TextReference,
|
||||
val onAddNewWalletClick: () -> Unit,
|
||||
val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
|
|
@ -6,12 +6,19 @@ 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.components.bottomsheets.BottomSheetOption
|
||||
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheetContent
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase
|
||||
import com.tangem.features.details.entity.UserWalletListUM
|
||||
import com.tangem.features.details.impl.R
|
||||
import com.tangem.features.details.utils.UserWalletSaver
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.wallet.utils.UserWalletsFetcher
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -20,22 +27,28 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class UserWalletListModel @Inject constructor(
|
||||
userWalletsFetcherFactory: UserWalletsFetcher.Factory,
|
||||
shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase,
|
||||
private val router: Router,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val userWalletSaver: UserWalletSaver,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val userWalletSaver: UserWalletSaver,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
|
||||
private val userWalletsFetcher = userWalletsFetcherFactory.create(
|
||||
messageSender = messageSender,
|
||||
onlyMultiCurrency = false,
|
||||
authMode = false,
|
||||
onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) },
|
||||
)
|
||||
|
||||
|
|
@ -44,7 +57,8 @@ internal class UserWalletListModel @Inject constructor(
|
|||
userWallets = persistentListOf(),
|
||||
isWalletSavingInProgress = false,
|
||||
addNewWalletText = TextReference.EMPTY,
|
||||
onAddNewWalletClick = ::addUserWallet,
|
||||
onAddNewWalletClick = ::onAddNewWalletClick,
|
||||
addWalletBottomSheet = TangemBottomSheetConfig.Empty,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -53,8 +67,9 @@ internal class UserWalletListModel @Inject constructor(
|
|||
flow = userWalletsFetcher.userWallets,
|
||||
flow2 = shouldSaveUserWalletsUseCase(),
|
||||
flow3 = isWalletSavingInProgress,
|
||||
transform = ::updateState,
|
||||
).launchIn(modelScope)
|
||||
) { userWallets, shouldSaveUserWallets, isWalletSavingInProgress ->
|
||||
updateState(userWallets, shouldSaveUserWallets, isWalletSavingInProgress)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun updateState(
|
||||
|
|
@ -65,7 +80,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
value.copy(
|
||||
userWallets = userWallets,
|
||||
isWalletSavingInProgress = isWalletSavingInProgress,
|
||||
addNewWalletText = if (shouldSaveUserWallets) {
|
||||
addNewWalletText = if (shouldSaveUserWallets || hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
resourceReference(R.string.user_wallet_list_add_button)
|
||||
} else {
|
||||
resourceReference(R.string.scan_card_settings_button)
|
||||
|
|
@ -73,7 +88,64 @@ internal class UserWalletListModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
private fun addUserWallet() = withProgress(isWalletSavingInProgress) {
|
||||
userWalletSaver.scanAndSaveUserWallet(modelScope)
|
||||
private fun onAddNewWalletClick() {
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
addWalletBottomSheet = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = ::dismissAddWalletBottomSheet,
|
||||
content = createAddWalletBottomSheetContent(),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
withProgress(isWalletSavingInProgress) {
|
||||
userWalletSaver.scanAndSaveUserWallet(modelScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissAddWalletBottomSheet() {
|
||||
state.update { currentState ->
|
||||
currentState.copy(
|
||||
addWalletBottomSheet = currentState.addWalletBottomSheet.copy(isShown = false),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createAddWalletBottomSheetContent(): OptionsBottomSheetContent {
|
||||
return OptionsBottomSheetContent(
|
||||
options = persistentListOf(
|
||||
BottomSheetOption(
|
||||
key = ADD_WALLET_KEY_CREATE,
|
||||
label = resourceReference(string.home_button_create_new_wallet),
|
||||
),
|
||||
BottomSheetOption(
|
||||
key = ADD_WALLET_KEY_ADD,
|
||||
label = resourceReference(string.home_button_add_existing_wallet),
|
||||
),
|
||||
BottomSheetOption(
|
||||
key = ADD_WALLET_KEY_BUY,
|
||||
label = resourceReference(string.details_buy_wallet),
|
||||
),
|
||||
),
|
||||
onOptionClick = { optionKey ->
|
||||
dismissAddWalletBottomSheet()
|
||||
when (optionKey) {
|
||||
ADD_WALLET_KEY_CREATE -> router.push(AppRoute.CreateWalletSelection)
|
||||
ADD_WALLET_KEY_ADD -> router.push(AppRoute.AddExistingWallet)
|
||||
ADD_WALLET_KEY_BUY -> modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ADD_WALLET_KEY_CREATE = "create"
|
||||
private const val ADD_WALLET_KEY_ADD = "add"
|
||||
private const val ADD_WALLET_KEY_BUY = "buy"
|
||||
}
|
||||
}
|
||||
|
|
@ -15,9 +15,13 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import com.tangem.common.ui.userwallet.UserWalletItem
|
||||
import com.tangem.core.ui.R.*
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.bottomsheets.OptionsBottomSheet
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.details.component.UserWalletListComponent
|
||||
|
|
@ -44,6 +48,8 @@ internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = M
|
|||
onClick = state.onAddNewWalletClick,
|
||||
)
|
||||
}
|
||||
|
||||
AddWalletBottomSheet(state.addWalletBottomSheet)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -94,6 +100,15 @@ private fun AddWalletButton(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddWalletBottomSheet(config: TangemBottomSheetConfig) {
|
||||
OptionsBottomSheet(
|
||||
config = config,
|
||||
title = resourceReference(string.auth_info_add_wallet_title),
|
||||
containerColor = TangemTheme.colors.background.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ internal class DisclaimerModel @Inject constructor(
|
|||
val shouldAskPushPermission = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate()
|
||||
val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase()
|
||||
if (shouldAskPushPermission && !isHuaweiDevice) {
|
||||
router.push(AppRoute.PushNotification)
|
||||
router.push(AppRoute.PushNotification(AppRoute.PushNotification.Source.Stories))
|
||||
} else {
|
||||
neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION)
|
||||
neverRequestPermissionUseCase(PUSH_PERMISSION)
|
||||
|
|
|
|||
|
|
@ -51,9 +51,6 @@ dependencies {
|
|||
implementation(deps.compose.coil)
|
||||
implementation(deps.decompose.ext.compose)
|
||||
|
||||
/** Firebase */
|
||||
implementation(deps.firebase.analytics)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.card.android)
|
||||
implementation(tangemDeps.card.core)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.features.home.impl.analytics
|
||||
|
||||
internal sealed class AnalyticsParam {
|
||||
|
||||
sealed class CurrencyType(val value: String) {
|
||||
class Blockchain(blockchain: com.tangem.blockchain.common.Blockchain) : CurrencyType(blockchain.currency)
|
||||
class Token(token: com.tangem.blockchain.common.Token) : CurrencyType(token.symbol)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.features.home.impl.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
sealed class IntroductionProcess(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Introduction Process", event, params) {
|
||||
|
||||
object ScreenOpened : IntroductionProcess("Introduction Process Screen Opened")
|
||||
object ButtonTokensList : IntroductionProcess("Button - Tokens List")
|
||||
object ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
|
||||
object ButtonScanCard : IntroductionProcess("Button - Scan Card")
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.features.home.impl.analytics
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.card.CardTypesResolver
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
|
||||
|
||||
internal class ParamCardCurrencyConverter : Converter<CardTypesResolver, CoreAnalyticsParam.WalletType?> {
|
||||
|
||||
override fun convert(value: CardTypesResolver): CoreAnalyticsParam.WalletType? {
|
||||
if (value.isMultiwalletAllowed()) return CoreAnalyticsParam.WalletType.MultiCurrency
|
||||
|
||||
val type = when {
|
||||
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
|
||||
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
|
||||
value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
|
||||
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
|
||||
else -> null
|
||||
} ?: return null
|
||||
|
||||
return CoreAnalyticsParam.WalletType.SingleCurrency(type.value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.features.home.impl.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
internal sealed class Shop(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Shop", event, params) {
|
||||
|
||||
object ScreenOpened : Shop("Shop Screen Opened")
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.features.home.impl.model
|
||||
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.routing.AppRoute
|
||||
|
|
@ -23,20 +21,22 @@ 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.domain.wallets.usecase.SelectWalletUseCase
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.features.home.impl.analytics.IntroductionProcess
|
||||
import com.tangem.features.home.impl.analytics.ParamCardCurrencyConverter
|
||||
import com.tangem.features.home.impl.analytics.Shop
|
||||
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
|
||||
|
|
@ -64,16 +64,17 @@ internal class HomeModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val router: Router,
|
||||
private val selectWalletUseCase: SelectWalletUseCase,
|
||||
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() {
|
||||
|
||||
|
|
@ -135,10 +136,9 @@ internal class HomeModel @Inject constructor(
|
|||
private fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
|
||||
Firebase.analytics.appInstanceId
|
||||
.addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") }
|
||||
.addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) }
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchTokensClick() {
|
||||
|
|
@ -198,24 +198,17 @@ internal class HomeModel @Inject constructor(
|
|||
|
||||
saveWalletUseCase(userWallet).fold(
|
||||
ifLeft = {
|
||||
Timber.e(it.toString(), "Unable to save user wallet")
|
||||
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)
|
||||
|
||||
// Select the wallet using new mechanism
|
||||
selectWalletUseCase(userWallet.walletId).fold(
|
||||
ifLeft = {
|
||||
Timber.e("Unable to select user wallet: $it")
|
||||
setLoading(false)
|
||||
},
|
||||
ifRight = {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -228,7 +221,7 @@ internal class HomeModel @Inject constructor(
|
|||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
walletsCount = "1",
|
||||
walletsCount = userWalletsListManager.walletsCount.toString(),
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
|
|
@ -241,15 +234,9 @@ internal class HomeModel @Inject constructor(
|
|||
|
||||
fun handleScanError(error: TangemError) {
|
||||
when (error) {
|
||||
is TangemSdkError.NfcFeatureIsUnavailable -> {
|
||||
handleNfcFeatureUnavailable()
|
||||
}
|
||||
is TangemSdkError -> {
|
||||
Timber.e(error, "Scan error occurred")
|
||||
}
|
||||
else -> {
|
||||
Timber.e(error, "Error happened")
|
||||
}
|
||||
is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable()
|
||||
is TangemSdkError -> Timber.e(error, "Scan error occurred")
|
||||
else -> Timber.e(error, "Error happened")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -261,8 +248,4 @@ internal class HomeModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app"
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ 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.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
|
|
@ -27,7 +28,8 @@ internal class AccessCodeModel @Inject constructor(
|
|||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -77,15 +79,45 @@ internal class AccessCodeModel @Inject constructor(
|
|||
runCatching {
|
||||
val userWallet = getUserWalletUseCase(userWalletId)
|
||||
.getOrElse { error("User wallet with id $userWalletId not found") }
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth)
|
||||
val updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
auth = HotAuth.Password(accessCode.toCharArray()),
|
||||
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,
|
||||
)
|
||||
saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId), canOverride = true)
|
||||
params.callbacks.onAccessCodeConfirmed(params.userWalletId)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ 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
|
||||
|
|
@ -73,8 +74,9 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
|
|||
) {
|
||||
PinTextField(
|
||||
length = state.accessCodeLength,
|
||||
isPasswordVisual = true,
|
||||
isPasswordVisual = !state.isConfirmMode,
|
||||
value = state.accessCode,
|
||||
pinTextColor = PinTextColor.Primary,
|
||||
onValueChange = state.onAccessCodeChange,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,13 @@ internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override suspend fun successfulAuthentication() {
|
||||
// TODO handle successful authentication
|
||||
// TODO add delay
|
||||
model.successfulAuthentication()
|
||||
}
|
||||
|
||||
override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result {
|
||||
model.show(hasBiometry)
|
||||
override suspend fun requestPassword(
|
||||
attemptRequest: HotWalletPasswordRequester.AttemptRequest,
|
||||
): HotWalletPasswordRequester.Result {
|
||||
model.show(attemptRequest)
|
||||
return model.waitResult()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,37 +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.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,
|
||||
)
|
||||
}
|
||||
|
|
@ -42,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,
|
||||
|
|
@ -66,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) {
|
||||
|
|
@ -78,6 +128,68 @@ 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)
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
|
|
|
|||
|
|
@ -13,20 +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 successfulAuthentication() {
|
||||
call { successfulAuthentication() }
|
||||
}
|
||||
override suspend fun successfulAuthentication() = call { successfulAuthentication() }
|
||||
|
||||
override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result =
|
||||
call { requestPassword(hasBiometry) }
|
||||
override suspend fun requestPassword(
|
||||
attemptRequest: HotWalletPasswordRequester.AttemptRequest,
|
||||
): HotWalletPasswordRequester.Result = call { requestPassword(attemptRequest) }
|
||||
|
||||
override suspend fun dismiss() {
|
||||
call { dismiss() }
|
||||
}
|
||||
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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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
|
||||
|
|
@ -63,6 +64,7 @@ internal class AddExistingWalletChildFactory @Inject constructor(
|
|||
context = childContext,
|
||||
params = PushNotificationsParams(
|
||||
modelCallbacks = model.pushNotificationsCallbacks,
|
||||
source = AppRoute.PushNotification.Source.Onboarding,
|
||||
),
|
||||
)
|
||||
is AddExistingWalletRoute.SetupFinished -> MobileWalletSetupFinishedComponent(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -18,6 +17,5 @@ internal data class AddExistingWalletImportUM(
|
|||
val importWalletClick: () -> Unit,
|
||||
val suggestionsList: ImmutableList<String>,
|
||||
val onSuggestionClick: (String) -> Unit,
|
||||
val infoBottomSheetConfig: TangemBottomSheetConfig,
|
||||
val readyToImport: Boolean,
|
||||
)
|
||||
|
|
@ -1,9 +1,21 @@
|
|||
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
|
||||
|
|
@ -19,6 +31,7 @@ import kotlinx.coroutines.launch
|
|||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class AddExistingWalletImportModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
|
|
@ -27,12 +40,29 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
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,
|
||||
|
|
@ -45,6 +75,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
passphrase = passphrase,
|
||||
)
|
||||
},
|
||||
onPassphraseInfoClick = ::onPassphraseInfoClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -54,23 +85,42 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
@Suppress("UnusedPrivateMember")
|
||||
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {
|
||||
modelScope.launch {
|
||||
uiState.update {
|
||||
it.copy(importWalletProgress = true)
|
||||
}
|
||||
setImportProgress(true)
|
||||
|
||||
runCatching {
|
||||
val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth)
|
||||
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
|
||||
val userWallet = hotUserWalletBuilder.build()
|
||||
saveUserWalletUseCase(userWallet)
|
||||
params.callbacks.onWalletImported(userWallet.walletId)
|
||||
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)
|
||||
|
||||
uiState.update {
|
||||
it.copy(importWalletProgress = false)
|
||||
}
|
||||
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
|
||||
|
|
@ -49,11 +49,10 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
passphrase = it.text
|
||||
updateUiState { state -> state.copy(passPhrase = it) }
|
||||
},
|
||||
onPassphraseInfoClick = ::showInfoBS,
|
||||
onPassphraseInfoClick = onPassphraseInfoClick,
|
||||
importWalletClick = ::onCreateWallet,
|
||||
onSuggestionClick = { word -> addSuggestedWord(word) },
|
||||
readyToImport = false,
|
||||
infoBottomSheetConfig = TangemBottomSheetConfig.Empty,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ import com.tangem.core.ui.components.Notifier
|
|||
import com.tangem.core.ui.components.OutlineTextFieldWithIcon
|
||||
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
|
||||
|
|
@ -113,8 +112,6 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
PassphraseInfoBottomSheet(state.infoBottomSheetConfig)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -233,7 +230,6 @@ private fun PreviewAddExistingWalletImportContent() {
|
|||
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,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 = {},
|
||||
|
|
|
|||
|
|
@ -45,9 +45,8 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
runCatching {
|
||||
val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12)
|
||||
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
|
||||
saveUserWalletUseCase(
|
||||
hotUserWalletBuilder.build(),
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.features.hotwallet.createwalletbackup
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
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.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.CreateWalletBackupComponent
|
||||
import com.tangem.features.hotwallet.createwalletbackup.routing.CreateWalletBackupChildFactory
|
||||
import com.tangem.features.hotwallet.createwalletbackup.ui.CreateWalletBackupContent
|
||||
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 DefaultCreateWalletBackupComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted private val params: CreateWalletBackupComponent.Params,
|
||||
private val stepperStateManager: CreateWalletBackupStepperStateManager,
|
||||
createWalletBackupChildFactory: CreateWalletBackupChildFactory,
|
||||
stepperComponentFactory: DefaultHotWalletStepperComponent.Factory,
|
||||
) : CreateWalletBackupComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: CreateWalletBackupModel = getOrCreateModel(params)
|
||||
|
||||
private val innerStack = childStack(
|
||||
key = "createWalletBackupInnerStack",
|
||||
source = model.stackNavigation,
|
||||
serializer = null,
|
||||
initialConfiguration = model.startRoute,
|
||||
handleBackButton = true,
|
||||
childFactory = { configuration, factoryContext ->
|
||||
createWalletBackupChildFactory.createChild(
|
||||
route = configuration,
|
||||
childContext = childByContext(factoryContext),
|
||||
model = model,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
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::onBack)
|
||||
|
||||
val stepperState = stepperStateManager.getStepperState(currentRoute)
|
||||
stepperState?.let { stepperComponent.updateState(it) }
|
||||
|
||||
CreateWalletBackupContent(
|
||||
stackState = stackState,
|
||||
stepperComponent = stepperComponent.takeIf { stepperState != null },
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : CreateWalletBackupComponent.Factory {
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: CreateWalletBackupComponent.Params,
|
||||
): DefaultCreateWalletBackupComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.features.hotwallet.createwalletbackup.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.hotwallet.CreateWalletBackupComponent
|
||||
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel
|
||||
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupStepperStateManager
|
||||
import com.tangem.features.hotwallet.createwalletbackup.DefaultCreateWalletBackupComponent
|
||||
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 CreateWalletBackupModuleBinds {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindCreateWalletBackupComponentFactory(
|
||||
impl: DefaultCreateWalletBackupComponent.Factory,
|
||||
): CreateWalletBackupComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(CreateWalletBackupModel::class)
|
||||
fun bindCreateWalletBackupModel(model: CreateWalletBackupModel): Model
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CreateWalletBackupModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCreateWalletBackupStepperStateManager(): CreateWalletBackupStepperStateManager {
|
||||
return CreateWalletBackupStepperStateManager()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.tangem.features.hotwallet.createwalletbackup.routing
|
||||
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.features.hotwallet.createwalletbackup.CreateWalletBackupModel
|
||||
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 javax.inject.Inject
|
||||
|
||||
internal class CreateWalletBackupChildFactory @Inject constructor() {
|
||||
|
||||
fun createChild(
|
||||
route: CreateWalletBackupRoute,
|
||||
childContext: AppComponentContext,
|
||||
model: CreateWalletBackupModel,
|
||||
): ComposableContentComponent = when (route) {
|
||||
CreateWalletBackupRoute.RecoveryPhraseStart -> ManualBackupStartComponent(
|
||||
context = childContext,
|
||||
params = ManualBackupStartComponent.Params(
|
||||
callbacks = model.manualBackupStartModelCallbacks,
|
||||
),
|
||||
)
|
||||
CreateWalletBackupRoute.RecoveryPhrase -> ManualBackupPhraseComponent(
|
||||
context = childContext,
|
||||
params = ManualBackupPhraseComponent.Params(
|
||||
userWalletId = model.params.userWalletId,
|
||||
callbacks = model.manualBackupPhraseModelCallbacks,
|
||||
),
|
||||
)
|
||||
CreateWalletBackupRoute.ConfirmBackup -> ManualBackupCheckComponent(
|
||||
context = childContext,
|
||||
params = ManualBackupCheckComponent.Params(
|
||||
userWalletId = model.params.userWalletId,
|
||||
callbacks = model.manualBackupCheckModelCallbacks,
|
||||
),
|
||||
)
|
||||
CreateWalletBackupRoute.BackupCompleted -> ManualBackupCompletedComponent(
|
||||
context = childContext,
|
||||
params = ManualBackupCompletedComponent.Params(
|
||||
userWalletId = model.params.userWalletId,
|
||||
callbacks = model.manualBackupCompletedModelCallbacks,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.features.hotwallet.createwalletbackup.routing
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed interface CreateWalletBackupRoute {
|
||||
|
||||
@Serializable
|
||||
data object RecoveryPhraseStart : CreateWalletBackupRoute
|
||||
|
||||
@Serializable
|
||||
data object RecoveryPhrase : CreateWalletBackupRoute
|
||||
|
||||
@Serializable
|
||||
data object ConfirmBackup : CreateWalletBackupRoute
|
||||
|
||||
@Serializable
|
||||
data object BackupCompleted : CreateWalletBackupRoute
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.features.hotwallet.createwalletbackup.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
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.arkivanov.decompose.extensions.compose.stack.Children
|
||||
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
|
||||
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.createwalletbackup.routing.CreateWalletBackupRoute
|
||||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
|
||||
@Composable
|
||||
internal fun CreateWalletBackupContent(
|
||||
stackState: ChildStack<CreateWalletBackupRoute, ComposableContentComponent>,
|
||||
stepperComponent: HotWalletStepperComponent?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.imePadding()
|
||||
.systemBarsPadding(),
|
||||
) {
|
||||
stepperComponent?.Content(Modifier)
|
||||
|
||||
Children(
|
||||
stack = stackState,
|
||||
animation = stackAnimation(slide()),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
it.instance.Content(Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.features.hotwallet.walletactivation.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.accesscode.AccessCodeComponent
|
||||
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.accesscode.AccessCodeComponent
|
||||
import com.tangem.features.hotwallet.setupfinished.MobileWalletSetupFinishedComponent
|
||||
import com.tangem.features.hotwallet.walletactivation.entry.WalletActivationModel
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
|
||||
|
|
@ -72,6 +73,7 @@ internal class WalletActivationChildFactory @Inject constructor(
|
|||
context = childContext,
|
||||
params = PushNotificationsParams(
|
||||
modelCallbacks = model.pushNotificationsCallbacks,
|
||||
source = AppRoute.PushNotification.Source.Onboarding,
|
||||
),
|
||||
)
|
||||
is WalletActivationRoute.SetupFinished -> MobileWalletSetupFinishedComponent(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ internal data class WalletBackupUM(
|
|||
val googleDriveStatus: LabelUM?,
|
||||
val onRecoveryPhraseClick: () -> Unit,
|
||||
val onGoogleDriveClick: () -> Unit,
|
||||
val backedUp: Boolean,
|
||||
)
|
||||
|
||||
internal sealed class BackupStatus {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
package com.tangem.features.hotwallet.walletbackup.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.navigation.Router
|
||||
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.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.bottomSheetMessage
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.features.hotwallet.WalletBackupComponent
|
||||
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -22,6 +31,7 @@ internal class WalletBackupModel @Inject constructor(
|
|||
getWalletUseCase: GetUserWalletUseCase,
|
||||
private val router: Router,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
private val params: WalletBackupComponent.Params = paramsContainer.require()
|
||||
|
|
@ -38,11 +48,31 @@ internal class WalletBackupModel @Inject constructor(
|
|||
text = resourceReference(R.string.common_coming_soon),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
onRecoveryPhraseClick = { },
|
||||
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
|
||||
onGoogleDriveClick = { },
|
||||
backedUp = false,
|
||||
),
|
||||
)
|
||||
|
||||
private val makeBackupAtFirstAlertBS
|
||||
get() = bottomSheetMessage {
|
||||
infoBlock {
|
||||
icon(R.drawable.ic_passcode_lock_32) {
|
||||
type = MessageBottomSheetUMV2.Icon.Type.Accent
|
||||
backgroundType = MessageBottomSheetUMV2.Icon.BackgroundType.SameAsTint
|
||||
}
|
||||
title = resourceReference(R.string.hw_backup_need_title)
|
||||
body = resourceReference(R.string.hw_backup_need_description)
|
||||
}
|
||||
secondaryButton {
|
||||
text = resourceReference(R.string.hw_backup_need_action)
|
||||
onClick {
|
||||
router.push(AppRoute.CreateWalletBackup(params.userWalletId))
|
||||
closeBs()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
getWalletUseCase.invokeFlow(params.userWalletId)
|
||||
.map { it.getOrNull() }
|
||||
|
|
@ -80,5 +110,14 @@ internal class WalletBackupModel @Inject constructor(
|
|||
text = resourceReference(R.string.common_coming_soon),
|
||||
style = LabelStyle.REGULAR,
|
||||
),
|
||||
backedUp = userWallet.backedUp,
|
||||
)
|
||||
|
||||
private fun onRecoveryPhraseClick() {
|
||||
if (uiState.value.backedUp) {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
} else {
|
||||
uiMessageSender.send(makeBackupAtFirstAlertBS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -96,6 +96,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider<Wallet
|
|||
onBackClick = {},
|
||||
onRecoveryPhraseClick = {},
|
||||
onGoogleDriveClick = {},
|
||||
backedUp = false,
|
||||
),
|
||||
WalletBackupUM(
|
||||
recoveryPhraseStatus = LabelUM(
|
||||
|
|
@ -109,6 +110,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider<Wallet
|
|||
onBackClick = {},
|
||||
onRecoveryPhraseClick = {},
|
||||
onGoogleDriveClick = {},
|
||||
backedUp = false,
|
||||
),
|
||||
WalletBackupUM(
|
||||
recoveryPhraseStatus = LabelUM(
|
||||
|
|
@ -122,6 +124,7 @@ private class WalletBackupUMProvider : CollectionPreviewParameterProvider<Wallet
|
|||
onBackClick = {},
|
||||
onRecoveryPhraseClick = {},
|
||||
onGoogleDriveClick = {},
|
||||
backedUp = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -4,9 +4,14 @@ import com.tangem.core.decompose.context.AppComponentContext
|
|||
|
||||
interface KycComponent {
|
||||
|
||||
fun launch()
|
||||
fun launch(params: Params)
|
||||
|
||||
interface Factory {
|
||||
fun create(appComponentContext: AppComponentContext): KycComponent
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val targetAddress: String,
|
||||
val cardId: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -13,8 +13,7 @@ android {
|
|||
|
||||
dependencies {
|
||||
/** Api */
|
||||
//TODO disable for release because of the permissions
|
||||
// implementation(projects.features.kyc.api)
|
||||
implementation(projects.features.kyc.api)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.visa)
|
||||
|
|
|
|||
|
|
@ -1,57 +1,41 @@
|
|||
package com.tangem.features.kyc
|
||||
|
||||
import com.sumsub.sns.core.SNSMobileSDK
|
||||
import com.sumsub.sns.core.data.listener.SNSCompleteHandler
|
||||
import com.sumsub.sns.core.data.listener.TokenExpirationHandler
|
||||
import com.sumsub.sns.core.data.model.SNSCompletionResult
|
||||
import com.sumsub.sns.core.data.model.SNSInitConfig
|
||||
import com.sumsub.sns.core.data.model.SNSSDKState
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.kyc.theme.TangemSNSTheme
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.features.kyc.theme.TangemSNSIconHandler
|
||||
import com.tangem.features.kyc.theme.TangemSNSTheme
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.Locale
|
||||
|
||||
class DefaultKycComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
private val kycRepositoryFactory: KycRepository.Factory,
|
||||
) : KycComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val kycRepository = kycRepositoryFactory.create(UserWalletId("0FFFFF"))
|
||||
private val model: DefaultKycModel = getOrCreateModel()
|
||||
|
||||
override fun launch() {
|
||||
override fun launch(params: KycComponent.Params) {
|
||||
componentScope.launch {
|
||||
val startInfo = kycRepository.getKycStartInfo().getOrNull() ?: return@launch
|
||||
|
||||
val tokenExpirationHandler = object : TokenExpirationHandler {
|
||||
override fun onTokenExpired(): String? {
|
||||
val newToken = runBlocking { kycRepository.getKycStartInfo().getOrNull()?.token }
|
||||
return newToken
|
||||
model.uiState.collect {
|
||||
it?.let { startInfo ->
|
||||
val tokenExpirationHandler = object : TokenExpirationHandler {
|
||||
override fun onTokenExpired() = ""
|
||||
}
|
||||
val snsSdk = SNSMobileSDK.Builder(activity)
|
||||
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
|
||||
.withTheme(TangemSNSTheme.theme(activity))
|
||||
.withIconHandler(TangemSNSIconHandler())
|
||||
.withLocale(Locale("en"))
|
||||
.build()
|
||||
snsSdk.launch()
|
||||
}
|
||||
}
|
||||
|
||||
val snsSdk = SNSMobileSDK.Builder(activity)
|
||||
.withAccessToken(accessToken = startInfo.token, onTokenExpiration = tokenExpirationHandler)
|
||||
.withConf(SNSInitConfig(strings = mapOf()))
|
||||
.withTheme(TangemSNSTheme.theme(activity))
|
||||
.withIconHandler(TangemSNSIconHandler())
|
||||
.withLocale(Locale("en"))
|
||||
.withCompleteHandler(
|
||||
object : SNSCompleteHandler {
|
||||
override fun onComplete(result: SNSCompletionResult, state: SNSSDKState) {
|
||||
}
|
||||
},
|
||||
)
|
||||
.build()
|
||||
|
||||
snsSdk.launch()
|
||||
}
|
||||
model.getKycToken(params)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package com.tangem.features.kyc
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.domain.pay.KycStartInfo
|
||||
import com.tangem.domain.pay.repository.KycRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
class DefaultKycModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
kycRepositoryFactory: KycRepository.Factory,
|
||||
) : Model() {
|
||||
|
||||
private val kycRepository = kycRepositoryFactory.create()
|
||||
|
||||
private val _uiState: MutableStateFlow<KycStartInfo?> = MutableStateFlow(null)
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
fun getKycToken(params: KycComponent.Params) {
|
||||
modelScope.launch {
|
||||
kycRepository.getKycStartInfo(address = params.targetAddress, cardId = params.cardId).getOrNull()
|
||||
?.let { _uiState.emit(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
package com.tangem.features.kyc.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.kyc.DefaultKycComponent
|
||||
import com.tangem.features.kyc.DefaultKycModel
|
||||
import com.tangem.features.kyc.KycComponent
|
||||
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)
|
||||
|
|
@ -13,4 +18,13 @@ internal interface FeatureModule {
|
|||
|
||||
@Binds
|
||||
fun bindComponentFactory(impl: DefaultKycComponent.Factory): KycComponent.Factory
|
||||
}
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(DefaultKycModel::class)
|
||||
fun provideModel(model: DefaultKycModel): Model
|
||||
}
|
||||
|
|
@ -93,6 +93,7 @@ internal class ManageTokensListManager @AssistedInject constructor(
|
|||
actionsFlow = actionsFlow,
|
||||
coroutineScope = this,
|
||||
),
|
||||
// only for onboarding case, change carefully and check repository implementation
|
||||
loadUserTokensFromRemote = userWalletId != null && source == ManageTokensSource.ONBOARDING,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,10 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
|
|||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
|
|
@ -164,11 +165,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
type = percentChangeType.toChartType(),
|
||||
xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24),
|
||||
yAxisFormatter = { value ->
|
||||
BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = value,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
)
|
||||
value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
).price()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -196,11 +198,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
val state = MutableStateFlow(
|
||||
MarketsTokenDetailsUM(
|
||||
tokenName = params.token.name,
|
||||
priceText = BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = params.token.tokenQuotes.currentPrice,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
priceText = params.token.tokenQuotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
).price()
|
||||
},
|
||||
dateTimeText = resourceReference(R.string.common_today),
|
||||
priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() },
|
||||
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
|
||||
|
|
@ -403,7 +406,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
|
||||
state.update {
|
||||
it.copy(
|
||||
priceText = newInfo.quotes.currentPrice.formatAsPrice(currentAppCurrency.value),
|
||||
priceText = newInfo.quotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
).price()
|
||||
},
|
||||
priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval(
|
||||
interval = it.selectedInterval,
|
||||
),
|
||||
|
|
@ -490,7 +498,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
|
|||
)
|
||||
} ?: getDefaultDateTimeString(currentState.selectedInterval)
|
||||
|
||||
val priceText = (price ?: currentQuotes.value.currentPrice).formatAsPrice(currentAppCurrency.value)
|
||||
val priceText = (price ?: currentQuotes.value.currentPrice).format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
).price()
|
||||
}
|
||||
|
||||
val percent = price?.let {
|
||||
getChangePercentBetween(
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
|||
import com.tangem.core.ui.components.token.state.TokenItemState
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.markets.TokenMarketExchange
|
||||
import com.tangem.domain.markets.TokenMarketExchange.TrustScore
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
|
@ -29,11 +31,12 @@ internal object ExchangeItemStateConverter : Converter<TokenMarketExchange, Toke
|
|||
),
|
||||
titleState = TokenItemState.TitleState.Content(text = stringReference(value.name)),
|
||||
fiatAmountState = TokenItemState.FiatAmountState.Content(
|
||||
text = BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = value.volumeInUsd,
|
||||
fiatCurrencyCode = "USD",
|
||||
fiatCurrencySymbol = "$",
|
||||
),
|
||||
text = value.volumeInUsd.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = "USD",
|
||||
fiatCurrencySymbol = "$",
|
||||
).price()
|
||||
},
|
||||
),
|
||||
subtitleState = TokenItemState.SubtitleState.TextContent(
|
||||
value = stringReference(value = if (value.isCentralized) "CEX" else "DEX"),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package com.tangem.features.markets.details.impl.model.converters
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
|
|
@ -45,11 +47,12 @@ internal class PricePerformanceConverter(
|
|||
private fun BigDecimal?.convert(): String {
|
||||
val currency = appCurrency()
|
||||
|
||||
return BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
return format {
|
||||
fiat(
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
).price()
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float {
|
||||
|
|
|
|||
|
|
@ -3,22 +3,13 @@ package com.tangem.features.markets.details.impl.model.formatter
|
|||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.getFiatPriceAmountWithScale
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenQuotes
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal fun BigDecimal.formatAsPrice(currency: AppCurrency): String {
|
||||
return BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = this,
|
||||
fiatCurrencyCode = currency.code,
|
||||
fiatCurrencySymbol = currency.symbol,
|
||||
)
|
||||
}
|
||||
|
||||
internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String {
|
||||
val percent = when (interval) {
|
||||
PriceChangeInterval.H24 -> h24ChangePercent
|
||||
|
|
@ -66,8 +57,8 @@ internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: Bi
|
|||
}
|
||||
|
||||
internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType {
|
||||
val current = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = currentPrice).first
|
||||
val updated = BigDecimalFormatter.getFiatPriceUncappedWithScale(value = updatedPrice).first
|
||||
val current = getFiatPriceAmountWithScale(value = currentPrice).first
|
||||
val updated = getFiatPriceAmountWithScale(value = updatedPrice).first
|
||||
|
||||
return when {
|
||||
updated > current -> PriceChangeType.UP
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.tangem.features.markets.details.impl.model.state
|
|||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.PriceChangeInterval
|
||||
import com.tangem.domain.markets.TokenMarketInfo
|
||||
|
|
@ -57,7 +60,12 @@ internal class QuotesStateUpdater(
|
|||
|
||||
state.update { stateToUpdate ->
|
||||
stateToUpdate.copy(
|
||||
priceText = newQuotes.currentPrice.formatAsPrice(currentAppCurrency()),
|
||||
priceText = newQuotes.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencySymbol = currentAppCurrency().symbol,
|
||||
fiatCurrencyCode = currentAppCurrency().code,
|
||||
).price()
|
||||
},
|
||||
priceChangePercentText = newQuotes.getFormattedPercentByInterval(
|
||||
interval = stateToUpdate.selectedInterval,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ 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.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.price
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetCurrencyQuotesUseCase
|
||||
|
|
@ -86,13 +87,14 @@ internal class TokenMarketBlockModel @Inject constructor(
|
|||
)
|
||||
|
||||
state.value = state.value.copy(
|
||||
currentPrice = BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = res.fiatRate,
|
||||
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
),
|
||||
currentPrice = res.fiatRate.format {
|
||||
fiat(
|
||||
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
|
||||
fiatCurrencyCode = currentAppCurrency.value.code,
|
||||
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
|
||||
fiatCurrencySymbol = currentAppCurrency.value.symbol,
|
||||
).price()
|
||||
},
|
||||
h24Percent = res.priceChange.format { percent() },
|
||||
priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ import com.tangem.common.ui.charts.state.sorted
|
|||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.compact
|
||||
import com.tangem.core.ui.format.bigdecimal.fiat
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.core.ui.format.bigdecimal.percent
|
||||
import com.tangem.core.ui.utils.BigDecimalFormatter
|
||||
import com.tangem.core.ui.format.bigdecimal.*
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.markets.impl.R
|
||||
|
|
@ -94,11 +90,12 @@ internal class MarketsTokenItemConverter(
|
|||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
val prevPrice = prev?.tokenQuotesShort?.currentPrice
|
||||
|
||||
val priceText = BigDecimalFormatter.formatFiatPriceUncapped(
|
||||
fiatAmount = tokenQuotesShort.currentPrice,
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
)
|
||||
val priceText = tokenQuotesShort.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).price()
|
||||
}
|
||||
|
||||
val changeType = if (prevPrice != null) {
|
||||
if (tokenQuotesShort.currentPrice > prevPrice) {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ package com.tangem.features.nft.component
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.nft.models.NFTAsset
|
||||
|
||||
interface NFTDetailsBlockComponent : ComposableContentComponent {
|
||||
|
||||
|
|
@ -11,6 +12,8 @@ interface NFTDetailsBlockComponent : ComposableContentComponent {
|
|||
val userWalletId: UserWalletId,
|
||||
val nftAsset: NFTAsset,
|
||||
val nftCollectionName: String,
|
||||
val title: TextReference,
|
||||
val isSuccessScreen: Boolean,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, NFTDetailsBlockComponent>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class DefaultNFTDetailsBlockComponent @AssistedInject constructor(
|
|||
assetName = stringReference(params.nftAsset.name.orEmpty()),
|
||||
collectionName = stringReference(params.nftCollectionName),
|
||||
assetImage = params.nftAsset.media?.imageUrl,
|
||||
title = params.title,
|
||||
isSuccessScreen = params.isSuccessScreen,
|
||||
networkIconRes = getActiveIconRes(params.nftAsset.network.rawId),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
|
@ -19,12 +20,15 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.features.nft.common.ui.NFTLogo
|
||||
import com.tangem.features.nft.impl.R
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun NFTDetailsBlock(
|
||||
title: TextReference,
|
||||
assetName: TextReference,
|
||||
collectionName: TextReference,
|
||||
assetImage: String?,
|
||||
networkIconRes: Int,
|
||||
isSuccessScreen: Boolean,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -35,20 +39,21 @@ internal fun NFTDetailsBlock(
|
|||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "NFT Asset",
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
NFTLogo(
|
||||
assetImage,
|
||||
networkIconRes,
|
||||
background = TangemTheme.colors.background.action,
|
||||
)
|
||||
|
||||
if (isSuccessScreen) {
|
||||
NFTLogo(
|
||||
assetImage,
|
||||
networkIconRes,
|
||||
background = TangemTheme.colors.background.action,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
|
|
@ -63,6 +68,14 @@ internal fun NFTDetailsBlock(
|
|||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
if (!isSuccessScreen) {
|
||||
SpacerWMax()
|
||||
NFTLogo(
|
||||
assetImage,
|
||||
networkIconRes,
|
||||
background = TangemTheme.colors.background.action,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +91,8 @@ private fun NFTDetailsBlock_Preview() {
|
|||
collectionName = stringReference("NFT Collection"),
|
||||
assetImage = null,
|
||||
networkIconRes = R.drawable.img_polygon_22,
|
||||
title = stringReference("From My Wallet"),
|
||||
isSuccessScreen = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
|
@ -12,6 +13,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -73,6 +75,7 @@ internal fun MultiWalletAccessCodeEnter(
|
|||
label = stringResourceSafe(id = R.string.onboarding_wallet_info_title_third),
|
||||
isError = state.codesNotMatchError,
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||
caption = when {
|
||||
state.codesNotMatchError && reEnterAccessCodeState ->
|
||||
stringResourceSafe(R.string.onboarding_access_codes_doesnt_match)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent
|
||||
|
|
@ -51,6 +52,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val cardRepository: CardRepository,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
|
|
@ -231,7 +233,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
OnboardingMultiWalletComponent.Mode.Onboarding,
|
||||
OnboardingMultiWalletComponent.Mode.ContinueFinalize,
|
||||
-> {
|
||||
userWalletsListManager.save(
|
||||
saveWalletUseCase(
|
||||
userWallet = userWalletCreated.copy(
|
||||
scanResponse = scanResponse.updateScanResponseAfterBackup(),
|
||||
),
|
||||
|
|
@ -247,13 +249,11 @@ internal class MultiWalletFinalizeModel @Inject constructor(
|
|||
}
|
||||
?: userWalletCreated
|
||||
|
||||
userWalletsListManager.update(
|
||||
userWalletId = userWallet.walletId,
|
||||
update = { wallet ->
|
||||
wallet.requireColdWallet().copy(
|
||||
scanResponse = scanResponse.updateScanResponseAfterBackup(),
|
||||
)
|
||||
},
|
||||
saveWalletUseCase(
|
||||
userWallet = userWallet.requireColdWallet().copy(
|
||||
scanResponse = scanResponse.updateScanResponseAfterBackup(),
|
||||
),
|
||||
canOverride = true,
|
||||
)
|
||||
|
||||
userWallet
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation
|
|||
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState
|
||||
import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT
|
||||
|
|
@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
|
||||
@Assisted context: AppComponentContext,
|
||||
@Assisted val params: OnboardingNoteComponent.Params,
|
||||
val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : OnboardingNoteComponent, AppComponentContext by context {
|
||||
|
||||
|
|
@ -100,14 +101,14 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
|
|||
childParams = childParams,
|
||||
onWalletCreated = { userWallet ->
|
||||
model.onWalletCreated(userWallet)
|
||||
model.stackNavigation.push(OnboardingNoteRoute.TopUp)
|
||||
model.stackNavigation.push(OnboardingNoteRoute.Done)
|
||||
},
|
||||
),
|
||||
)
|
||||
OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent(
|
||||
appComponentContext = factoryContext,
|
||||
params = OnboardingNoteTopUpComponent.Params(
|
||||
childParams = childParams,
|
||||
OnboardingNoteRoute.Done -> onboardingDoneComponentFactory.create(
|
||||
context = factoryContext,
|
||||
params = OnboardingDoneComponent.Params(
|
||||
mode = OnboardingDoneComponent.Mode.WalletCreated,
|
||||
onDone = { params.onDone() },
|
||||
),
|
||||
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
|
||||
|
|
|
|||
|
|
@ -1,68 +0,0 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.topup
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.decompose.ComponentContext
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.slot.childSlot
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp
|
||||
import com.tangem.features.tokenreceive.TokenReceiveComponent
|
||||
|
||||
internal class OnboardingNoteTopUpComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: Params,
|
||||
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
|
||||
) : ComposableContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val model: OnboardingNoteTopUpModel = getOrCreateModel(params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = model.bottomSheetNavigation,
|
||||
serializer = TokenReceiveConfig.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val bottomSheet by bottomSheetSlot.subscribeAsState()
|
||||
|
||||
BackHandler(onBack = remember(this) { { params.childParams.onBack() } })
|
||||
|
||||
OnboardingNoteTopUp(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
bottomSheet.child?.instance?.BottomSheet()
|
||||
}
|
||||
|
||||
private fun bottomSheetChild(
|
||||
config: TokenReceiveConfig,
|
||||
componentContext: ComponentContext,
|
||||
): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create(
|
||||
context = childByContext(componentContext),
|
||||
params = TokenReceiveComponent.Params(
|
||||
config = config,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
),
|
||||
)
|
||||
|
||||
data class Params(
|
||||
val childParams: DefaultOnboardingNoteComponent.ChildParams,
|
||||
val onDone: () -> Unit,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,319 +0,0 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.topup.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.analytics.Analytics
|
||||
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.navigation.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.network.NetworkAddress
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.extensions.isPositive
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class OnboardingNoteTopUpModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val shareManager: ShareManager,
|
||||
private val rampStateManager: RampStateManager,
|
||||
private val cardRepository: CardRepository,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val walletBalanceFetcher: WalletBalanceFetcher,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingNoteTopUpComponent.Params>()
|
||||
private val commonState = params.childParams.commonState
|
||||
private val scanResponse = params.childParams.commonState.value.scanResponse
|
||||
private var userWallet = params.childParams.commonState.value.userWallet
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
OnboardingNoteTopUpUM(
|
||||
onRefreshBalanceClick = ::refreshBalance,
|
||||
onBuyCryptoClick = ::onBuyCryptoClick,
|
||||
onShowWalletAddressClick = ::onShowWalletAddressClick,
|
||||
onDismissBottomSheet = ::onDismissBottomSheet,
|
||||
),
|
||||
)
|
||||
|
||||
val uiState: StateFlow<OnboardingNoteTopUpUM> = _uiState
|
||||
|
||||
init {
|
||||
Analytics.send(OnboardingEvent.Topup.ScreenOpened)
|
||||
observeArtwork()
|
||||
modelScope.launch {
|
||||
createUserWalletIfNull()
|
||||
cardRepository.finishCardActivation(scanResponse.card.cardId)
|
||||
observeCryptoCurrencyStatus()
|
||||
refreshBalance()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshBalance() {
|
||||
modelScope.launch {
|
||||
showBalanceLoadingProgress(true)
|
||||
createUserWalletIfNull()
|
||||
val userWalletId = requireNotNull(userWallet?.walletId)
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
|
||||
.onLeft(Timber::e)
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true)
|
||||
}
|
||||
showBalanceLoadingProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBuyCryptoClick() {
|
||||
val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return
|
||||
modelScope.launch {
|
||||
getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight {
|
||||
urlOpener.openUrl(it)
|
||||
}
|
||||
}
|
||||
Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency))
|
||||
}
|
||||
|
||||
private fun onShowWalletAddressClick() {
|
||||
val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return
|
||||
val networkAddress = currencyStatus.value.networkAddress ?: return
|
||||
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
val userWalletId = userWallet?.walletId ?: return
|
||||
modelScope.launch {
|
||||
configureReceiveAddresses(
|
||||
cryptoCurrencyStatus = currencyStatus,
|
||||
userWalletId = userWalletId,
|
||||
)?.let { bottomSheetNavigation.activate(it) }
|
||||
}
|
||||
} else {
|
||||
_uiState.update {
|
||||
it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress))
|
||||
}
|
||||
}
|
||||
Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress)
|
||||
}
|
||||
|
||||
private fun onDismissBottomSheet() {
|
||||
_uiState.update {
|
||||
it.copy(addressBottomSheetConfig = null)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createUserWalletIfNull() {
|
||||
if (userWallet != null) {
|
||||
return
|
||||
}
|
||||
val commonState = params.childParams.commonState.value
|
||||
userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse)
|
||||
}
|
||||
|
||||
private fun observeArtwork() {
|
||||
modelScope.launch {
|
||||
params.childParams.commonState.collect {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
cardArtwork = it.cardArtwork,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeCryptoCurrencyStatus() {
|
||||
val userWalletId = userWallet?.walletId ?: return
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId)
|
||||
.map { it.getOrNull() }
|
||||
.filterNotNull()
|
||||
.onEach(::applyCryptoCurrencyStatusToState)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) {
|
||||
if (commonState.value.cryptoCurrencyStatus == null) {
|
||||
loadAvailableForBuy(status)
|
||||
}
|
||||
|
||||
commonState.update {
|
||||
it.copy(cryptoCurrencyStatus = status)
|
||||
}
|
||||
|
||||
val amount = when (status.value) {
|
||||
is CryptoCurrencyStatus.Loaded -> status.value.amount
|
||||
is CryptoCurrencyStatus.NoAccount -> status.value.amount
|
||||
is CryptoCurrencyStatus.NoQuote -> status.value.amount
|
||||
else -> null
|
||||
}
|
||||
val hasCurrentNetworkTransactions = when (status.value) {
|
||||
is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions
|
||||
is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions
|
||||
else -> false
|
||||
}
|
||||
val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount
|
||||
|
||||
if (amount?.isPositive() == true || hasCurrentNetworkTransactions) {
|
||||
params.onDone()
|
||||
}
|
||||
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
amountToCreateAccount = amountToCreateAccount
|
||||
?.format {
|
||||
crypto(
|
||||
symbol = status.currency.symbol,
|
||||
decimals = status.currency.decimals,
|
||||
)
|
||||
},
|
||||
balance = amount?.format {
|
||||
crypto(
|
||||
symbol = status.currency.symbol,
|
||||
decimals = status.currency.decimals,
|
||||
)
|
||||
}.orEmpty(),
|
||||
isTopUpDataLoading = status.value.networkAddress == null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBalanceLoadingProgress(value: Boolean) {
|
||||
_uiState.update {
|
||||
it.copy(isRefreshing = value)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) {
|
||||
modelScope.launch {
|
||||
val availableForBuy = rampStateManager.availableForBuy(
|
||||
userWallet = userWallet ?: return@launch,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
availableForBuy = availableForBuy == ScenarioUnavailabilityReason.None,
|
||||
availableForBuyLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) =
|
||||
TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = uiState.value.onDismissBottomSheet,
|
||||
content = TokenReceiveBottomSheetConfig(
|
||||
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
|
||||
name = currencyStatus.currency.name,
|
||||
symbol = currencyStatus.currency.symbol,
|
||||
),
|
||||
network = currencyStatus.currency.network,
|
||||
networkAddress = networkAddress,
|
||||
showMemoDisclaimer =
|
||||
currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
|
||||
onCopyClick = {
|
||||
Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol))
|
||||
clipboardManager.setText(text = it, isSensitive = true)
|
||||
},
|
||||
onShareClick = {
|
||||
Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol))
|
||||
shareManager.shareText(text = it)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet {
|
||||
val wallet = requireNotNull(
|
||||
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
saveWalletUseCase(wallet, false)
|
||||
return wallet
|
||||
}
|
||||
|
||||
private suspend fun configureReceiveAddresses(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
userWalletId: UserWalletId,
|
||||
): TokenReceiveConfig? {
|
||||
val addresses = cryptoCurrencyStatus.value.networkAddress ?: return null
|
||||
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
address = addresses.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
displayName = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
addresses.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Default,
|
||||
value = address.value,
|
||||
displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return TokenReceiveConfig(
|
||||
shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
userWalletId = userWalletId,
|
||||
showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network
|
||||
.TransactionExtrasType.NONE,
|
||||
receiveAddress = receiveAddresses,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
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.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.artwork.ArtworkUM
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalTangemShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.onboarding.v2.common.ui.RefreshButton
|
||||
import com.tangem.features.onboarding.v2.common.ui.WalletCard
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
|
||||
@Composable
|
||||
fun OnboardingNoteTopUpHeader(
|
||||
balance: String,
|
||||
cardArtwork: ArtworkUM?,
|
||||
isRefreshing: Boolean,
|
||||
onRefreshBalanceClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.heightIn(min = 180.dp)
|
||||
.widthIn(max = 450.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
TangemTheme.colors.button.secondary,
|
||||
shape = TangemTheme.shapes.roundedCornersMedium,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(horizontal = 32.dp),
|
||||
) {
|
||||
SpacerHMax()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_balance_title),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpacerH8()
|
||||
Text(
|
||||
modifier = if (balance.isEmpty()) {
|
||||
Modifier
|
||||
.width(120.dp)
|
||||
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius3))
|
||||
.shimmer(LocalTangemShimmer.current)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
text = balance,
|
||||
)
|
||||
SpacerHMax()
|
||||
}
|
||||
}
|
||||
WalletCard(
|
||||
modifier = Modifier.width(120.dp).align(Alignment.TopCenter),
|
||||
artwork = cardArtwork,
|
||||
)
|
||||
RefreshButton(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
isRefreshing = isRefreshing,
|
||||
onRefreshBalanceClick = onRefreshBalanceClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun OnboardinNoteTopUpHeaderPreview() {
|
||||
TangemThemePreview {
|
||||
OnboardingNoteTopUpHeader(
|
||||
balance = "0.00000001 BTC",
|
||||
cardArtwork = ArtworkUM(null, ""),
|
||||
onRefreshBalanceClick = {},
|
||||
isRefreshing = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
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.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
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.onboarding.v2.impl.R
|
||||
import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM
|
||||
|
||||
@Composable
|
||||
fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.navigationBarsPadding(),
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
OnboardingNoteTopUpHeader(
|
||||
balance = state.balance,
|
||||
cardArtwork = state.cardArtwork,
|
||||
onRefreshBalanceClick = state.onRefreshBalanceClick,
|
||||
isRefreshing = state.isRefreshing,
|
||||
modifier = Modifier
|
||||
.padding(top = 64.dp)
|
||||
.padding(horizontal = 24.dp)
|
||||
.weight(ALL_STEPS_TOP_CONTAINER_WEIGHT)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SpacerHMax()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onboarding_topup_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
)
|
||||
|
||||
val text = if (state.amountToCreateAccount != null) {
|
||||
stringResourceSafe(
|
||||
R.string.onboarding_top_up_min_create_account_amount,
|
||||
state.amountToCreateAccount,
|
||||
)
|
||||
} else {
|
||||
stringResourceSafe(R.string.onboarding_top_up_body)
|
||||
}
|
||||
SpacerH16()
|
||||
Text(
|
||||
text = text,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpacerHMax()
|
||||
}
|
||||
|
||||
BottomButtons(state)
|
||||
|
||||
state.addressBottomSheetConfig?.let { config ->
|
||||
TokenReceiveBottomSheet(config = config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomButtons(state: OnboardingNoteTopUpUM) {
|
||||
if (state.availableForBuy) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto),
|
||||
onClick = state.onBuyCryptoClick,
|
||||
)
|
||||
} else {
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.onboarding_button_receive_crypto),
|
||||
onClick = state.onShowWalletAddressClick,
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(visible = !state.availableForBuyLoading) {
|
||||
if (state.availableForBuy) {
|
||||
SecondaryButton(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address),
|
||||
onClick = state.onShowWalletAddressClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun OnboardingNoteTopUpPreview() {
|
||||
TangemThemePreview {
|
||||
OnboardingNoteTopUp(
|
||||
state = OnboardingNoteTopUpUM(
|
||||
availableForBuy = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state
|
||||
|
||||
import com.tangem.core.ui.components.artwork.ArtworkUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
|
||||
data class OnboardingNoteTopUpUM(
|
||||
val cardArtwork: ArtworkUM? = null,
|
||||
val availableForBuy: Boolean = false,
|
||||
val availableForBuyLoading: Boolean = true,
|
||||
val balance: String = "",
|
||||
val isRefreshing: Boolean = false,
|
||||
val isTopUpDataLoading: Boolean = true,
|
||||
val amountToCreateAccount: String? = null,
|
||||
val addressBottomSheetConfig: TangemBottomSheetConfig? = null,
|
||||
val onBuyCryptoClick: () -> Unit = {},
|
||||
val onShowWalletAddressClick: () -> Unit = {},
|
||||
val onRefreshBalanceClick: () -> Unit = {},
|
||||
val onDismissBottomSheet: () -> Unit = {},
|
||||
)
|
||||
|
|
@ -5,7 +5,6 @@ import com.tangem.core.decompose.model.Model
|
|||
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel
|
||||
import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel
|
||||
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -37,9 +36,4 @@ internal interface ModelModule {
|
|||
@IntoMap
|
||||
@ClassKey(OnboardingNoteCreateWalletModel::class)
|
||||
fun provideNoteCreateWalletModel(model: OnboardingNoteCreateWalletModel): Model
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(OnboardingNoteTopUpModel::class)
|
||||
fun provideNoteTopUpModel(model: OnboardingNoteTopUpModel): Model
|
||||
}
|
||||
|
|
@ -84,7 +84,7 @@ internal class OnboardingNoteModel @Inject constructor(
|
|||
return if (card.wallets.isEmpty()) {
|
||||
OnboardingNoteRoute.CreateWallet
|
||||
} else {
|
||||
OnboardingNoteRoute.TopUp
|
||||
OnboardingNoteRoute.Done
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ internal sealed class OnboardingNoteRoute {
|
|||
data object CreateWallet : OnboardingNoteRoute()
|
||||
|
||||
@Serializable
|
||||
data object TopUp : OnboardingNoteRoute()
|
||||
data object Done : OnboardingNoteRoute()
|
||||
}
|
||||
|
||||
internal const val ONBOARDING_NOTE_STEPS_COUNT = 3
|
||||
internal const val ONBOARDING_NOTE_STEPS_COUNT = 2
|
||||
|
|
@ -2,5 +2,5 @@ package com.tangem.features.onboarding.v2.note.impl.route
|
|||
|
||||
internal fun OnboardingNoteRoute.stepNum() = when (this) {
|
||||
OnboardingNoteRoute.CreateWallet -> 1
|
||||
OnboardingNoteRoute.TopUp -> 2
|
||||
OnboardingNoteRoute.Done -> 2
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.features.onboarding.v2.twin.impl.model
|
||||
|
||||
import com.arkivanov.decompose.router.slot.SlotNavigation
|
||||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.tangem.Message
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.KeyPair
|
||||
|
|
@ -9,47 +7,26 @@ import com.tangem.common.core.TangemError
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
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.share.ShareManager
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.clipboard.ClipboardManager
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.toWrappedList
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
|
||||
import com.tangem.domain.card.common.util.twinsIsTwinned
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.common.TwinCardNumber
|
||||
import com.tangem.domain.common.getTwinCardNumber
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.ReceiveAddressModel
|
||||
import com.tangem.domain.models.TokenReceiveConfig
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
|
||||
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
|
||||
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
|
||||
import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase
|
||||
import com.tangem.domain.tokens.TokensFeatureToggles
|
||||
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
|
||||
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
|
||||
import com.tangem.domain.transaction.usecase.GetEnsNameUseCase
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
|
||||
import com.tangem.features.onboarding.v2.common.ui.interruptBackupDialog
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
|
|
@ -58,7 +35,6 @@ import com.tangem.features.onboarding.v2.twin.api.OnboardingTwinComponent.Params
|
|||
import com.tangem.features.onboarding.v2.twin.impl.DefaultOnboardingTwinComponent
|
||||
import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM
|
||||
import com.tangem.features.onboarding.v2.twin.impl.ui.state.OnboardingTwinUM
|
||||
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.sdk.extensions.localizedDescriptionRes
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -67,11 +43,9 @@ import com.tangem.utils.coroutines.saveIn
|
|||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
|
|
@ -80,33 +54,21 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val deleteWalletUseCase: DeleteWalletUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase,
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val issuersConfigStorage: IssuersConfigStorage,
|
||||
private val cardRepository: CardRepository,
|
||||
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val clipboardManager: ClipboardManager,
|
||||
private val shareManager: ShareManager,
|
||||
private val tokensFeatureToggles: TokensFeatureToggles,
|
||||
private val walletBalanceFetcher: WalletBalanceFetcher,
|
||||
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
|
||||
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
|
||||
private val getEnsNameUseCase: GetEnsNameUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<OnboardingTwinComponent.Params>()
|
||||
private val firstCardTwinNumber = params.scanResponse.card.getTwinCardNumber() ?: error("Not twin")
|
||||
private val cryptoCurrencyStatusJobHolder = JobHolder()
|
||||
|
||||
val bottomSheetNavigation: SlotNavigation<TokenReceiveConfig> = SlotNavigation()
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
when (params.mode) {
|
||||
Mode.WelcomeOnly -> {
|
||||
|
|
@ -126,14 +88,10 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
Mode.CreateWallet -> {
|
||||
if (params.scanResponse.twinsIsTwinned()) {
|
||||
OnboardingTwinUM.TopUpPrepare
|
||||
} else {
|
||||
OnboardingTwinUM.Welcome(
|
||||
pairCardNumber = firstCardTwinNumber.pairNumber().number,
|
||||
onContinueClick = ::navigateToFirstScan,
|
||||
)
|
||||
}
|
||||
OnboardingTwinUM.Welcome(
|
||||
pairCardNumber = firstCardTwinNumber.pairNumber().number,
|
||||
onContinueClick = ::navigateToFirstScan,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -149,11 +107,6 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
saveTwinsOnboardingShownUseCase()
|
||||
}
|
||||
}
|
||||
OnboardingTwinUM.TopUpPrepare -> {
|
||||
modelScope.launch {
|
||||
setTopUpState(params.scanResponse)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
|
@ -211,9 +164,9 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
|
||||
// remove wallet only after first step of retwin
|
||||
if (params.mode == Mode.RecreateWallet) {
|
||||
userWalletsListManager.delete(
|
||||
listOfNotNull(UserWalletIdBuilder.scanResponse(params.scanResponse).build()),
|
||||
)
|
||||
UserWalletIdBuilder.scanResponse(params.scanResponse).build()?.let {
|
||||
deleteWalletUseCase(it)
|
||||
}
|
||||
}
|
||||
|
||||
analyticsEventHandler.send(OnboardingEvent.CreateWallet.WalletCreatedSuccessfully())
|
||||
|
|
@ -228,10 +181,7 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
innerNavigationState.update {
|
||||
it.copy(stackSize = 2)
|
||||
}
|
||||
innerNavigationState.update { it.copy(stackSize = 2) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -239,7 +189,6 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
|
||||
private fun createSecondWallet(firstPublicKey: String) {
|
||||
setLoading(true)
|
||||
|
||||
modelScope.launch {
|
||||
val secondCardNumber = firstCardTwinNumber.pairNumber().number
|
||||
val result = tangemSdkManager.createSecondTwinWallet(
|
||||
|
|
@ -328,142 +277,31 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
Mode.CreateWallet -> {
|
||||
modelScope.launch {
|
||||
setLoading(true)
|
||||
setTopUpState(scanResponse)
|
||||
finishActivation(scanResponse)
|
||||
}.saveIn(cryptoCurrencyStatusJobHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setTopUpState(scanResponse: ScanResponse) = coroutineScope {
|
||||
private suspend fun finishActivation(scanResponse: ScanResponse) = coroutineScope {
|
||||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() ?: run {
|
||||
Timber.e("User wallet not created")
|
||||
setLoading(false)
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
userWalletsListManager.save(userWallet, canOverride = true)
|
||||
saveWalletUseCase(
|
||||
userWallet = userWallet,
|
||||
canOverride = true,
|
||||
).onLeft {
|
||||
Timber.e("Unable to save user wallet: $it")
|
||||
setLoading(false)
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
cardRepository.finishCardActivation(params.scanResponse.card.cardId)
|
||||
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase.invoke(userWalletId = userWallet.walletId, refresh = true)
|
||||
}
|
||||
.onLeft {
|
||||
Timber.e("Unable to fetch currency status: $it")
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
|
||||
.firstOrNull()?.getOrNull()
|
||||
?: run {
|
||||
setLoading(false)
|
||||
Timber.e("Unable to get currency status")
|
||||
return@coroutineScope
|
||||
}
|
||||
|
||||
launch {
|
||||
getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
|
||||
.collect {
|
||||
it.onRight { status ->
|
||||
applyCryptoCurrencyStatusToState(status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_uiState.value = OnboardingTwinUM.TopUp(
|
||||
onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) },
|
||||
onRefreshClick = { onRefreshBalanceClick(userWallet) },
|
||||
onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) },
|
||||
isLoading = true,
|
||||
)
|
||||
|
||||
innerNavigationState.update {
|
||||
it.copy(stackSize = 4)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) {
|
||||
val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return
|
||||
if (amount > BigDecimal.ZERO) {
|
||||
params.modelCallbacks.onDone()
|
||||
} else {
|
||||
update<OnboardingTwinUM.TopUp> {
|
||||
it.copy(
|
||||
balance = BigDecimal.ZERO.format { crypto(status.currency) },
|
||||
onBuyCryptoClick = { onBuyCryptoClick(status) },
|
||||
onShowAddressClick = { onShowAddressClick(status) },
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBuyCryptoClick(status: CryptoCurrencyStatus) {
|
||||
modelScope.launch {
|
||||
getLegacyTopUpUrlUseCase(status).onRight {
|
||||
urlOpener.openUrl(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onShowAddressClick(status: CryptoCurrencyStatus) {
|
||||
val currency = status.currency
|
||||
val networkAddress = status.value.networkAddress ?: return
|
||||
|
||||
if (tokenReceiveFeatureToggle.isNewTokenReceiveEnabled) {
|
||||
modelScope.launch {
|
||||
configureReceiveAddresses(cryptoCurrencyStatus = status)?.let {
|
||||
bottomSheetNavigation.activate(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
update<OnboardingTwinUM.TopUp> {
|
||||
it.copy(
|
||||
bottomSheetConfig = TangemBottomSheetConfig(
|
||||
isShown = true,
|
||||
onDismissRequest = {
|
||||
update<OnboardingTwinUM.TopUp> {
|
||||
it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty)
|
||||
}
|
||||
},
|
||||
content = TokenReceiveBottomSheetConfig(
|
||||
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
|
||||
name = currency.name,
|
||||
symbol = currency.symbol,
|
||||
),
|
||||
network = currency.network,
|
||||
networkAddress = networkAddress,
|
||||
showMemoDisclaimer =
|
||||
currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
|
||||
onCopyClick = {
|
||||
Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol))
|
||||
clipboardManager.setText(text = it, isSensitive = true)
|
||||
},
|
||||
onShareClick = {
|
||||
Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol))
|
||||
shareManager.shareText(text = it)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRefreshBalanceClick(userWallet: UserWallet) {
|
||||
update<OnboardingTwinUM.TopUp> {
|
||||
it.copy(isLoading = true)
|
||||
}
|
||||
modelScope.launch {
|
||||
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
|
||||
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
|
||||
.onLeft(Timber::e)
|
||||
} else {
|
||||
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true)
|
||||
}
|
||||
}
|
||||
params.modelCallbacks.onDone()
|
||||
}
|
||||
|
||||
private fun saveWalletAndDone() {
|
||||
|
|
@ -476,7 +314,15 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
userWalletsListManager.save(userWallet, canOverride = true)
|
||||
saveWalletUseCase(
|
||||
userWallet = userWallet,
|
||||
canOverride = true,
|
||||
).onLeft {
|
||||
Timber.e("Unable to save user wallet: $it")
|
||||
setLoading(false)
|
||||
return@launch
|
||||
}
|
||||
|
||||
params.modelCallbacks.onDone()
|
||||
}
|
||||
}
|
||||
|
|
@ -526,45 +372,4 @@ internal class OnboardingTwinModel @Inject constructor(
|
|||
TwinWalletArtworkUM.Leapfrog.Step.FirstCard -> TwinWalletArtworkUM.Leapfrog.Step.SecondCard
|
||||
TwinWalletArtworkUM.Leapfrog.Step.SecondCard -> TwinWalletArtworkUM.Leapfrog.Step.FirstCard
|
||||
}
|
||||
|
||||
private suspend fun configureReceiveAddresses(cryptoCurrencyStatus: CryptoCurrencyStatus): TokenReceiveConfig? {
|
||||
val userWallet = coldUserWalletBuilderFactory.create(params.scanResponse).build() ?: return null
|
||||
val addresses = cryptoCurrencyStatus.value.networkAddress ?: return null
|
||||
|
||||
val ensName = getEnsNameUseCase.invoke(
|
||||
userWalletId = userWallet.walletId,
|
||||
network = cryptoCurrencyStatus.currency.network,
|
||||
address = addresses.defaultAddress.value,
|
||||
)
|
||||
|
||||
val receiveAddresses = buildList {
|
||||
ensName?.let { ens ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Ens,
|
||||
value = ens,
|
||||
displayName = ens,
|
||||
),
|
||||
)
|
||||
}
|
||||
addresses.availableAddresses.map { address ->
|
||||
add(
|
||||
ReceiveAddressModel(
|
||||
nameService = ReceiveAddressModel.NameService.Default,
|
||||
value = address.value,
|
||||
displayName = "${cryptoCurrencyStatus.currency.name} (${cryptoCurrencyStatus.currency.symbol})",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return TokenReceiveConfig(
|
||||
shouldShowWarning = cryptoCurrencyStatus.currency.name !in getViewedTokenReceiveWarningUseCase(),
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
userWalletId = userWallet.walletId,
|
||||
showMemoDisclaimer = cryptoCurrencyStatus.currency.network.transactionExtrasType != Network
|
||||
.TransactionExtrasType.NONE,
|
||||
receiveAddress = receiveAddresses,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,9 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconEnd
|
||||
import com.tangem.core.ui.components.SecondaryButton
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemAnimations
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
|
@ -43,11 +41,6 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi
|
|||
.weight(.48f)
|
||||
.fillMaxWidth(),
|
||||
state = state.artwork,
|
||||
balance = (state as? OnboardingTwinUM.TopUp)?.balance ?: "",
|
||||
isRefreshing = state.isLoading,
|
||||
onRefreshBalanceClick = {
|
||||
(state as? OnboardingTwinUM.TopUp)?.onRefreshClick()
|
||||
},
|
||||
)
|
||||
|
||||
AnimatedContent(
|
||||
|
|
@ -60,16 +53,10 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi
|
|||
when (st) {
|
||||
is OnboardingTwinUM.ResetWarning -> ResetWarning(st)
|
||||
is OnboardingTwinUM.ScanCard -> ScanCard(st)
|
||||
is OnboardingTwinUM.TopUp -> TopUp(st)
|
||||
is OnboardingTwinUM.Welcome -> Welcome(st)
|
||||
OnboardingTwinUM.TopUpPrepare -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state is OnboardingTwinUM.TopUp) {
|
||||
TokenReceiveBottomSheet(config = state.bottomSheetConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -154,55 +141,6 @@ private fun ResetWarning(state: OnboardingTwinUM.ResetWarning, modifier: Modifie
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopUp(state: OnboardingTwinUM.TopUp, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 32.dp, end = 32.dp, bottom = 16.dp)
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onboarding_topup_title),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.h2,
|
||||
)
|
||||
|
||||
SpacerH16()
|
||||
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.onboarding_top_up_body),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto),
|
||||
onClick = state.onBuyCryptoClick,
|
||||
)
|
||||
|
||||
SecondaryButton(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address),
|
||||
onClick = state.onShowAddressClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanCard(state: OnboardingTwinUM.ScanCard, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
|
|
@ -287,14 +225,6 @@ private fun Welcome(state: OnboardingTwinUM.Welcome, modifier: Modifier = Modifi
|
|||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewTopUp() {
|
||||
TangemThemePreview {
|
||||
OnboardingTwin(OnboardingTwinUM.TopUp())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun PreviewWelcome() {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
package com.tangem.features.onboarding.v2.twin.impl.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
|
|
@ -16,21 +14,15 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.zIndex
|
||||
import com.tangem.core.ui.components.SpacerH8
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.components.artwork.ArtworkUM
|
||||
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.wallets.models.Artwork
|
||||
import com.tangem.features.onboarding.v2.common.ui.RefreshButton
|
||||
import com.tangem.features.onboarding.v2.common.ui.WalletCard
|
||||
import com.tangem.features.onboarding.v2.impl.R
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
|
@ -45,8 +37,6 @@ internal sealed class TwinWalletArtworkUM {
|
|||
FirstCard, SecondCard
|
||||
}
|
||||
}
|
||||
|
||||
data object TopUp : TwinWalletArtworkUM()
|
||||
}
|
||||
|
||||
private data class CardsTransitionState(
|
||||
|
|
@ -64,15 +54,10 @@ private data class WalletCardTransitionState(
|
|||
val zIndex: Float = 0f,
|
||||
)
|
||||
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun TwinWalletArtworks(
|
||||
state: TwinWalletArtworkUM,
|
||||
balance: String,
|
||||
isRefreshing: Boolean,
|
||||
onRefreshBalanceClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
internal fun TwinWalletArtworks(state: TwinWalletArtworkUM, modifier: Modifier = Modifier) {
|
||||
BoxWithConstraints(
|
||||
modifier
|
||||
.heightIn(min = 180.dp)
|
||||
|
|
@ -110,22 +95,6 @@ internal fun TwinWalletArtworks(
|
|||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state == TwinWalletArtworkUM.TopUp,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 24.dp, horizontal = 16.dp)
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
TangemTheme.colors.button.secondary,
|
||||
shape = TangemTheme.shapes.roundedCornersMedium,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedTwinCards(
|
||||
transition1 = transition1,
|
||||
transition2 = transition2,
|
||||
|
|
@ -133,46 +102,6 @@ internal fun TwinWalletArtworks(
|
|||
.widthIn(max = 450.dp)
|
||||
.matchParentSize(),
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = state == TwinWalletArtworkUM.TopUp,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SpacerHMax()
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_balance_title),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpacerH8()
|
||||
Text(
|
||||
text = balance,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
SpacerHMax()
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
visible = state == TwinWalletArtworkUM.TopUp,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
RefreshButton(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefreshBalanceClick = onRefreshBalanceClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -308,26 +237,6 @@ private fun TwinWalletArtworkUM.toTransitionSetState(
|
|||
)
|
||||
}
|
||||
}
|
||||
TwinWalletArtworkUM.TopUp -> {
|
||||
val scale = 0.4f
|
||||
val yTranslation = -maxHeightDp * density - 24 * density
|
||||
listOf(
|
||||
CardsTransitionState(
|
||||
walletCard1 = WalletCardTransitionState(
|
||||
yTranslation = yTranslation,
|
||||
xScale = scale,
|
||||
yScale = scale,
|
||||
zIndex = 2f,
|
||||
),
|
||||
walletCard2 = WalletCardTransitionState(
|
||||
yTranslation = yTranslation * 0.35f,
|
||||
xScale = scale * 0.8f,
|
||||
yScale = scale * 0.8f,
|
||||
zIndex = 1f,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
|
||||
|
|
@ -341,16 +250,13 @@ private fun Preview() {
|
|||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) }
|
||||
var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.Spread) }
|
||||
|
||||
TwinWalletArtworks(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(top = 250.dp)
|
||||
.fillMaxWidth(),
|
||||
balance = "1 USD",
|
||||
isRefreshing = false,
|
||||
onRefreshBalanceClick = {},
|
||||
)
|
||||
|
||||
var index by remember { mutableIntStateOf(0) }
|
||||
|
|
@ -366,7 +272,6 @@ private fun Preview() {
|
|||
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard),
|
||||
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard),
|
||||
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard),
|
||||
TwinWalletArtworkUM.TopUp,
|
||||
)
|
||||
|
||||
state = list[index % list.size]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.features.onboarding.v2.twin.impl.ui.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM
|
||||
|
||||
@Immutable
|
||||
|
|
@ -11,12 +10,6 @@ internal sealed class OnboardingTwinUM {
|
|||
abstract val isLoading: Boolean
|
||||
abstract val artwork: TwinWalletArtworkUM
|
||||
|
||||
data object TopUpPrepare : OnboardingTwinUM() {
|
||||
override val stepIndex: Int = 0
|
||||
override val isLoading: Boolean = false
|
||||
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread
|
||||
}
|
||||
|
||||
data class Welcome(
|
||||
override val isLoading: Boolean = false,
|
||||
val pairCardNumber: Int = 2,
|
||||
|
|
@ -56,23 +49,9 @@ internal sealed class OnboardingTwinUM {
|
|||
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep)
|
||||
}
|
||||
|
||||
data class TopUp(
|
||||
override val isLoading: Boolean = false,
|
||||
val balance: String = "",
|
||||
val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
|
||||
val onBuyCryptoClick: () -> Unit = {},
|
||||
val onShowAddressClick: () -> Unit = {},
|
||||
val onRefreshClick: () -> Unit = {},
|
||||
) : OnboardingTwinUM() {
|
||||
override val stepIndex: Int = 2
|
||||
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp
|
||||
}
|
||||
|
||||
fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) {
|
||||
is Welcome -> copy(isLoading = isLoading)
|
||||
is ResetWarning -> copy()
|
||||
is ScanCard -> copy(isLoading = isLoading)
|
||||
is TopUp -> copy(isLoading = isLoading)
|
||||
TopUpPrepare -> this
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +1,11 @@
|
|||
package com.tangem.features.onboarding.v2.visa.impl.child.choosewallet.ui
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
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.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
|
||||
|
|
@ -19,9 +15,9 @@ import com.tangem.core.ui.components.notifications.Notification
|
|||
import com.tangem.core.ui.components.notifications.NotificationConfig
|
||||
import com.tangem.core.ui.components.rows.RowContentContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.outsetBorder
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.selectedBorder
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -112,17 +108,7 @@ private fun SelectableChainRow(
|
|||
RowContentContainer(
|
||||
modifier = modifier
|
||||
.heightIn(min = 48.dp)
|
||||
.outsetBorder(
|
||||
color = if (selected) TangemTheme.colors.icon.accent.copy(alpha = 0.15f) else Color.Transparent,
|
||||
width = 5.dp,
|
||||
shape = RoundedCornerShape(size = 18.dp),
|
||||
)
|
||||
.clip(TangemTheme.shapes.roundedCornersXMedium)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = if (selected) TangemTheme.colors.icon.accent else Color.Transparent,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
)
|
||||
.selectedBorder(selected)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(12.dp),
|
||||
icon = {
|
||||
|
|
@ -163,7 +149,7 @@ private fun Preview() {
|
|||
),
|
||||
),
|
||||
selectedOption = SelectableChainRowUM(
|
||||
event = OnboardingVisaChooseWalletComponent.Params.Event.OtherWallet,
|
||||
event = OnboardingVisaChooseWalletComponent.Params.Event.TangemWallet,
|
||||
icon = R.drawable.ic_tangem_24,
|
||||
text = TextReference.Str("Tangem Wallet"),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ import com.tangem.domain.visa.model.VisaCardId
|
|||
import com.tangem.domain.visa.repository.VisaActivationRepository
|
||||
import com.tangem.domain.visa.repository.VisaAuthRepository
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Config
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.inprogress.OnboardingVisaInProgressComponent.Params
|
||||
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
|
||||
|
|
@ -46,7 +46,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
private val visaAuthTokenStorage: VisaAuthTokenStorage,
|
||||
private val otpStorage: VisaOTPStorage,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
|
@ -173,7 +173,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
|
|||
}
|
||||
|
||||
val userWallet = createUserWallet(params.scanResponse, newTokens)
|
||||
userWalletsListManager.save(userWallet)
|
||||
saveWalletUseCase(userWallet)
|
||||
visaAuthTokenStorage.remove(params.scanResponse.card.cardId)
|
||||
otpStorage.removeOTP(params.scanResponse.card.cardId)
|
||||
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ private fun PinCode(
|
|||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
keyboardType = KeyboardType.Number,
|
||||
keyboardType = KeyboardType.NumberPassword,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import com.tangem.domain.tokens.GetAssetRequirementsUseCase
|
|||
import com.tangem.domain.tokens.GetTokenListUseCase
|
||||
import com.tangem.domain.tokens.error.TokenListError
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.transaction.models.AssetRequirementsCondition
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.onramp.impl.R
|
||||
import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent
|
||||
|
|
@ -210,21 +209,22 @@ internal class OnrampTokenListModel @Inject constructor(
|
|||
val isOperationAvailable = checkAvailabilityByOperation(status = status)
|
||||
val isNotMissedDerivation = status.value !is CryptoCurrencyStatus.MissedDerivation
|
||||
val isNotLoading = status.value !is CryptoCurrencyStatus.Loading
|
||||
|
||||
val requirements = getAssetRequirementsUseCase(
|
||||
userWalletId = userWallet.walletId,
|
||||
currency = status.currency,
|
||||
).getOrNull()
|
||||
|
||||
val isNotTrustlineRequired = requirements !is AssetRequirementsCondition.RequiredTrustline
|
||||
val isAvailableForBuy = rampStateManager.checkAssetRequirements(requirements)
|
||||
val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable
|
||||
|
||||
val isAvailable = when (params.filterOperation) {
|
||||
OnrampOperation.BUY -> {
|
||||
isNotTrustlineRequired
|
||||
isAvailableForBuy
|
||||
} // unreachable state is available for Buy operation
|
||||
OnrampOperation.SELL -> isNotUnreachable
|
||||
OnrampOperation.SWAP -> {
|
||||
isNotUnreachable && isNotTrustlineRequired
|
||||
isNotUnreachable && isAvailableForBuy
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,5 @@ data class PushNotificationsParams(
|
|||
val isBottomSheet: Boolean = false,
|
||||
val nextRoute: AppRoute? = null,
|
||||
val modelCallbacks: PushNotificationsModelCallbacks,
|
||||
val source: AppRoute.PushNotification.Source,
|
||||
)
|
||||
|
|
@ -53,6 +53,15 @@ sealed class PushNotificationAnalyticEvents(
|
|||
),
|
||||
)
|
||||
|
||||
data class NotificationsScreenOpened(
|
||||
val source: AnalyticsParam.ScreensSources,
|
||||
) : PushNotificationAnalyticEvents(
|
||||
event = "Push Notification Screen Opened",
|
||||
params = mapOf(
|
||||
AnalyticsParam.SOURCE to source.value,
|
||||
),
|
||||
)
|
||||
|
||||
data class NotificationsEnabled(
|
||||
val isEnabled: Boolean,
|
||||
) : PushNotificationAnalyticEvents(
|
||||
|
|
|
|||
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