Updated on 2026-08-14
This commit is contained in:
commit
3cbc2dadfb
822 changed files with 12838 additions and 5366 deletions
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:PortfolioFetcher.kt$PortfolioFetcher.Mode.All$val onlyMultiCurrency: Boolean</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$/** * for some Feature specific filtering * combine and update with your Feature data and [PortfolioFetcher.data] */ val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean></ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$val isAccountMode: Flow<Boolean></ID>
|
||||
<ID>UseSumOfInsteadOfFlatMapSize:PortfolioFetcher.kt$PortfolioFetcher.Data$flatten()</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -26,7 +26,7 @@ interface PortfolioFetcher {
|
|||
|
||||
val isSingleChoice: Boolean = balances.values
|
||||
.map { it.accountsBalance.accountStatuses }
|
||||
.flatten().size == 1
|
||||
.sumOf { it.size } == 1
|
||||
|
||||
fun isSingleChoice(walletId: UserWalletId): Boolean = balances[walletId]
|
||||
?.accountsBalance
|
||||
|
|
@ -43,7 +43,7 @@ interface PortfolioFetcher {
|
|||
}
|
||||
|
||||
sealed interface Mode {
|
||||
data class All(val onlyMultiCurrency: Boolean) : Mode
|
||||
data class All(val isOnlyMultiCurrency: Boolean) : Mode
|
||||
data class Wallet(val walletId: UserWalletId) : Mode
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ interface PortfolioSelectorController {
|
|||
* combine and update with your Feature data and [PortfolioFetcher.data]
|
||||
*/
|
||||
val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean>
|
||||
suspend fun isAccountModeSync(): Boolean
|
||||
|
||||
fun selectAccount(accountId: AccountId?)
|
||||
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorModel.kt$PortfolioSelectorModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
|
||||
</CurrentIssues>
|
||||
<CurrentIssues/>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
package com.tangem.features.account.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
|
||||
sealed class AccountSettingsAnalyticEvents(
|
||||
category: String = "Settings / Account",
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category, event, params) {
|
||||
|
||||
class AccountSettingsScreenOpened : AccountSettingsAnalyticEvents(
|
||||
event = "Account Settings Screen Opened",
|
||||
)
|
||||
|
||||
class ButtonManageTokens : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Manage Tokens",
|
||||
)
|
||||
|
||||
class ButtonArchiveAccount : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Archive Account",
|
||||
)
|
||||
|
||||
class ButtonArchiveAccountConfirmation : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Archive Account Confirmation",
|
||||
)
|
||||
|
||||
class ButtonCancelAccountArchivation : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Cancel Account Archivation",
|
||||
)
|
||||
|
||||
class AccountArchived : AccountSettingsAnalyticEvents(
|
||||
event = "Account Archived",
|
||||
)
|
||||
|
||||
class ButtonEdit : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Edit",
|
||||
)
|
||||
|
||||
class AccountEditScreenOpened : AccountSettingsAnalyticEvents(
|
||||
event = "Account Edit Screen Opened",
|
||||
)
|
||||
|
||||
class ButtonSave(
|
||||
val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Save",
|
||||
params = buildMap {
|
||||
val accountName = when (name) {
|
||||
is AccountName.Custom -> name.value
|
||||
AccountName.DefaultMain -> "DefaultMain"
|
||||
}
|
||||
put("Name", accountName)
|
||||
put("Color", icon.color.name)
|
||||
put("Icon", icon.value.name)
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonAddNewAccount(
|
||||
val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
val derivationIndex: Int,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Add New Account",
|
||||
params = buildMap {
|
||||
val accountName = when (name) {
|
||||
is AccountName.Custom -> name.value
|
||||
AccountName.DefaultMain -> "DefaultMain"
|
||||
}
|
||||
put("Name", accountName)
|
||||
put("Color", icon.color.name)
|
||||
put("Icon", icon.value.name)
|
||||
put("Derivation", derivationIndex.toString())
|
||||
},
|
||||
)
|
||||
|
||||
class AccountError(
|
||||
val source: Source,
|
||||
val error: String,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Account Error",
|
||||
params = buildMap {
|
||||
put("Error", error)
|
||||
},
|
||||
)
|
||||
|
||||
enum class Source(val value: String) {
|
||||
NEW_ACCOUNT("New Account"), EDIT("Edit"), ARCHIVE("Archive")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun AccountCreateEditComponent.Params.toAnalyticSource() = when (this) {
|
||||
is AccountCreateEditComponent.Params.Create -> Source.NEW_ACCOUNT
|
||||
is AccountCreateEditComponent.Params.Edit -> Source.EDIT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.account.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
sealed class WalletSettingsAccountAnalyticEvents(
|
||||
category: String = "Settings / Wallet Settings",
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category, event, params) {
|
||||
|
||||
class AccountCreated : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Account Created",
|
||||
)
|
||||
|
||||
class AccountRecovered : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Account Recovered",
|
||||
)
|
||||
|
||||
class ArchivedAccountsScreenOpened : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Archived Accounts Screen Opened",
|
||||
)
|
||||
|
||||
class ButtonRecoverAccount : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Button - Recover Account",
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.account.archived
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -18,6 +19,7 @@ import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
|
|||
import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.features.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUM
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder.Companion.toggleProgress
|
||||
|
|
@ -40,6 +42,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase,
|
||||
private val getArchivedAccountsUseCase: GetArchivedAccountsUseCase,
|
||||
private val umBuilder: AccountArchivedUMBuilder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -52,6 +55,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
private val getArchivedAccountsJob = JobHolder()
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened())
|
||||
getArchivedAccounts()
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +100,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount())
|
||||
uiState.update { it.toggleProgress(accountId, isLoading = true) }
|
||||
val result = withContext(dispatchers.default) {
|
||||
recoverCryptoPortfolioUseCase(accountId)
|
||||
|
|
@ -120,7 +125,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.account_recover_limit_dialog_title),
|
||||
title = resourceReference(R.string.common_something_went_wrong),
|
||||
message = resourceReference(
|
||||
id = R.string.account_recover_limit_dialog_description,
|
||||
formatArgs = wrappedList(AccountList.MAX_ACCOUNTS_COUNT.toString()),
|
||||
|
|
@ -148,6 +153,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun showSuccessRecoverMessage() {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountRecovered())
|
||||
val message = resourceReference(R.string.account_recover_success_message)
|
||||
messageSender.send(ToastMessage(message = message))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.annotation.StringRes
|
|||
import com.tangem.common.ui.account.AccountNameUM
|
||||
import com.tangem.common.ui.account.toDomain
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
|
|
@ -25,6 +26,9 @@ 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.analytics.AccountSettingsAnalyticEvents
|
||||
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents.Companion.toAnalyticSource
|
||||
import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents
|
||||
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
|
||||
|
|
@ -54,6 +58,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase,
|
||||
private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AccountCreateEditComponent.Params>()
|
||||
|
|
@ -65,6 +70,8 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
init {
|
||||
if (params is AccountCreateEditComponent.Params.Create) {
|
||||
updateDerivationInfo(userWalletId = params.userWalletId)
|
||||
} else {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountEditScreenOpened())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +112,12 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val icon = state.account.portfolioIcon.toDomain()
|
||||
val index = state.account.derivationInfo.index ?: return
|
||||
val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return
|
||||
val event = AccountSettingsAnalyticEvents.ButtonAddNewAccount(
|
||||
name = name,
|
||||
icon = icon,
|
||||
derivationIndex = derivationIndex.value,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
uiState.value = uiState.value.toggleProgress(showProgress = true)
|
||||
val result = addCryptoPortfolioUseCase(
|
||||
|
|
@ -118,12 +131,22 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
result
|
||||
.onLeft(::handleAddAccountError)
|
||||
.onRight {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated())
|
||||
showMessage(R.string.account_create_success_message)
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) {
|
||||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = params.toAnalyticSource(),
|
||||
error = when (error) {
|
||||
is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag
|
||||
is AddCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty()
|
||||
},
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
val isDuplicateAccountNamesError = (error as? AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
|
||||
?.cause is AccountList.Error.DuplicateAccountNames
|
||||
when {
|
||||
|
|
@ -141,6 +164,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val icon = state.account.portfolioIcon.toDomain()
|
||||
val isNewName = name != params.account.accountName
|
||||
val isNewIcon = icon != params.account.portfolioIcon
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon))
|
||||
|
||||
uiState.value = uiState.value.toggleProgress(showProgress = true)
|
||||
val result = updateCryptoPortfolioUseCase(
|
||||
|
|
@ -159,6 +183,15 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) {
|
||||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = params.toAnalyticSource(),
|
||||
error = when (error) {
|
||||
is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag
|
||||
is UpdateCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty()
|
||||
UpdateCryptoPortfolioUseCase.Error.NothingToUpdate -> error::class.simpleName.orEmpty()
|
||||
},
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
val isDuplicateAccountNamesError = (error as? UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
|
||||
?.cause is AccountList.Error.DuplicateAccountNames
|
||||
when {
|
||||
|
|
@ -274,7 +307,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
private fun showSomethingWrong() {
|
||||
val dialogMessage = DialogMessage(
|
||||
title = resourceReference(R.string.common_something_went_wrong),
|
||||
message = resourceReference(R.string.account_could_not_create),
|
||||
message = resourceReference(R.string.account_generic_error_dialog_message),
|
||||
)
|
||||
messageSender.send(dialogMessage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
package com.tangem.features.account.createedit.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -30,6 +25,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.account.*
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
|
|
@ -186,9 +183,9 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) {
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||
) {
|
||||
colorsState.list.forEach { color ->
|
||||
colorsState.list.fastForEach { color ->
|
||||
val isSelected = color == colorsState.selected
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -236,8 +233,9 @@ private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) {
|
|||
FlowRow(
|
||||
maxItemsInEachRow = 6,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
iconsState.list.forEachIndexed { index, icon ->
|
||||
iconsState.list.fastForEachIndexed { index, icon ->
|
||||
val isSelected = icon == iconsState.selected
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.account.details
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -20,6 +21,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
|
||||
import com.tangem.features.account.details.entity.AccountDetailsUM
|
||||
import com.tangem.features.account.details.entity.AccountDetailsUM.ArchiveMode
|
||||
|
|
@ -37,6 +39,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
|
||||
singleAccountSupplier: SingleAccountSupplier,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -48,12 +51,14 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
private val accountId = params.account.accountId
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened())
|
||||
singleAccountSupplier(SingleAccountProducer.Params(accountId))
|
||||
.onEach { account -> uiState.update { buildUI(account) } }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onEditAccountClick(account: Account) {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit())
|
||||
router.push(AppRoute.EditAccount(account))
|
||||
}
|
||||
|
||||
|
|
@ -62,17 +67,21 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
source = AppRoute.ManageTokens.Source.SETTINGS,
|
||||
portfolioId = PortfolioId(account.accountId),
|
||||
)
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonManageTokens())
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
private fun onArchiveAccountClick() {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccount())
|
||||
confirmArchiveDialog()
|
||||
}
|
||||
|
||||
private fun confirmArchiveDialog() {
|
||||
val secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
onClick = {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation())
|
||||
},
|
||||
)
|
||||
val firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.account_details_archive_action),
|
||||
|
|
@ -90,6 +99,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun archiveCryptoPortfolio() = modelScope.launch {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation())
|
||||
uiState.update { it.toggleProgress(true) }
|
||||
archiveCryptoPortfolioUseCase(accountId)
|
||||
.onLeft { error ->
|
||||
|
|
@ -97,6 +107,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
uiState.update { it.toggleProgress(false) }
|
||||
}
|
||||
.onRight {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountArchived())
|
||||
val message = resourceReference(R.string.account_archive_success_message)
|
||||
messageSender.send(ToastMessage(message = message))
|
||||
router.pop()
|
||||
|
|
@ -104,21 +115,18 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) {
|
||||
val titleRes = when (error) {
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
|
||||
-> R.string.common_something_went_wrong
|
||||
is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus,
|
||||
-> R.string.account_could_not_archive_referral_program_title
|
||||
}
|
||||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = AccountSettingsAnalyticEvents.Source.ARCHIVE,
|
||||
error = error.tag,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
val titleRes = R.string.common_something_went_wrong
|
||||
val messageRes = when (error) {
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
|
||||
-> R.string.account_could_not_archive
|
||||
-> R.string.account_generic_error_dialog_message
|
||||
is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus,
|
||||
-> R.string.account_could_not_archive_referral_program_message
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor(
|
|||
|
||||
private fun List<UserWallet>.filterWallets(mode: Mode): List<UserWallet> = this.filter { wallet ->
|
||||
when (mode) {
|
||||
is Mode.All -> if (mode.onlyMultiCurrency) wallet.isMultiCurrency else true
|
||||
is Mode.All -> if (mode.isOnlyMultiCurrency) wallet.isMultiCurrency else true
|
||||
is Mode.Wallet -> wallet.walletId == mode.walletId
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ internal class DefaultPortfolioSelectorController @Inject constructor(
|
|||
|
||||
override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true }
|
||||
|
||||
override suspend fun isAccountModeSync(): Boolean = isAccountsModeEnabledUseCase.invokeSync()
|
||||
|
||||
override fun selectAccount(accountId: AccountId?) {
|
||||
_selectedAccount.tryEmit(accountId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val bottomSheetVariant: Boolean = false</ID>
|
||||
<ID>BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val showProgress: Boolean = false</ID>
|
||||
<ID>BooleanPropertyNaming:DefaultAskBiometryComponent.kt$DefaultAskBiometryComponent$val bsShown by bsShown.collectAsStateWithLifecycle()</ID>
|
||||
<ID>MultilineLambdaItParameter:AskBiometryModel.kt$AskBiometryModel${ uiMessageSender.send( SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")), ) }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -41,10 +41,10 @@ internal class DefaultAskBiometryComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val bsShown by bsShown.collectAsStateWithLifecycle()
|
||||
val bsConfig = remember(this, bsShown) {
|
||||
val isBSShown by bsShown.collectAsStateWithLifecycle()
|
||||
val bsConfig = remember(this, isBSShown) {
|
||||
TangemBottomSheetConfig(
|
||||
isShown = bsShown,
|
||||
isShown = isBSShown,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
|
||||
private val _uiState = MutableStateFlow(
|
||||
AskBiometryUM(
|
||||
bottomSheetVariant = params.isBottomSheetVariant,
|
||||
isBottomSheetVariant = params.isBottomSheetVariant,
|
||||
onAllowClick = ::onAllowClick,
|
||||
onDontAllowClick = ::dontAllow,
|
||||
onDismiss = ::dismiss,
|
||||
|
|
@ -85,7 +85,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
_uiState.update { it.copy(showProgress = true) }
|
||||
_uiState.update { it.copy(shouldShowProgress = true) }
|
||||
|
||||
/*
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
uiMessageSender.send(
|
||||
SnackbarMessage(stringReference("No selected user wallet")),
|
||||
)
|
||||
_uiState.update { it.copy(showProgress = false) }
|
||||
_uiState.update { it.copy(shouldShowProgress = false) }
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
if (_uiState.value.bottomSheetVariant) {
|
||||
if (_uiState.value.isBottomSheetVariant) {
|
||||
dismissBSFlow.emit(Unit)
|
||||
delay(timeMillis = 500)
|
||||
}
|
||||
|
|
@ -142,9 +142,9 @@ internal class AskBiometryModel @Inject constructor(
|
|||
userWalletId = userWallet.walletId,
|
||||
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
|
||||
changeUnsecured = false,
|
||||
).onLeft {
|
||||
).onLeft { error ->
|
||||
uiMessageSender.send(
|
||||
SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")),
|
||||
SnackbarMessage(stringReference("Something went wrong. Please contact support: $error")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
|
|||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
if (state.bottomSheetVariant) {
|
||||
if (state.isBottomSheetVariant) {
|
||||
Header(onCloseClick = state.onDismiss)
|
||||
}
|
||||
|
||||
|
|
@ -128,12 +128,12 @@ private fun Footer(state: AskBiometryUM, modifier: Modifier = Modifier) {
|
|||
) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showProgress = state.showProgress,
|
||||
showProgress = state.shouldShowProgress,
|
||||
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics),
|
||||
onClick = state.onAllowClick,
|
||||
)
|
||||
|
||||
if (state.bottomSheetVariant.not()) {
|
||||
if (state.isBottomSheetVariant.not()) {
|
||||
SpacerH12()
|
||||
|
||||
SecondaryButton(
|
||||
|
|
@ -199,7 +199,7 @@ private fun Preview() {
|
|||
private fun PreviewBS() {
|
||||
TangemThemePreview {
|
||||
AskBiometry(
|
||||
state = AskBiometryUM(bottomSheetVariant = true),
|
||||
state = AskBiometryUM(isBottomSheetVariant = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@ package com.tangem.features.biometry.impl.ui.state
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class AskBiometryUM(
|
||||
val bottomSheetVariant: Boolean = false,
|
||||
val showProgress: Boolean = false,
|
||||
val isBottomSheetVariant: Boolean = false,
|
||||
val shouldShowProgress: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val onAllowClick: () -> Unit = {},
|
||||
val onDontAllowClick: () -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.hotWallet)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,20 @@ package com.tangem.features.createwalletselection
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
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.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.analytics.Shop
|
||||
import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM
|
||||
import com.tangem.features.createwalletselection.impl.R
|
||||
|
|
@ -28,9 +33,12 @@ import javax.inject.Inject
|
|||
internal class CreateWalletSelectionModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val isHotWalletCreationSupported: IsHotWalletCreationSupported,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
internal val uiState: StateFlow<CreateWalletSelectionUM>
|
||||
|
|
@ -93,7 +101,20 @@ internal class CreateWalletSelectionModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onMobileWalletClick() {
|
||||
router.push(AppRoute.CreateMobileWallet)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet(
|
||||
source = AnalyticsParam.ScreensSources.AddNewWallet.value,
|
||||
),
|
||||
)
|
||||
if (!isHotWalletCreationSupported()) {
|
||||
uiMessageSender.send(
|
||||
hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.AddNewWallet.value))
|
||||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
|
|
@ -101,8 +122,7 @@ internal class CreateWalletSelectionModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.AddNewWallet))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import androidx.compose.animation.AnimatedVisibility
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -22,6 +25,7 @@ import com.tangem.core.ui.components.label.entity.LabelStyle
|
|||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -71,6 +75,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
|
|
@ -113,6 +118,7 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun WalletBlock(
|
||||
title: String,
|
||||
|
|
@ -137,11 +143,11 @@ private fun WalletBlock(
|
|||
vertical = 12.dp,
|
||||
),
|
||||
) {
|
||||
Row {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.padding(end = 8.dp),
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -176,6 +182,7 @@ private fun WalletBlock(
|
|||
private fun Feature(feature: CreateWalletSelectionUM.Feature, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
|
|
@ -221,6 +228,8 @@ private fun AlreadyHaveTangemWalletBlock(
|
|||
text = stringResourceSafe(R.string.wallet_add_hardware_purchase),
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
SecondaryButton(
|
||||
|
|
@ -242,7 +251,7 @@ private fun PreviewCreateWalletContent() {
|
|||
onBackClick = { },
|
||||
blocks = persistentListOf(
|
||||
CreateWalletSelectionUM.Block(
|
||||
title = resourceReference(R.string.wallet_create_hardware_title),
|
||||
title = stringReference("Hardware wallet very long title"),
|
||||
titleLabel = LabelUM(
|
||||
text = resourceReference(R.string.common_recommended),
|
||||
style = LabelStyle.ACCENT,
|
||||
|
|
@ -277,6 +286,7 @@ private fun PreviewCreateWalletContent() {
|
|||
onClick = { },
|
||||
),
|
||||
),
|
||||
shouldShowAlreadyHaveWallet = true,
|
||||
onBuyClick = { },
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.hotWallet)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:CreateWalletStartUM.kt$CreateWalletStartUM$val showScanSecondaryButton: Boolean</ID>
|
||||
<ID>MultilineLambdaItParameter:CreateWalletStartContent.kt${ FeatureItem( iconResId = it.iconResId, text = it.text, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CreateWalletStartModel.kt$CreateWalletStartModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { appRouter.replaceAll(AppRoute.Wallet) } } } }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -6,8 +6,9 @@ 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.analytics.models.Basic
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -18,12 +19,13 @@ 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.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
|
|
@ -51,14 +53,16 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
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 isHotWalletCreationSupported: IsHotWalletCreationSupported,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<CreateWalletStartComponent.Params>()
|
||||
|
|
@ -84,7 +88,7 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
),
|
||||
),
|
||||
imageResId = R.drawable.img_hardware_wallet,
|
||||
showScanSecondaryButton = true,
|
||||
shouldShowScanSecondaryButton = true,
|
||||
onPrimaryButtonClick = ::onBuyClick,
|
||||
primaryButtonText = resourceReference(R.string.details_buy_wallet),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
|
|
@ -112,7 +116,7 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
),
|
||||
),
|
||||
imageResId = R.drawable.img_mobile_wallet,
|
||||
showScanSecondaryButton = false,
|
||||
shouldShowScanSecondaryButton = false,
|
||||
onPrimaryButtonClick = ::onStartWithMobileWalletClick,
|
||||
primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title),
|
||||
|
|
@ -125,15 +129,38 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(
|
||||
event = IntroductionProcess.CreateWalletIntroScreenOpened(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
analyticsEventHandler.send(
|
||||
event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateWalletIntro),
|
||||
)
|
||||
scanCard()
|
||||
}
|
||||
|
||||
private fun onStartWithMobileWalletClick() {
|
||||
router.push(AppRoute.CreateMobileWallet)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet(
|
||||
source = AnalyticsParam.ScreensSources.CreateWalletIntro.value,
|
||||
),
|
||||
)
|
||||
if (!isHotWalletCreationSupported()) {
|
||||
uiMessageSender.send(
|
||||
hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.CreateWalletIntro.value))
|
||||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWalletIntro))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
|
|
@ -182,11 +209,11 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
}
|
||||
|
||||
saveWalletUseCase(userWallet = userWallet).fold(
|
||||
ifLeft = {
|
||||
ifLeft = { error ->
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
when (it) {
|
||||
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
|
||||
when (error) {
|
||||
is SaveWalletError.DataError -> Timber.e(error.toString(), "Unable to save user wallet")
|
||||
is SaveWalletError.WalletAlreadySaved -> {
|
||||
userWalletsListRepository.unlock(
|
||||
userWalletId = userWallet.walletId,
|
||||
|
|
@ -199,28 +226,11 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
|
||||
isImported = isImported,
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
uiState.update { it.copy(isScanInProgress = isLoading) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ internal data class CreateWalletStartUM(
|
|||
val featureItems: ImmutableList<FeatureItem>,
|
||||
val imageResId: Int,
|
||||
val isScanInProgress: Boolean,
|
||||
val showScanSecondaryButton: Boolean,
|
||||
val shouldShowScanSecondaryButton: Boolean,
|
||||
val primaryButtonText: TextReference,
|
||||
val onPrimaryButtonClick: () -> Unit,
|
||||
val otherMethodDescription: TextReference,
|
||||
|
|
|
|||
|
|
@ -120,10 +120,10 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi
|
|||
horizontalArrangement = Arrangement.Center,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
state.featureItems.forEach {
|
||||
state.featureItems.forEach { item ->
|
||||
FeatureItem(
|
||||
iconResId = it.iconResId,
|
||||
text = it.text,
|
||||
iconResId = item.iconResId,
|
||||
text = item.text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi
|
|||
)
|
||||
},
|
||||
bottomContent = {
|
||||
if (state.showScanSecondaryButton) {
|
||||
if (state.shouldShowScanSecondaryButton) {
|
||||
SecondaryButtonIconEnd(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -232,7 +232,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi
|
|||
minImageHeight = 160.dp,
|
||||
)
|
||||
}
|
||||
if (!state.showScanSecondaryButton) {
|
||||
if (!state.shouldShowScanSecondaryButton) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
|
|
@ -425,7 +425,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide
|
|||
),
|
||||
),
|
||||
imageResId = R.drawable.img_hardware_wallet,
|
||||
showScanSecondaryButton = true,
|
||||
shouldShowScanSecondaryButton = true,
|
||||
onPrimaryButtonClick = { },
|
||||
primaryButtonText = resourceReference(R.string.details_buy_wallet),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
|
|
@ -455,7 +455,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide
|
|||
),
|
||||
),
|
||||
imageResId = R.drawable.img_mobile_wallet,
|
||||
showScanSecondaryButton = false,
|
||||
shouldShowScanSecondaryButton = false,
|
||||
onPrimaryButtonClick = { },
|
||||
primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title),
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ dependencies {
|
|||
implementation(projects.features.tester.api)
|
||||
implementation(projects.features.createWalletSelection.api)
|
||||
implementation(projects.features.hotWallet.api)
|
||||
implementation(projects.features.tangempay.details.api)
|
||||
|
||||
/* Project - Core */
|
||||
implementation(projects.core.decompose)
|
||||
|
|
@ -46,6 +47,7 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.legacy)
|
||||
implementation(projects.domain.visa)
|
||||
|
||||
/* SDK */
|
||||
// TODO: For TangemError model, should be removed after card domain scanning refactoring
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
<ID>MultilineLambdaItParameter:DetailsModel.kt$DetailsModel${ it.copy( selectFeedbackEmailTypeBSConfig = it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:PreviewUserWalletListComponent.kt$PreviewUserWalletListComponent${ it.copy( balance = UserWalletItemUM.Balance.Loaded( value = "1.000 BTC", isFlickering = true, ), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:UserWalletSaver.kt$UserWalletSaver${ val message = it.message if (!message.isNullOrEmpty()) { messageSender.send(SnackbarMessage(message)) } }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:UserWalletListModel.kt$UserWalletListModel$private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)</ID>
|
||||
<ID>RedundantSuspendModifier:UserWalletSaver.kt$UserWalletSaver$suspend</ID>
|
||||
<ID>UnnecessaryLet:ItemsBuilder.kt$ItemsBuilder$let(::add)</ID>
|
||||
</CurrentIssues>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import android.content.res.Resources
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.AppInstanceIdProvider
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -13,13 +16,15 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
|||
import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
||||
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.pay.TangemPayEligibilityManager
|
||||
import com.tangem.domain.redux.LegacyAction
|
||||
import com.tangem.domain.redux.ReduxStateHolder
|
||||
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
|
||||
import com.tangem.features.details.component.DetailsComponent
|
||||
|
|
@ -29,6 +34,8 @@ import com.tangem.features.details.entity.DetailsUM
|
|||
import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS
|
||||
import com.tangem.features.details.utils.ItemsBuilder
|
||||
import com.tangem.features.details.utils.SocialsBuilder
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.tangempay.TangemPayFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.version.AppVersionProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
|
@ -46,7 +53,7 @@ import javax.inject.Inject
|
|||
@Suppress("LongParameterList")
|
||||
internal class DetailsModel @Inject constructor(
|
||||
socialsBuilder: SocialsBuilder,
|
||||
itemsBuilder: ItemsBuilder,
|
||||
private val itemsBuilder: ItemsBuilder,
|
||||
private val appVersionProvider: AppVersionProvider,
|
||||
private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase,
|
||||
private val router: Router,
|
||||
|
|
@ -60,6 +67,11 @@ internal class DetailsModel @Inject constructor(
|
|||
private val getWalletsUseCase: GetWalletsUseCase,
|
||||
private val feedbackFeatureToggles: FeedbackFeatureToggles,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val tangemPayEligibilityManager: TangemPayEligibilityManager,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params: DetailsComponent.Params = paramsContainer.require()
|
||||
|
|
@ -92,6 +104,8 @@ internal class DetailsModel @Inject constructor(
|
|||
),
|
||||
)
|
||||
|
||||
addTangemPayItemIfEligible()
|
||||
|
||||
state = MutableStateFlow(
|
||||
value = DetailsUM(
|
||||
items = items.value,
|
||||
|
|
@ -216,7 +230,12 @@ internal class DetailsModel @Inject constructor(
|
|||
|
||||
private fun onBuyClick() {
|
||||
modelScope.launch {
|
||||
urlOpener.openUrl(buildBuyLink())
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Settings))
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
} else {
|
||||
urlOpener.openUrl(buildBuyLink())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +245,16 @@ internal class DetailsModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun addTangemPayItemIfEligible() {
|
||||
if (!tangemPayFeatureToggles.isTangemPayEnabled) return
|
||||
modelScope.launch {
|
||||
val isEligible = tangemPayEligibilityManager.getEligibleWallets().isNotEmpty()
|
||||
if (isEligible) {
|
||||
items.update { itemsBuilder.addVisaItem(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})"
|
||||
|
||||
private suspend fun buildBuyLink(): String {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.tangem.features.details.model
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.userwallet.handle
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.SignIn
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
|
|
@ -39,6 +42,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
private val userWalletSaver: UserWalletSaver,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val unlockWalletUseCase: UnlockWalletUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
|
||||
|
|
@ -87,6 +91,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
|
||||
private fun onAddNewWalletClick() {
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn))
|
||||
router.push(AppRoute.CreateWalletSelection)
|
||||
} else {
|
||||
withProgress(isWalletSavingInProgress) {
|
||||
|
|
@ -105,6 +110,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
error.handle(
|
||||
onUserCancelled = {},
|
||||
onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
showMessage = messageSender::send,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,9 @@ private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing6)
|
||||
.testTag(DetailsScreenTestTags.VERSION_NAME),
|
||||
text = model.appVersion,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
|
|
|
|||
|
|
@ -37,6 +37,17 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
|
|||
).let(::add)
|
||||
}.toImmutableList()
|
||||
|
||||
fun addVisaItem(items: ImmutableList<DetailsItemUM>): ImmutableList<DetailsItemUM> {
|
||||
return items.toMutableList().map { block ->
|
||||
if (block.id == "shop" && block is DetailsItemUM.Basic) {
|
||||
val newItems = block.items.toMutableList().apply { add(getVisaItem()) }
|
||||
block.copy(items = newItems.toImmutableList())
|
||||
} else {
|
||||
block
|
||||
}
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun buildWalletConnectBlock(isWalletConnectAvailable: Boolean, userWalletId: UserWalletId): DetailsItemUM? {
|
||||
return if (isWalletConnectAvailable) {
|
||||
DetailsItemUM.WalletConnect(
|
||||
|
|
@ -114,4 +125,15 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
|
|||
).let(::add)
|
||||
}.toPersistentList(),
|
||||
)
|
||||
|
||||
private fun getVisaItem(): DetailsItemUM.Basic.Item = DetailsItemUM.Basic.Item(
|
||||
id = "get_tangem_visa",
|
||||
block = BlockUM(
|
||||
text = resourceReference(R.string.details_get_visa),
|
||||
iconRes = R.drawable.ic_tangem_pay_24,
|
||||
onClick = {
|
||||
router.push(AppRoute.TangemPayOnboarding(AppRoute.TangemPayOnboarding.Mode.FromBannerInSettings))
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.feed.entry
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.features.feed.entry.BottomSheetState
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface FeedEntryComponent {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.news)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
|
|
@ -86,11 +87,13 @@ dependencies {
|
|||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/* Common */
|
||||
implementation(projects.common.ui)
|
||||
implementation(projects.common.uiCharts)
|
||||
implementation(projects.common.routing)
|
||||
implementation(projects.common.uiMarkets)
|
||||
|
||||
/* Libs */
|
||||
implementation(projects.libs.crypto)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.tangem.features.feed.components
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.arkivanov.decompose.extensions.compose.subscribeAsState
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.arkivanov.decompose.router.stack.StackNavigation
|
||||
import com.arkivanov.decompose.router.stack.childStack
|
||||
|
|
@ -13,11 +16,15 @@ import com.arkivanov.decompose.value.Value
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.navigation.inner.InnerRouter
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.entry.BottomSheetState
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import com.tangem.features.feed.model.feed.FeedModelClickIntents
|
||||
import com.tangem.features.feed.ui.EntryBottomSheetContent
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -35,7 +42,37 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
popCallback = { onChildBack() },
|
||||
)
|
||||
|
||||
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, Any>> = childStack(
|
||||
private val clickIntents = object : FeedEntryClickIntents {
|
||||
override fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency) {
|
||||
innerRouter.push(
|
||||
route = FeedEntryChildFactory.Child.TokenDetails(
|
||||
params = DefaultMarketsTokenDetailsComponent.Params(
|
||||
token = token,
|
||||
appCurrency = appCurrency,
|
||||
shouldShowPortfolio = true,
|
||||
analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams(
|
||||
blockchain = null,
|
||||
source = "Market",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMarketOpenClick(sortBy: SortByTypeUM) {
|
||||
innerRouter.push(FeedEntryChildFactory.Child.TokenList)
|
||||
}
|
||||
|
||||
override fun onArticleClick(articleId: Int) {
|
||||
innerRouter.push(FeedEntryChildFactory.Child.NewsDetails)
|
||||
}
|
||||
|
||||
override fun onOpenAllNews() {
|
||||
innerRouter.push(FeedEntryChildFactory.Child.NewsList)
|
||||
}
|
||||
}
|
||||
|
||||
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>> = childStack(
|
||||
key = "main",
|
||||
source = stackNavigation,
|
||||
serializer = FeedEntryChildFactory.Child.serializer(),
|
||||
|
|
@ -48,7 +85,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
componentContext = factoryContext,
|
||||
router = innerRouter,
|
||||
),
|
||||
onTokenClick = ::marketsListTokenSelected,
|
||||
feedEntryClickIntents = clickIntents,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -59,22 +96,15 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
modifier: Modifier,
|
||||
) {
|
||||
bottomSheetState // TODO will be continued in next tasks.
|
||||
}
|
||||
val stackState by stack.subscribeAsState()
|
||||
|
||||
private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) {
|
||||
innerRouter.push(
|
||||
route = FeedEntryChildFactory.Child.TokenDetails(
|
||||
params = DefaultMarketsTokenDetailsComponent.Params(
|
||||
token = token,
|
||||
appCurrency = appCurrency,
|
||||
shouldShowPortfolio = true,
|
||||
analyticsParams = DefaultMarketsTokenDetailsComponent.AnalyticsParams(
|
||||
blockchain = null,
|
||||
source = "Market",
|
||||
),
|
||||
),
|
||||
),
|
||||
BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) {
|
||||
onChildBack()
|
||||
}
|
||||
|
||||
EntryBottomSheetContent(
|
||||
stackState = stackState,
|
||||
onHeaderSizeChange = onHeaderSizeChange,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -88,4 +118,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
|
|||
interface Factory : FeedEntryComponent.Factory {
|
||||
override fun create(context: AppComponentContext): DefaultFeedEntryComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal interface FeedEntryClickIntents : FeedModelClickIntents
|
||||
|
|
@ -3,16 +3,16 @@ package com.tangem.features.feed.components
|
|||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent
|
||||
import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent
|
||||
import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent
|
||||
import com.tangem.features.feed.components.news.list.DefaultNewsListComponent
|
||||
import kotlinx.serialization.Serializable
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class FeedEntryChildFactory {
|
||||
internal class FeedEntryChildFactory @Inject constructor() {
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
|
|
@ -42,8 +42,8 @@ internal class FeedEntryChildFactory {
|
|||
fun createChild(
|
||||
child: Child,
|
||||
appComponentContext: AppComponentContext,
|
||||
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
|
||||
): Any {
|
||||
feedEntryClickIntents: FeedEntryClickIntents,
|
||||
): ComposableModularContentComponent {
|
||||
return when (child) {
|
||||
is Child.TokenDetails -> {
|
||||
DefaultMarketsTokenDetailsComponent(
|
||||
|
|
@ -54,7 +54,12 @@ internal class FeedEntryChildFactory {
|
|||
is Child.TokenList -> {
|
||||
DefaultMarketsTokenListComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
onTokenClick = onTokenClick,
|
||||
onTokenClick = { token, appCurrency ->
|
||||
feedEntryClickIntents.onMarketItemClick(
|
||||
token,
|
||||
appCurrency,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
Child.NewsDetails -> {
|
||||
|
|
@ -70,6 +75,7 @@ internal class FeedEntryChildFactory {
|
|||
Child.Feed -> {
|
||||
DefaultFeedComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,49 @@
|
|||
package com.tangem.features.feed.components.feed
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.compose.LifecycleStartEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import com.tangem.features.feed.model.feed.FeedModelClickIntents
|
||||
import com.tangem.features.feed.ui.feed.FeedList
|
||||
import com.tangem.features.feed.ui.feed.FeedListHeader
|
||||
|
||||
internal class DefaultFeedComponent(
|
||||
appComponentContext: AppComponentContext,
|
||||
private val params: FeedParams,
|
||||
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
|
||||
|
||||
private val feedComponentModel = getOrCreateModel<FeedComponentModel, FeedParams>(params = params)
|
||||
|
||||
@Composable
|
||||
override fun Title() {
|
||||
val state by feedComponentModel.state.collectAsStateWithLifecycle()
|
||||
FeedListHeader(state.searchBar)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
LifecycleStartEffect(Unit) {
|
||||
feedComponentModel.isVisibleOnScreen.value = true
|
||||
onStopOrDispose {
|
||||
feedComponentModel.isVisibleOnScreen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
val state by feedComponentModel.state.collectAsStateWithLifecycle()
|
||||
FeedList(
|
||||
modifier = modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Footer() {
|
||||
}
|
||||
override fun Footer() = Unit
|
||||
|
||||
data class FeedParams(val feedClickIntents: FeedModelClickIntents)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.features.feed.di
|
||||
|
||||
import com.tangem.features.feed.components.DefaultFeedEntryComponent
|
||||
import com.tangem.features.feed.entry.components.FeedEntryComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface FeedComponentModule {
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindFeedEntryComponent(factory: DefaultFeedEntryComponent.Factory): FeedEntryComponent.Factory
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeedComponentModel::class)
|
||||
fun bindsFeedComponentModel(model: FeedComponentModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package com.tangem.features.feed.model.converter
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartData
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
|
||||
import com.tangem.common.ui.charts.state.sorted
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
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.*
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarket
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListUM
|
||||
import com.tangem.utils.converter.Converter
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
internal class MarketsTokenItemConverter(
|
||||
private val currentTrendInterval: MarketsListUM.TrendInterval,
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<TokenMarket, MarketsListItemUM> {
|
||||
|
||||
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false)
|
||||
|
||||
override fun convert(value: TokenMarket): MarketsListItemUM {
|
||||
return MarketsListItemUM(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
currencySymbol = value.symbol,
|
||||
ratingPosition = value.marketRating?.toString(),
|
||||
marketCap = value.getMarketCap(),
|
||||
iconUrl = value.imageUrlLarge,
|
||||
price = value.getCurrentPrice(),
|
||||
trendPercentText = value.getTrendPercent(),
|
||||
trendType = value.getTrendType(),
|
||||
chartData = value.getChartData(),
|
||||
isUnder100kMarketCap = value.isUnderMarketCapLimit,
|
||||
stakingRate = value.stakingRate?.format { percent() }?.let {
|
||||
resourceReference(R.string.markets_apy_placeholder, wrappedList(it))
|
||||
},
|
||||
updateTimestamp = value.updateTimestamp,
|
||||
)
|
||||
}
|
||||
|
||||
fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM {
|
||||
require(prev.id == new.id) {
|
||||
"Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]"
|
||||
}
|
||||
|
||||
return prevUI.copy(
|
||||
name = new.name,
|
||||
currencySymbol = new.symbol,
|
||||
ratingPosition = new.marketRating?.toString(),
|
||||
marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() },
|
||||
iconUrl = new.imageUrlLarge,
|
||||
price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) {
|
||||
new.getCurrentPrice(
|
||||
prev = prev,
|
||||
)
|
||||
},
|
||||
trendPercentText = ifChanged(
|
||||
prev.tokenQuotesShort,
|
||||
new.tokenQuotesShort,
|
||||
prevUI.trendPercentText,
|
||||
) { new.getTrendPercent() },
|
||||
trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() },
|
||||
chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() },
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <T, R> ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R {
|
||||
return if (force || prev != new) change(new) else prevR
|
||||
}
|
||||
|
||||
private fun TokenMarket.getMarketCap(): String? {
|
||||
val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null
|
||||
|
||||
return value.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).compact(
|
||||
threeDigitsMethod = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price {
|
||||
val prevPrice = prev?.tokenQuotesShort?.currentPrice
|
||||
|
||||
val priceText = tokenQuotesShort.currentPrice.format {
|
||||
fiat(
|
||||
fiatCurrencyCode = appCurrency.code,
|
||||
fiatCurrencySymbol = appCurrency.symbol,
|
||||
).price()
|
||||
}
|
||||
|
||||
val changeType = if (prevPrice != null) {
|
||||
if (tokenQuotesShort.currentPrice > prevPrice) {
|
||||
PriceChangeType.UP
|
||||
} else {
|
||||
PriceChangeType.DOWN
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return MarketsListItemUM.Price(
|
||||
text = priceText,
|
||||
changeType = changeType,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarket.getChartData(): MarketChartRawData? {
|
||||
val chart = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenCharts.h24
|
||||
MarketsListUM.TrendInterval.D7 -> tokenCharts.week
|
||||
MarketsListUM.TrendInterval.M1 -> tokenCharts.month
|
||||
}
|
||||
|
||||
return chart?.let { ct ->
|
||||
priceAndTimePointValuesConverter.convert(
|
||||
MarketChartData.Data(
|
||||
y = ct.priceY.toImmutableList(),
|
||||
x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(),
|
||||
).sorted(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun TokenMarket.getTrendType(): PriceChangeType {
|
||||
val percent = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
|
||||
MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
|
||||
MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
val scaled = percent?.setScale(4, RoundingMode.HALF_UP)
|
||||
return when {
|
||||
scaled == null -> PriceChangeType.NEUTRAL
|
||||
scaled > BigDecimal.ZERO -> PriceChangeType.UP
|
||||
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
|
||||
else -> PriceChangeType.NEUTRAL
|
||||
}
|
||||
}
|
||||
|
||||
private fun TokenMarket.getTrendPercent(): String {
|
||||
val percent = when (currentTrendInterval) {
|
||||
MarketsListUM.TrendInterval.H24 -> tokenQuotesShort.h24ChangePercent
|
||||
MarketsListUM.TrendInterval.D7 -> tokenQuotesShort.weekChangePercent
|
||||
MarketsListUM.TrendInterval.M1 -> tokenQuotesShort.monthChangePercent
|
||||
}
|
||||
|
||||
return percent.format { percent() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
package com.tangem.features.feed.model.feed
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.utils.DateTimeFormatters
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase
|
||||
import com.tangem.domain.markets.TokenMarketListConfig
|
||||
import com.tangem.domain.markets.toSerializableParam
|
||||
import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase
|
||||
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
|
||||
import com.tangem.features.feed.components.feed.DefaultFeedComponent
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentHashMap
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.DateTimeZone
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class FeedComponentModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase,
|
||||
private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase,
|
||||
getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
paramsContainer: ParamsContainer,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<DefaultFeedComponent.FeedParams>()
|
||||
|
||||
private var quotesUpdateJob: Job? = null
|
||||
|
||||
private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = AppCurrency.Default,
|
||||
)
|
||||
|
||||
private val marketsBatchFlowManager = FeedMarketsBatchFlowManager(
|
||||
getTopFiveMarketTokenUseCase = getTopFiveMarketTokenUseCase,
|
||||
currentAppCurrency = Provider { currentAppCurrency.value },
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
internal val state: StateFlow<FeedListUM>
|
||||
field = MutableStateFlow<FeedListUM>(initialState())
|
||||
|
||||
private val searchBarStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
SearchBarStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
onStateUpdate = { newState -> state.update { newState } },
|
||||
)
|
||||
}
|
||||
|
||||
private val trendingNewsStateFactory by lazy(LazyThreadSafetyMode.NONE) {
|
||||
TrendingNewsStateFactory(
|
||||
currentStateProvider = Provider { state.value },
|
||||
onStateUpdate = { newState -> state.update { newState } },
|
||||
)
|
||||
}
|
||||
|
||||
val isVisibleOnScreen = MutableStateFlow(false)
|
||||
|
||||
init {
|
||||
updateCallbacks()
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase()
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
combine(
|
||||
flow = marketsBatchFlowManager.itemsByOrder,
|
||||
flow2 = marketsBatchFlowManager.loadingStatesByOrder,
|
||||
flow3 = marketsBatchFlowManager.errorStatesByOrder,
|
||||
flow4 = manageTrendingNewsUseCase.observeTrendingNews(),
|
||||
) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult ->
|
||||
updateMarketCharts(itemsByOrder, loadingStatesByOrder, errorStatesByOrder)
|
||||
trendingNewsStateFactory.updateTrendingNewsState(
|
||||
result = trendingNewsResult,
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
}
|
||||
},
|
||||
)
|
||||
updateGlobalState()
|
||||
val currentSortType = state.value.marketChartConfig.currentSortByType
|
||||
val items = itemsByOrder[currentSortType]
|
||||
val isLoading = loadingStatesByOrder[currentSortType] == true
|
||||
if (items != null && items.isNotEmpty() && !isLoading) {
|
||||
val order = currentSortType.toOrder()
|
||||
marketsBatchFlowManager.loadCharts(order)
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
currentAppCurrency.drop(1).collect {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
}
|
||||
}
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
TokenMarketListConfig.Order.entries.forEach { order ->
|
||||
marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey ->
|
||||
marketsBatchFlowManager.loadCharts(order)
|
||||
if (batchKey == 0) {
|
||||
startQuotesUpdateTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialState(): FeedListUM {
|
||||
return FeedListUM(
|
||||
currentDate = getCurrentDate(),
|
||||
searchBar = SearchBarUM(
|
||||
placeholderText = resourceReference(R.string.markets_search_header_title),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = { },
|
||||
),
|
||||
feedListCallbacks = FeedListCallbacks(
|
||||
onSearchClick = {},
|
||||
onMarketOpenClick = {},
|
||||
onArticleClick = {},
|
||||
onOpenAllNews = {},
|
||||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
),
|
||||
news = NewsUM.Loading,
|
||||
trendingArticle = null,
|
||||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach {
|
||||
put(it, MarketChartUM.Loading)
|
||||
}
|
||||
}.toPersistentHashMap(),
|
||||
currentSortByType = SortByTypeUM.Trending,
|
||||
),
|
||||
globalState = GlobalFeedState.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getCurrentDate(): String {
|
||||
val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault())
|
||||
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
|
||||
}
|
||||
|
||||
private fun updateMarketCharts(
|
||||
itemsByOrder: Map<SortByTypeUM, ImmutableList<com.tangem.common.ui.markets.models.MarketsListItemUM>>,
|
||||
loadingStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
errorStatesByOrder: Map<SortByTypeUM, Boolean>,
|
||||
) {
|
||||
state.update { currentState ->
|
||||
val newMarketCharts = buildMap {
|
||||
SortByTypeUM.entries.forEach { sortByType ->
|
||||
val items = itemsByOrder[sortByType] ?: persistentListOf()
|
||||
val isLoading = loadingStatesByOrder[sortByType] == true
|
||||
val hasError = errorStatesByOrder[sortByType] == true
|
||||
|
||||
when {
|
||||
hasError -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
isLoading -> {
|
||||
put(sortByType, MarketChartUM.Loading)
|
||||
}
|
||||
items.isEmpty() -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.LoadingError(
|
||||
onRetryClicked = {
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
put(
|
||||
sortByType,
|
||||
MarketChartUM.Content(
|
||||
items = items,
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = sortByType == currentState.marketChartConfig.currentSortByType,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.toPersistentHashMap()
|
||||
|
||||
currentState.copy(
|
||||
marketChartConfig = currentState.marketChartConfig.copy(
|
||||
marketCharts = newMarketCharts,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateGlobalState() {
|
||||
state.update { currentState ->
|
||||
val newsState = currentState.news
|
||||
val marketCharts = currentState.marketChartConfig.marketCharts
|
||||
|
||||
val isNewsLoading = newsState is NewsUM.Loading
|
||||
val areAllChartsLoading = marketCharts.values.all { it is MarketChartUM.Loading }
|
||||
|
||||
val isNewsError = newsState is NewsUM.Error
|
||||
val areAllChartsError = marketCharts.values.all { it is MarketChartUM.LoadingError }
|
||||
|
||||
val newGlobalState = when {
|
||||
isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading
|
||||
isNewsError && areAllChartsError -> GlobalFeedState.Error(
|
||||
onRetryClicked = {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
fetchTrendingNewsUseCase.invoke()
|
||||
marketsBatchFlowManager.reloadAll()
|
||||
}
|
||||
},
|
||||
)
|
||||
else -> GlobalFeedState.Content
|
||||
}
|
||||
|
||||
val currentGlobalState = currentState.globalState
|
||||
if (currentGlobalState::class != newGlobalState::class) {
|
||||
currentState.copy(globalState = newGlobalState)
|
||||
} else {
|
||||
currentState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSortTypeClick(sortByType: SortByTypeUM) {
|
||||
state.update { currentState ->
|
||||
val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) ->
|
||||
when (chart) {
|
||||
is MarketChartUM.Content -> {
|
||||
chart.copy(
|
||||
sortChartConfig = chart.sortChartConfig.copy(
|
||||
isSelected = chartSortType == sortByType,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> chart
|
||||
}
|
||||
}
|
||||
|
||||
currentState.copy(
|
||||
marketChartConfig = currentState.marketChartConfig.copy(
|
||||
currentSortByType = sortByType,
|
||||
marketCharts = updatedCharts.toPersistentHashMap(),
|
||||
),
|
||||
)
|
||||
}
|
||||
modelScope.launch(dispatchers.default) {
|
||||
marketsBatchFlowManager.loadCharts(sortByType.toOrder())
|
||||
}
|
||||
}
|
||||
|
||||
private fun startQuotesUpdateTimer() {
|
||||
quotesUpdateJob?.cancel()
|
||||
quotesUpdateJob = modelScope.launch {
|
||||
while (true) {
|
||||
delay(DELAY_TO_FETCH_QUOTES)
|
||||
isVisibleOnScreen.first { it }
|
||||
marketsBatchFlowManager.updateQuotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCallbacks() {
|
||||
state.update { feedListUM ->
|
||||
feedListUM.copy(
|
||||
searchBar = state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange),
|
||||
feedListCallbacks = feedListUM.feedListCallbacks.copy(
|
||||
onSortTypeClick = ::onSortTypeClick,
|
||||
onMarketItemClick = { item ->
|
||||
val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id)
|
||||
if (tokenMarket != null) {
|
||||
params.feedClickIntents.onMarketItemClick(
|
||||
token = tokenMarket.toSerializableParam(),
|
||||
appCurrency = currentAppCurrency.value,
|
||||
)
|
||||
}
|
||||
},
|
||||
onMarketOpenClick = { sortBy ->
|
||||
params.feedClickIntents.onMarketOpenClick(sortBy)
|
||||
},
|
||||
onArticleClick = { articleId ->
|
||||
params.feedClickIntents.onArticleClick(articleId)
|
||||
},
|
||||
onOpenAllNews = {
|
||||
params.feedClickIntents.onOpenAllNews()
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order {
|
||||
return when (this) {
|
||||
SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating
|
||||
SortByTypeUM.Trending -> TokenMarketListConfig.Order.Trending
|
||||
SortByTypeUM.ExperiencedBuyers -> TokenMarketListConfig.Order.Buyers
|
||||
SortByTypeUM.TopGainers -> TokenMarketListConfig.Order.TopGainers
|
||||
SortByTypeUM.TopLosers -> TokenMarketListConfig.Order.TopLosers
|
||||
SortByTypeUM.Staking -> TokenMarketListConfig.Order.Staking
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DELAY_TO_FETCH_QUOTES = 60_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.features.feed.model.feed
|
||||
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
|
||||
/**
|
||||
* Callback interface for feed model navigation actions.
|
||||
*/
|
||||
internal interface FeedModelClickIntents {
|
||||
fun onMarketItemClick(token: TokenMarketParams, appCurrency: AppCurrency)
|
||||
fun onMarketOpenClick(sortBy: SortByTypeUM)
|
||||
fun onArticleClick(articleId: Int)
|
||||
fun onOpenAllNews()
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.features.feed.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.arkivanov.decompose.router.stack.ChildStack
|
||||
import com.tangem.core.ui.decompose.ComposableModularContentComponent
|
||||
import com.tangem.features.feed.components.FeedEntryChildFactory
|
||||
|
||||
@Composable
|
||||
internal fun EntryBottomSheetContent(
|
||||
stackState: ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>,
|
||||
onHeaderSizeChange: (Dp) -> Unit,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
|
||||
Scaffold(
|
||||
contentWindowInsets = WindowInsets(0.dp),
|
||||
topBar = {
|
||||
AnimatedContent(
|
||||
targetState = stackState.active.instance,
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
},
|
||||
) { currentState ->
|
||||
currentState.Title()
|
||||
}
|
||||
},
|
||||
content = { contentPadding ->
|
||||
AnimatedContent(
|
||||
stackState.active.instance,
|
||||
) { currentState ->
|
||||
currentState.Content(modifier = Modifier.padding(contentPadding))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
package com.tangem.features.feed.ui.feed
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
|
|
@ -14,7 +11,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -22,25 +21,29 @@ import androidx.compose.ui.draw.drawBehind
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.tangem.common.ui.markets.MarketsListItem
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
import com.tangem.core.ui.components.UnableToLoadData
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.components.block.TangemBlockCardColors
|
||||
import com.tangem.core.ui.components.buttons.SecondarySmallButton
|
||||
import com.tangem.core.ui.components.buttons.SmallButtonConfig
|
||||
import com.tangem.core.ui.components.fields.SearchBar
|
||||
import com.tangem.common.ui.news.ArticleCard
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.fields.TangemSearchBarDefaults
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
|
|
@ -48,19 +51,60 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
|||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListCallbacks
|
||||
import com.tangem.features.feed.ui.feed.state.FeedListUM
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartConfig
|
||||
import com.tangem.features.feed.ui.feed.state.MarketChartUM
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItem
|
||||
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier) {
|
||||
val density = LocalDensity.current
|
||||
internal fun FeedListHeader(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
SearchBar(
|
||||
modifier = modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(horizontal = 16.dp)
|
||||
.padding(bottom = 12.dp),
|
||||
state = searchBarUM,
|
||||
colors = TangemSearchBarDefaults.defaultTextFieldColors.copy(
|
||||
focusedContainerColor = TangemTheme.colors.field.focused,
|
||||
unfocusedContainerColor = TangemTheme.colors.field.focused,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun FeedList(state: FeedListUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
||||
AnimatedContent(
|
||||
modifier = modifier,
|
||||
targetState = state.globalState,
|
||||
) { animatedState ->
|
||||
when (animatedState) {
|
||||
is GlobalFeedState.Loading -> {
|
||||
FeeListLoading(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.drawBehind { drawRect(background) },
|
||||
)
|
||||
}
|
||||
is GlobalFeedState.Error -> {
|
||||
FeedListGlobalError(
|
||||
onRetryClick = animatedState.onRetryClicked,
|
||||
modifier = Modifier.drawBehind { drawRect(background) },
|
||||
)
|
||||
}
|
||||
is GlobalFeedState.Content -> {
|
||||
FeeListContent(
|
||||
modifier = Modifier,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeeListContent(state: FeedListUM, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
Column(
|
||||
modifier = modifier
|
||||
|
|
@ -68,67 +112,61 @@ internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modif
|
|||
.verticalScroll(rememberScrollState())
|
||||
.drawBehind { drawRect(background) },
|
||||
) {
|
||||
SearchBar(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
end = 16.dp,
|
||||
bottom = 8.dp,
|
||||
)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
if (coordinates.size.height > 0) {
|
||||
with(density) {
|
||||
onHeaderSizeChange(coordinates.size.height.toDp())
|
||||
}
|
||||
}
|
||||
.fillMaxSize()
|
||||
.padding(WindowInsets.navigationBars.asPaddingValues()),
|
||||
) {
|
||||
SpacerH(20.dp)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = stringResourceSafe(R.string.feed_market_and_news),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
text = state.currentDate,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
|
||||
AnimatedVisibility(state.marketChartConfig.marketCharts[SortByTypeUM.Rating] != null) {
|
||||
val marketChart = remember(state.marketChartConfig.marketCharts[SortByTypeUM.Rating]) {
|
||||
state.marketChartConfig.marketCharts[SortByTypeUM.Rating]
|
||||
}
|
||||
.padding(bottom = 4.dp),
|
||||
state = state.searchBar,
|
||||
)
|
||||
if (marketChart != null) {
|
||||
MarketBlock(
|
||||
marketChart = marketChart,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SpacerH(20.dp)
|
||||
NewsBlock(
|
||||
news = state.news,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
trendingArticle = state.trendingArticle,
|
||||
)
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = stringResourceSafe(R.string.feed_market_and_news),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
text = state.currentDate,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
|
||||
MarketBlock(
|
||||
marketChartConfig = state.marketChartConfig,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
|
||||
NewsBlock(
|
||||
news = state.news,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
trendingArticle = state.trendingArticle,
|
||||
)
|
||||
|
||||
MarketPulseBlock(
|
||||
marketChartConfig = state.marketChartConfig,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
MarketPulseBlock(
|
||||
marketChartConfig = state.marketChartConfig,
|
||||
feedListCallbacks = state.feedListCallbacks,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
private fun MarketBlock(marketChart: MarketChartUM, feedListCallbacks: FeedListCallbacks) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
|
|
@ -142,20 +180,33 @@ private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks:
|
|||
|
||||
SpacerH(12.dp)
|
||||
|
||||
marketChartConfig.marketCharts[SortByTypeUM.Rating]?.let { chart ->
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = chart,
|
||||
)
|
||||
}
|
||||
Charts(
|
||||
onItemClick = feedListCallbacks.onMarketItemClick,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
marketChart = marketChart,
|
||||
)
|
||||
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
|
||||
val onSeeAllClick by rememberUpdatedState {
|
||||
feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType)
|
||||
}
|
||||
if (marketChartConfig.marketCharts.isNotEmpty()) {
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_pulse_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { onSeeAllClick() },
|
||||
)
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
|
|
@ -175,17 +226,6 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb
|
|||
}
|
||||
}
|
||||
|
||||
Header(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.markets_common_title),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
},
|
||||
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
AnimatedContent(
|
||||
|
|
@ -207,18 +247,41 @@ private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallb
|
|||
|
||||
@Suppress("CanBeNonNullable")
|
||||
@Composable
|
||||
private fun NewsBlock(
|
||||
private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) {
|
||||
AnimatedContent(news) { newsUM ->
|
||||
when (newsUM) {
|
||||
is NewsUM.Content -> {
|
||||
if (newsUM.content.isNotEmpty()) {
|
||||
NewsContentBlock(
|
||||
feedListCallbacks = feedListCallbacks,
|
||||
news = newsUM,
|
||||
trendingArticle = trendingArticle,
|
||||
)
|
||||
}
|
||||
}
|
||||
NewsUM.Loading -> {
|
||||
NewsLoadingBlock()
|
||||
}
|
||||
is NewsUM.Error -> {
|
||||
NewsErrorBlock(onRetryClick = newsUM.onRetryClicked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsContentBlock(
|
||||
feedListCallbacks: FeedListCallbacks,
|
||||
news: ImmutableList<ArticleConfigUM>,
|
||||
news: NewsUM.Content,
|
||||
trendingArticle: ArticleConfigUM?,
|
||||
) {
|
||||
if (news.isNotEmpty()) {
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
|
|
@ -244,13 +307,12 @@ private fun NewsBlock(
|
|||
append(stringResourceSafe(R.string.feed_tangem_ai))
|
||||
}
|
||||
},
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = feedListCallbacks.onOpenAllNews,
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
|
||||
trendingArticle?.let { article ->
|
||||
|
|
@ -260,8 +322,8 @@ private fun NewsBlock(
|
|||
.padding(horizontal = 16.dp),
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
|
||||
SpacerH(12.dp)
|
||||
}
|
||||
|
||||
|
|
@ -272,13 +334,16 @@ private fun NewsBlock(
|
|||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(
|
||||
items = news,
|
||||
items = news.content,
|
||||
key = ArticleConfigUM::id,
|
||||
) { article ->
|
||||
ArticleCard(
|
||||
articleConfigUM = article,
|
||||
onArticleClick = { feedListCallbacks.onArticleClick(article.id) },
|
||||
modifier = Modifier.size(164.dp),
|
||||
modifier = Modifier
|
||||
.height(164.dp)
|
||||
.widthIn(max = 216.dp),
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -292,6 +357,7 @@ private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) {
|
|||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
title()
|
||||
|
||||
|
|
@ -310,12 +376,11 @@ private fun Charts(
|
|||
onItemClick: (MarketsListItemUM) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BlockCard(modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
BlockCard(
|
||||
modifier = modifier,
|
||||
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
when (marketChart) {
|
||||
MarketChartUM.Loading -> {
|
||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
|
|
@ -323,7 +388,10 @@ private fun Charts(
|
|||
}
|
||||
}
|
||||
is MarketChartUM.LoadingError -> {
|
||||
// TODO will be created in [REDACTED_TASK_KEY]
|
||||
UnableToLoadData(
|
||||
onRetryClick = marketChart.onRetryClicked,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
is MarketChartUM.Content -> {
|
||||
marketChart.items.fastForEach { chart ->
|
||||
|
|
@ -370,6 +438,43 @@ private fun FilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick:
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedListGlobalError(onRetryClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.drawBehind { drawRect(background) }
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
UnableToLoadData(onRetryClick = onRetryClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewsErrorBlock(onRetryClick: () -> Unit) {
|
||||
Column {
|
||||
Header(
|
||||
title = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.common_news),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
},
|
||||
onSeeAllClick = {},
|
||||
)
|
||||
SpacerH(12.dp)
|
||||
UnableToLoadData(
|
||||
onRetryClick = onRetryClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
||||
private const val GRADIENT_START = 0f
|
||||
private const val GRADIENT_END = 0.5f
|
||||
|
|
@ -380,9 +485,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED)
|
|||
@Composable
|
||||
private fun FeedListPreview() {
|
||||
TangemThemePreview {
|
||||
FeedList(
|
||||
state = createFeedPreviewState(),
|
||||
onHeaderSizeChange = {},
|
||||
)
|
||||
FeedList(state = createFeedPreviewState())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.features.feed.ui.feed
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
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.unit.dp
|
||||
import com.tangem.common.ui.markets.MarketsListItemPlaceholder
|
||||
import com.tangem.common.ui.news.DefaultLoadingArticle
|
||||
import com.tangem.common.ui.news.TrendingLoadingArticle
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.block.BlockCard
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
||||
@Composable
|
||||
internal fun FeeListLoading(modifier: Modifier = Modifier) {
|
||||
Column(modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(WindowInsets.navigationBars.asPaddingValues()),
|
||||
) {
|
||||
MarketLoadingBlock()
|
||||
NewsLoadingBlock()
|
||||
MarketPulseLoadingBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MarketLoadingBlock() {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.size(width = 104.dp, height = 18.dp),
|
||||
)
|
||||
SpacerH(15.dp)
|
||||
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
SpacerH(35.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun MarketPulseLoadingBlock() {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.size(width = 104.dp, height = 18.dp),
|
||||
)
|
||||
SpacerH(15.dp)
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
RectangleShimmer(modifier = Modifier.size(width = 124.dp, height = 36.dp))
|
||||
}
|
||||
}
|
||||
SpacerH(16.dp)
|
||||
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
SpacerH(32.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun NewsLoadingBlock() {
|
||||
Column {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.size(width = 104.dp, height = 18.dp),
|
||||
)
|
||||
SpacerH(15.dp)
|
||||
TrendingLoadingArticle(modifier = Modifier.padding(horizontal = 16.dp))
|
||||
SpacerH(12.dp)
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
state = rememberLazyListState(),
|
||||
) {
|
||||
items(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
DefaultLoadingArticle()
|
||||
}
|
||||
}
|
||||
SpacerH(35.dp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChartsLoading(modifier: Modifier = Modifier) {
|
||||
BlockCard(modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
repeat(DEFAULT_CHART_SIZE_IN_MARKET) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val DEFAULT_CHART_SIZE_IN_MARKET = 5
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun FeedListLoadingPreview() {
|
||||
TangemThemePreview {
|
||||
Column {
|
||||
MarketLoadingBlock()
|
||||
NewsLoadingBlock()
|
||||
MarketPulseLoadingBlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,16 @@
|
|||
package com.tangem.features.feed.ui.feed.preview
|
||||
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.common.ui.news.ArticleTagUM
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.feed.state.*
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.*
|
||||
|
||||
|
|
@ -38,7 +37,7 @@ internal object FeedListPreviewDataProvider {
|
|||
onMarketItemClick = {},
|
||||
onSortTypeClick = {},
|
||||
),
|
||||
news = articles.filter { it.isTrending.not() }.toImmutableList(),
|
||||
news = NewsUM.Content(articles.filter { it.isTrending.not() }.toImmutableList()),
|
||||
trendingArticle = articles.first { it.isTrending },
|
||||
marketChartConfig = MarketChartConfig(
|
||||
marketCharts = createMarketCharts(marketItems, includeErrorState = false),
|
||||
|
|
@ -94,7 +93,6 @@ internal object FeedListPreviewDataProvider {
|
|||
): MarketChartUM.Content {
|
||||
return MarketChartUM.Content(
|
||||
items = items,
|
||||
triggerScrollReset = consumedEvent(),
|
||||
sortChartConfig = SortChartConfigUM(
|
||||
sortByType = sortByType,
|
||||
isSelected = isSelected,
|
||||
|
|
@ -107,7 +105,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 1,
|
||||
title = "Bitcoin ETF reaches new highs, institutions pile in",
|
||||
score = 0.82f,
|
||||
createdAt = "2h ago",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = true,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -116,7 +114,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 2,
|
||||
title = "Layer 2 networks battle for dominance amid fee wars",
|
||||
score = 0.71f,
|
||||
createdAt = "4h ago",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = true,
|
||||
|
|
@ -125,7 +123,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 3,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -134,7 +132,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 4,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -143,7 +141,7 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 5,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
|
|
@ -152,28 +150,25 @@ internal object FeedListPreviewDataProvider {
|
|||
id = 6,
|
||||
title = "Stablecoins expand on-ramps across LATAM",
|
||||
score = 0.65f,
|
||||
createdAt = "Yesterday",
|
||||
createdAt = TextReference.Str("Yesterday"),
|
||||
isTrending = false,
|
||||
tags = createArticleTags(),
|
||||
isViewed = false,
|
||||
),
|
||||
)
|
||||
|
||||
private fun createArticleTags(): ImmutableSet<ArticleTagUM> {
|
||||
private fun createArticleTags(): ImmutableSet<LabelUM> {
|
||||
return persistentSetOf(
|
||||
ArticleTagUM.Token(
|
||||
title = TextReference.Str("BTC"),
|
||||
iconState = CurrencyIconState.CoinIcon(
|
||||
url = "",
|
||||
fallbackResId = 0,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
LabelUM(
|
||||
text = TextReference.Str("BTC"),
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/euro-coin.png",
|
||||
),
|
||||
),
|
||||
ArticleTagUM.Category(TextReference.Str("Regulation")),
|
||||
ArticleTagUM.Category(TextReference.Str("BTC")),
|
||||
ArticleTagUM.Category(TextReference.Str("Supply")),
|
||||
ArticleTagUM.Category(TextReference.Str("Demand")),
|
||||
LabelUM(TextReference.Str("Regulation")),
|
||||
LabelUM(TextReference.Str("BTC")),
|
||||
LabelUM(TextReference.Str("Supply")),
|
||||
LabelUM(TextReference.Str("Demand")),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
|
|
@ -14,9 +13,10 @@ internal data class FeedListUM(
|
|||
val currentDate: String,
|
||||
val searchBar: SearchBarUM,
|
||||
val feedListCallbacks: FeedListCallbacks,
|
||||
val news: ImmutableList<ArticleConfigUM>,
|
||||
val news: NewsUM,
|
||||
val trendingArticle: ArticleConfigUM?,
|
||||
val marketChartConfig: MarketChartConfig,
|
||||
val globalState: GlobalFeedState = GlobalFeedState.Content,
|
||||
)
|
||||
|
||||
internal data class FeedListCallbacks(
|
||||
|
|
@ -28,6 +28,13 @@ internal data class FeedListCallbacks(
|
|||
val onSortTypeClick: (SortByTypeUM) -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface NewsUM {
|
||||
data object Loading : NewsUM
|
||||
data class Content(val content: ImmutableList<ArticleConfigUM>) : NewsUM
|
||||
data class Error(val onRetryClicked: () -> Unit) : NewsUM
|
||||
}
|
||||
|
||||
internal data class MarketChartConfig(
|
||||
val marketCharts: ImmutableMap<SortByTypeUM, MarketChartUM>,
|
||||
val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers,
|
||||
|
|
@ -40,7 +47,6 @@ internal sealed interface MarketChartUM {
|
|||
|
||||
data class Content(
|
||||
val items: ImmutableList<MarketsListItemUM>,
|
||||
val triggerScrollReset: StateEvent<Unit>,
|
||||
val sortChartConfig: SortChartConfigUM,
|
||||
) : MarketChartUM
|
||||
|
||||
|
|
@ -49,7 +55,14 @@ internal sealed interface MarketChartUM {
|
|||
data class LoadingError(val onRetryClicked: () -> Unit) : MarketChartUM
|
||||
}
|
||||
|
||||
data class SortChartConfigUM(
|
||||
internal data class SortChartConfigUM(
|
||||
val sortByType: SortByTypeUM,
|
||||
val isSelected: Boolean,
|
||||
)
|
||||
)
|
||||
|
||||
@Immutable
|
||||
internal sealed interface GlobalFeedState {
|
||||
data object Loading : GlobalFeedState
|
||||
data object Content : GlobalFeedState
|
||||
data class Error(val onRetryClicked: () -> Unit) : GlobalFeedState
|
||||
}
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.model.converter.MarketsTokenItemConverter
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListUM
|
||||
import com.tangem.features.feed.ui.market.state.SortByTypeUM
|
||||
import com.tangem.pagination.Batch
|
||||
import com.tangem.pagination.BatchAction
|
||||
import com.tangem.pagination.BatchFetchResult
|
||||
import com.tangem.pagination.PaginationStatus
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class FeedMarketsBatchFlowManager(
|
||||
private val getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val managersByOrder = TokenMarketListConfig.Order.entries.associateWith { order ->
|
||||
createManagerForOrder(order)
|
||||
}
|
||||
|
||||
val itemsByOrder: StateFlow<Map<SortByTypeUM, ImmutableList<MarketsListItemUM>>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.uiItems?.map { items -> order to items }
|
||||
},
|
||||
) { itemsList ->
|
||||
itemsList.associate { (order, items) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to items
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
val loadingStatesByOrder: StateFlow<Map<SortByTypeUM, Boolean>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.isLoading?.map { isLoading -> order to isLoading }
|
||||
},
|
||||
) { loadingStatesList ->
|
||||
loadingStatesList.associate { (order, isLoading) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to isLoading
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
val errorStatesByOrder: StateFlow<Map<SortByTypeUM, Boolean>> =
|
||||
combine(
|
||||
TokenMarketListConfig.Order.entries.mapNotNull { order ->
|
||||
managersByOrder[order]?.hasError?.map { hasError -> order to hasError }
|
||||
},
|
||||
) { errorStatesList ->
|
||||
errorStatesList.associate { (order, hasError) ->
|
||||
val sortByType = order.toSortByTypeUM()
|
||||
sortByType to hasError
|
||||
}
|
||||
}.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = emptyMap(),
|
||||
)
|
||||
|
||||
init {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.reload(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createManagerForOrder(order: TokenMarketListConfig.Order): SingleOrderManager {
|
||||
val actionsFlow = MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>()
|
||||
|
||||
val batchFlow = getTopFiveMarketTokenUseCase(
|
||||
batchingContext = TokenListBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = modelScope,
|
||||
),
|
||||
order = order,
|
||||
)
|
||||
|
||||
return SingleOrderManager(
|
||||
order = order,
|
||||
actionsFlow = actionsFlow,
|
||||
batchFlow = batchFlow,
|
||||
currentAppCurrency = currentAppCurrency,
|
||||
modelScope = modelScope,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
||||
fun reloadAll() {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.reload(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuotes() {
|
||||
managersByOrder.values.forEach { manager ->
|
||||
manager.updateQuotes(currentAppCurrency().code)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCharts(order: TokenMarketListConfig.Order) {
|
||||
managersByOrder[order]?.loadCharts()
|
||||
}
|
||||
|
||||
fun getOnLastBatchLoadedSuccessFlow(order: TokenMarketListConfig.Order): Flow<Int>? {
|
||||
return managersByOrder[order]?.onLastBatchLoadedSuccess
|
||||
}
|
||||
|
||||
fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? {
|
||||
return managersByOrder.values
|
||||
.firstNotNullOfOrNull { manager -> manager.getTokenMarketById(tokenId) }
|
||||
}
|
||||
|
||||
private class SingleOrderManager(
|
||||
val order: TokenMarketListConfig.Order,
|
||||
private val actionsFlow: MutableSharedFlow<BatchAction<Int, TokenMarketListConfig, TokenMarketUpdateRequest>>,
|
||||
private val batchFlow: TokenListBatchFlow,
|
||||
private val currentAppCurrency: Provider<AppCurrency>,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val updateStateJob = JobHolder()
|
||||
private val resultBatches = MutableStateFlow(ResultBatches())
|
||||
private val uiBatches = resultBatches.map { it.uiBatches }
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<MarketsListItemUM>> =
|
||||
uiBatches
|
||||
.map { batches ->
|
||||
batches.asSequence()
|
||||
.map { it.data }
|
||||
.flatten()
|
||||
.toImmutableList()
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = persistentListOf(),
|
||||
)
|
||||
|
||||
val isLoading = batchFlow.state
|
||||
.map { state ->
|
||||
when (state.status) {
|
||||
is PaginationStatus.InitialLoading -> true
|
||||
is PaginationStatus.NextBatchLoading -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val hasError = batchFlow.state
|
||||
.map { state ->
|
||||
when (val status = state.status) {
|
||||
is PaginationStatus.InitialLoadingError -> true
|
||||
is PaginationStatus.Paginating -> status.lastResult is BatchFetchResult.Error
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.stateIn(
|
||||
scope = modelScope,
|
||||
started = SharingStarted.Companion.Eagerly,
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val onLastBatchLoadedSuccess = batchFlow.state
|
||||
.distinctUntilChanged { old, new -> old.status == new.status && old.data.size == new.data.size }
|
||||
.mapNotNull { batchListState ->
|
||||
when (val status = batchListState.status) {
|
||||
is PaginationStatus.Paginating -> {
|
||||
if (status.lastResult is BatchFetchResult.Success) {
|
||||
batchListState.data.lastOrNull()?.key
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
is PaginationStatus.EndOfPagination -> {
|
||||
batchListState.data.lastOrNull()?.key
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
batchFlow.state
|
||||
.map { it.data }
|
||||
.distinctUntilChanged { a, b ->
|
||||
a.size == b.size &&
|
||||
a.map { it.key } == b.map { it.key } &&
|
||||
a.map { it.data }.flatten() == b.map { it.data }.flatten()
|
||||
}
|
||||
.onEach {
|
||||
coroutineScope {
|
||||
launch {
|
||||
updateState(it)
|
||||
}.saveIn(updateStateJob)
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private suspend fun updateState(newList: List<Batch<Int, List<TokenMarket>>>, forceUpdate: Boolean = false) =
|
||||
withContext(dispatchers.default) {
|
||||
resultBatches.update { resultBatches ->
|
||||
val items = resultBatches.uiBatches
|
||||
val previousList = resultBatches.processedItems
|
||||
|
||||
val converter = MarketsTokenItemConverter(
|
||||
currentTrendInterval = MarketsListUM.TrendInterval.H24,
|
||||
appCurrency = currentAppCurrency(),
|
||||
)
|
||||
|
||||
if (newList.isEmpty()) {
|
||||
return@update ResultBatches(processedItems = emptyList())
|
||||
}
|
||||
|
||||
val isInitialLoading =
|
||||
forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key
|
||||
|
||||
val outItems = if (isInitialLoading) {
|
||||
newList.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// As nextBatchSize = 0, we only have one batch, but keep the logic for safety
|
||||
if (previousList.size != newList.size) {
|
||||
val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet())
|
||||
val newBatches = newList.filter { keysToAdd.contains(it.key) }
|
||||
|
||||
items + newBatches.map { batch ->
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = converter.convertList(batch.data),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
items.mapIndexed { batchIndex, batch ->
|
||||
val prevBatch = previousList[batchIndex]
|
||||
val newBatch = newList[batchIndex]
|
||||
if (prevBatch == newBatch) return@mapIndexed batch
|
||||
|
||||
Batch(
|
||||
key = batch.key,
|
||||
data = batch.data.mapIndexed { index, marketsListItemUM ->
|
||||
val prevItem = prevBatch.data.getOrNull(index)
|
||||
val newItem = newBatch.data.getOrNull(index)
|
||||
if (prevItem != null && newItem != null) {
|
||||
converter.update(prevItem, marketsListItemUM, newItem)
|
||||
} else {
|
||||
newItem?.let { converter.convert(it) } ?: marketsListItemUM
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentCoroutineContext().ensureActive()
|
||||
|
||||
ResultBatches(
|
||||
uiBatches = outItems,
|
||||
processedItems = newList,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun reload(fiatPriceCurrency: String) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
resultBatches.value = ResultBatches()
|
||||
actionsFlow.emit(
|
||||
BatchAction.Reload(
|
||||
requestParams = TokenMarketListConfig(
|
||||
fiatPriceCurrency = fiatPriceCurrency,
|
||||
searchText = null,
|
||||
priceChangeInterval = TokenMarketListConfig.Interval.H24,
|
||||
order = order,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateQuotes(fiatPriceCurrency: String) {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.CancelUpdates {
|
||||
it.updateRequest is TokenMarketUpdateRequest.UpdateQuotes
|
||||
},
|
||||
)
|
||||
|
||||
actionsFlow.emit(
|
||||
BatchAction.UpdateBatches(
|
||||
keys = batchFlow
|
||||
.state
|
||||
.value
|
||||
.data
|
||||
.map { it.key }
|
||||
.toSet(),
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateQuotes(
|
||||
currencyId = fiatPriceCurrency,
|
||||
),
|
||||
async = true,
|
||||
operationId = "update quotes",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCharts() {
|
||||
modelScope.launch(dispatchers.default) {
|
||||
val currentData = batchFlow.state.value.data
|
||||
val alreadyLoadedChartsBatchKeys = currentData
|
||||
.filter { batch ->
|
||||
val first = batch.data.firstOrNull() ?: return@filter false
|
||||
first.tokenCharts.h24 != null
|
||||
}
|
||||
.map { it.key }
|
||||
.toSet()
|
||||
|
||||
val batchesKeysToLoad = currentData.map { it.key }.toSet().minus(alreadyLoadedChartsBatchKeys)
|
||||
|
||||
if (batchesKeysToLoad.isNotEmpty()) {
|
||||
actionsFlow.emit(
|
||||
BatchAction.UpdateBatches(
|
||||
keys = batchesKeysToLoad,
|
||||
updateRequest = TokenMarketUpdateRequest.UpdateChart(
|
||||
interval = TokenMarketListConfig.Interval.H24,
|
||||
currency = currentAppCurrency().code,
|
||||
),
|
||||
async = true,
|
||||
operationId = batchesKeysToLoad.toString() + "h24",
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getTokenMarketById(tokenId: CryptoCurrency.RawID): TokenMarket? {
|
||||
return resultBatches.value.processedItems
|
||||
?.asSequence()
|
||||
?.flatMap { it.data }
|
||||
?.firstOrNull { it.id == tokenId }
|
||||
}
|
||||
|
||||
private data class ResultBatches(
|
||||
val uiBatches: List<Batch<Int, List<MarketsListItemUM>>> = emptyList(),
|
||||
val processedItems: List<Batch<Int, List<TokenMarket>>>? = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TokenMarketListConfig.Order.toSortByTypeUM(): SortByTypeUM {
|
||||
return when (this) {
|
||||
TokenMarketListConfig.Order.ByRating -> SortByTypeUM.Rating
|
||||
TokenMarketListConfig.Order.Trending -> SortByTypeUM.Trending
|
||||
TokenMarketListConfig.Order.Buyers -> SortByTypeUM.ExperiencedBuyers
|
||||
TokenMarketListConfig.Order.TopGainers -> SortByTypeUM.TopGainers
|
||||
TokenMarketListConfig.Order.TopLosers -> SortByTypeUM.TopLosers
|
||||
TokenMarketListConfig.Order.Staking -> SortByTypeUM.Staking
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
internal class SearchBarStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
private val onStateUpdate: (FeedListUM) -> Unit,
|
||||
) {
|
||||
|
||||
val searchQuery: String
|
||||
get() = currentStateProvider().searchBar.query
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
val currentState = currentStateProvider()
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
searchBar = currentState.searchBar.copy(
|
||||
query = query,
|
||||
isActive = query.isNotEmpty(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import com.tangem.common.ui.news.ArticleConfigUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.WrappedList
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.core.ui.utils.FormattedDate
|
||||
import com.tangem.core.ui.utils.getFormattedDate
|
||||
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.news.ShortArticle
|
||||
import com.tangem.domain.models.news.TrendingNews
|
||||
import com.tangem.features.feed.impl.R
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.StringsSigns
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.collections.immutable.toPersistentSet
|
||||
import org.joda.time.DateTime
|
||||
|
||||
internal class TrendingNewsStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
private val onStateUpdate: (FeedListUM) -> Unit,
|
||||
) {
|
||||
|
||||
fun updateTrendingNewsState(result: TrendingNews, onRetryClicked: () -> Unit) {
|
||||
val currentState = currentStateProvider()
|
||||
when (result) {
|
||||
is TrendingNews.Data -> handleDataState(currentState, result.articles)
|
||||
is TrendingNews.Error -> handleErrorState(currentState, onRetryClicked)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDataState(currentState: FeedListUM, articles: List<ShortArticle>) {
|
||||
val (trendingArticle, commonArticles) = separateTrendingAndCommonArticles(articles)
|
||||
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
trendingArticle = trendingArticle?.let { mapToArticleConfigUM(it, isTrending = true) },
|
||||
news = NewsUM.Content(
|
||||
commonArticles.map { mapToArticleConfigUM(it, isTrending = false) }.toPersistentList(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun handleErrorState(currentState: FeedListUM, onRetryClicked: () -> Unit) {
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
trendingArticle = null,
|
||||
news = NewsUM.Error(onRetryClicked = onRetryClicked),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun separateTrendingAndCommonArticles(
|
||||
articles: List<ShortArticle>,
|
||||
): Pair<ShortArticle?, List<ShortArticle>> {
|
||||
val trendingArticleIndex = articles.indexOfFirst { it.isTrending }
|
||||
return if (trendingArticleIndex != -1) {
|
||||
val trendingArticle = articles[trendingArticleIndex]
|
||||
val commonArticles = articles.toMutableList().apply { removeAt(trendingArticleIndex) }
|
||||
trendingArticle to commonArticles
|
||||
} else {
|
||||
null to articles
|
||||
}
|
||||
}
|
||||
|
||||
private fun mapToArticleConfigUM(article: ShortArticle, isTrending: Boolean): ArticleConfigUM {
|
||||
return ArticleConfigUM(
|
||||
id = article.id,
|
||||
title = article.title,
|
||||
score = article.score,
|
||||
isTrending = isTrending,
|
||||
tags = buildArticleTags(article),
|
||||
createdAt = mapFormattedDate(article.createdAt),
|
||||
isViewed = article.viewed,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildArticleTags(article: ShortArticle): kotlinx.collections.immutable.ImmutableSet<LabelUM> {
|
||||
val categoryLabels = article.categories.map { category ->
|
||||
LabelUM(text = TextReference.Str(category.name))
|
||||
}
|
||||
val tokenLabels = article.relatedTokens.map { token ->
|
||||
LabelUM(
|
||||
text = TextReference.Str(token.symbol),
|
||||
leadingContent = LabelLeadingContentUM.Token(
|
||||
iconUrl = getTokenIconUrlFromDefaultHost(
|
||||
tokenId = CryptoCurrency.RawID(token.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
return (categoryLabels + tokenLabels).toPersistentSet()
|
||||
}
|
||||
|
||||
private fun mapFormattedDate(createdAt: String): TextReference {
|
||||
val formattedDate = getFormattedDate(
|
||||
createdAt = createdAt,
|
||||
now = DateTime.now(),
|
||||
)
|
||||
return when (formattedDate) {
|
||||
is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date)
|
||||
is FormattedDate.HoursAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_hours_ago,
|
||||
count = formattedDate.hours,
|
||||
formatArgs = wrappedList(formattedDate.hours),
|
||||
)
|
||||
is FormattedDate.MinutesAgo -> TextReference.PluralRes(
|
||||
id = R.plurals.news_published_minutes_ago,
|
||||
count = formattedDate.minutes,
|
||||
formatArgs = wrappedList(formattedDate.minutes),
|
||||
)
|
||||
is FormattedDate.Today -> TextReference.Combined(
|
||||
refs = WrappedList(
|
||||
data = listOf(
|
||||
TextReference.Res(R.string.common_today),
|
||||
TextReference.Str(StringsSigns.COMA_SIGN),
|
||||
TextReference.Str(StringsSigns.WHITE_SPACE),
|
||||
TextReference.Str(formattedDate.time),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,322 +0,0 @@
|
|||
package com.tangem.features.feed.ui.market.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import com.tangem.common.ui.charts.MarketChartMini
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.common.ui.tokens.TokenPriceText
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.currency.icon.CoinIcon
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
import com.tangem.features.feed.ui.market.preview.MarketChartListItemPreviewDataProvider
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import com.tangem.utils.StringsSigns.MINUS
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
internal fun MarketsListItem(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
MarketsListItemContent(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RectangleShape)
|
||||
.clickable(onClick = onClick)
|
||||
.testTag(MarketsTestTags.TOKENS_LIST_ITEM),
|
||||
model = model,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MarketsListItemContent(model: MarketsListItemUM, modifier: Modifier = Modifier) {
|
||||
val windowSize = LocalWindowSize.current
|
||||
|
||||
Row(
|
||||
modifier = modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing15,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CoinIcon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size36),
|
||||
url = model.iconUrl,
|
||||
alpha = 1f,
|
||||
colorFilter = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
)
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
TokenTitle(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
name = model.name,
|
||||
currencySymbol = model.currencySymbol,
|
||||
)
|
||||
SpacerW8()
|
||||
TokenPriceText(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
price = model.price.text,
|
||||
priceChangeType = model.price.changeType,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
) {
|
||||
TokenSubtitle(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
ratingPosition = model.ratingPosition,
|
||||
marketCap = model.marketCap,
|
||||
stakingRate = model.stakingRate,
|
||||
)
|
||||
PriceChangeInPercent(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
textStyle = TangemTheme.typography.caption2,
|
||||
type = model.trendType,
|
||||
valueInPercent = model.trendPercentText,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
|
||||
|
||||
Chart(
|
||||
chartType = model.chartType,
|
||||
chartRawData = model.chartData,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier = Modifier) {
|
||||
Row(modifier = modifier) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.alignByBaseline(),
|
||||
text = name,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
SpacerW4()
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
text = currencySymbol,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Visible,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokenSubtitle(
|
||||
ratingPosition: String?,
|
||||
marketCap: String?,
|
||||
stakingRate: TextReference?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TokenRatingPlace(ratingPosition = ratingPosition)
|
||||
if (marketCap != null) {
|
||||
SpacerW4()
|
||||
TokenMarketCapText(
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
text = marketCap,
|
||||
)
|
||||
}
|
||||
if (stakingRate != null) {
|
||||
SpacerW4()
|
||||
StakingRate(stakingRate = stakingRate.resolveReference())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenRatingPlace(ratingPosition: String?) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens.size16)
|
||||
.background(
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing5),
|
||||
) {
|
||||
Text(
|
||||
text = ratingPosition ?: MINUS,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.StakingRate(stakingRate: String) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.heightIn(min = TangemTheme.dimens.size16)
|
||||
.border(
|
||||
width = TangemTheme.dimens.size1,
|
||||
color = TangemTheme.colors.field.primary,
|
||||
shape = TangemTheme.shapes.roundedCornersSmall2,
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing5),
|
||||
) {
|
||||
Text(
|
||||
text = stakingRate,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption1,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.TokenMarketCapText(text: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier.alignByBaseline(),
|
||||
text = text,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) {
|
||||
val chartWidth = TangemTheme.dimens.size56
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing2)
|
||||
.size(height = TangemTheme.dimens.size24, width = chartWidth),
|
||||
) {
|
||||
if (chartRawData != null) {
|
||||
MarketChartMini(
|
||||
rawData = chartRawData,
|
||||
type = chartType,
|
||||
)
|
||||
} else {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size12)
|
||||
.align(Alignment.Center),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 260, name = "small width")
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM) {
|
||||
TangemThemePreview {
|
||||
var state1 by remember { mutableStateOf(state) }
|
||||
var state2 by remember { mutableStateOf(state) }
|
||||
var prices by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
100 to PriceChangeType.NEUTRAL,
|
||||
200 to PriceChangeType.NEUTRAL,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) {
|
||||
MarketsListItem(
|
||||
modifier = Modifier,
|
||||
model = state1,
|
||||
)
|
||||
MarketsListItem(
|
||||
modifier = Modifier,
|
||||
model = state2,
|
||||
)
|
||||
Row {
|
||||
Button(
|
||||
onClick = {
|
||||
state1 = state1.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
trendType = PriceChangeType.entries.random(),
|
||||
)
|
||||
},
|
||||
) { Text(text = "trend") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
prices = prices.map { (price, _) ->
|
||||
if (Random.nextBoolean()) {
|
||||
price.inc() to PriceChangeType.UP
|
||||
} else {
|
||||
price.dec() to PriceChangeType.DOWN
|
||||
}
|
||||
}
|
||||
state1 = state1.copy(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[0].first}023 $",
|
||||
changeType = prices[0].second,
|
||||
),
|
||||
)
|
||||
state2 = state2.copy(
|
||||
price = MarketsListItemUM.Price(
|
||||
text = "0.${prices[1].first}023 $",
|
||||
changeType = prices[1].second,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { Text(text = "price") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
package com.tangem.features.feed.ui.market.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.res.LocalWindowSize
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MarketsListItemPlaceholder() {
|
||||
val density = LocalDensity.current
|
||||
val windowSize = LocalWindowSize.current
|
||||
val sp12 = with(density) { 12.sp.toDp() }
|
||||
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing15,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CircleShimmer(Modifier.size(TangemTheme.dimens.size36))
|
||||
|
||||
SpacerW12()
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing4),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size70)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing2)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = TangemTheme.dimens.spacing2),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.width(TangemTheme.dimens.size52)
|
||||
.height(sp12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (windowSize.widthAtLeast(WindowSizeType.Small)) {
|
||||
Spacer(Modifier.width(TangemTheme.dimens.spacing10))
|
||||
|
||||
Box {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.width(TangemTheme.dimens.size56)
|
||||
.height(TangemTheme.dimens.size12),
|
||||
radius = TangemTheme.dimens.radius3,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal")
|
||||
@Preview(showBackground = true, widthDp = 360, name = "normal night", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(showBackground = true, widthDp = 320, name = "small width")
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
Column(Modifier.background(TangemTheme.colors.background.tertiary)) {
|
||||
repeat(20) {
|
||||
MarketsListItemPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
package com.tangem.features.feed.ui.market.preview
|
||||
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.feed.ui.market.state.MarketsListItemUM
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
internal class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider<MarketsListItemUM>(
|
||||
collection = listOf(
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = "",
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.NEUTRAL,
|
||||
chartData = null,
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = "$6.23348172384781234 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.DOWN,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = "10",
|
||||
marketCap = null,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = "$6.233 B",
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
MarketsListItemUM(
|
||||
id = CryptoCurrency.RawID("1"),
|
||||
name = "Bitcoin",
|
||||
currencySymbol = "BTC",
|
||||
iconUrl = null,
|
||||
ratingPosition = null,
|
||||
marketCap = null,
|
||||
price = MarketsListItemUM.Price(text = "31 285.72$"),
|
||||
trendPercentText = "12.43%",
|
||||
trendType = PriceChangeType.UP,
|
||||
chartData = MarketChartRawData(
|
||||
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||
),
|
||||
isUnder100kMarketCap = false,
|
||||
stakingRate = stringReference("APY 12.34%"),
|
||||
updateTimestamp = 0,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package com.tangem.features.feed.ui.market.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
||||
@Immutable
|
||||
data class MarketsListItemUM(
|
||||
val id: CryptoCurrency.RawID,
|
||||
val name: String,
|
||||
val currencySymbol: String,
|
||||
val iconUrl: String?,
|
||||
val ratingPosition: String?,
|
||||
val marketCap: String?,
|
||||
val price: Price,
|
||||
val trendPercentText: String,
|
||||
val trendType: PriceChangeType,
|
||||
val chartData: MarketChartRawData?,
|
||||
val isUnder100kMarketCap: Boolean,
|
||||
val stakingRate: TextReference?,
|
||||
val updateTimestamp: Long?,
|
||||
) {
|
||||
val chartType: MarketChartLook.Type = when (trendType) {
|
||||
PriceChangeType.UP -> MarketChartLook.Type.Growing
|
||||
PriceChangeType.DOWN -> MarketChartLook.Type.Falling
|
||||
PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class Price(
|
||||
val text: String,
|
||||
val changeType: PriceChangeType? = null,
|
||||
)
|
||||
|
||||
@Suppress("NullableToStringCall")
|
||||
fun getComposeKey(): String {
|
||||
return id.value + TOKEN_LAZY_LIST_ID_SEPARATOR + marketCap.toString() + updateTimestamp
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TOKEN_LAZY_LIST_ID_SEPARATOR = "@"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.features.feed.ui.market.state
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.fields.entity.SearchBarUM
|
||||
|
|
|
|||
|
|
@ -6,9 +6,6 @@
|
|||
<ID>BooleanPropertyNaming:HomeUM.kt$HomeUM$val scanInProgress: Boolean</ID>
|
||||
<ID>MultilineLambdaItParameter:HomeModel.kt$HomeModel${ 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) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:StoriesProgressBar.kt${ when (index) { currentStep -> it.fillMaxWidth(progress.value) in 0 until currentStep -> it.fillMaxWidth(fraction = 1f) else -> it } }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isFirstStepLaunched = remember { mutableStateOf(false) }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isLaunched = remember { mutableStateOf(false) }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isSecondStepLaunched = remember { mutableStateOf(false) }</ID>
|
||||
<ID>ReusedModifierInstance:HomeButtonsV2.kt$StoriesButton( modifier = modifier, text = stringResourceSafe(id = R.string.common_get_started), useDarkerColors = false, onClick = onGetStartedClick, )</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import com.tangem.common.routing.AppRouter
|
|||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
|
||||
import com.tangem.core.analytics.models.Basic.SignedInLegacy
|
||||
import com.tangem.core.analytics.models.Basic.SignedInLegacy.SignInType
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -93,7 +93,7 @@ internal class HomeModel @Inject constructor(
|
|||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(IntroductionProcess.ScreenOpened)
|
||||
analyticsEventHandler.send(IntroductionProcess.ScreenOpened())
|
||||
observeUserCountryChanges()
|
||||
|
||||
when (params.launchMode) {
|
||||
|
|
@ -127,20 +127,20 @@ internal class HomeModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCardLegacy())
|
||||
scanCard()
|
||||
}
|
||||
|
||||
private fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
|
||||
analyticsEventHandler.send(Shop.ScreenOpened())
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchTokensClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList)
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
|
||||
router.push(AppRoute.ManageTokens(Source.STORIES))
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ internal class HomeModel @Inject constructor(
|
|||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
SignedIn(
|
||||
SignedInLegacy(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
|
|
|
|||
|
|
@ -4,5 +4,9 @@ import com.tangem.core.decompose.factory.ComponentFactory
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface CreateMobileWalletComponent : ComposableContentComponent {
|
||||
interface Factory : ComponentFactory<Unit, CreateMobileWalletComponent>
|
||||
data class Params(
|
||||
val source: String,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, CreateMobileWalletComponent>
|
||||
}
|
||||
|
|
@ -11,17 +11,13 @@
|
|||
<ID>BooleanPropertyNaming:ForgetWalletUM.kt$ForgetWalletUM$val secondCheckboxChecked: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:HotAccessCodeRequestUM.kt$HotAccessCodeRequestUM$val useBiometricVisible: Boolean = true</ID>
|
||||
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showBackButton: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showFeedbackButton: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showSkipButton: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonEnabled: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonProgress: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM.WordField$val error: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:MobileWalletSetupFinishedContent.kt$var showConfetti by remember { mutableStateOf(false) }</ID>
|
||||
<ID>BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId && it.card.wallets.map { it.curve }.toSet().isNotEmpty() }</ID>
|
||||
<ID>BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not() }</ID>
|
||||
<ID>BooleanPropertyNaming:WalletBackupUM.kt$WalletBackupUM$val backedUp: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:WalletHardwareBackupUM.kt$WalletHardwareBackupUM$val showPurchaseBlock: Boolean = false</ID>
|
||||
<ID>MaxChainedCallsOnSameLine:UpgradeWalletModel.kt$UpgradeWalletModel$it.card.wallets.map { it.curve }.toSet().isNotEmpty()</ID>
|
||||
<ID>MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ Timber.e(it) setImportProgress(false) }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ 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)), ) } } }</ID>
|
||||
<ID>MultilineLambdaItParameter:CreateHardwareWalletModel.kt$CreateHardwareWalletModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { router.replaceAll(AppRoute.Wallet) } } } }</ID>
|
||||
|
|
@ -48,14 +44,12 @@
|
|||
<ID>MultilineLambdaItParameter:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.filterIndexed { index, _ -> WORD_FIELD_INDICES.contains(index + 1) }.toImmutableList(), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManualBackupPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManualBackupPhraseModel.kt$ManualBackupPhraseModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s -> EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:UpgradeWalletModel.kt$UpgradeWalletModel${ // Check if user attempted to upgrade before but something went wrong and a full reset is required val userWallet = coldUserWalletBuilderFactory.create(it).build() val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not() } val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId && it.card.wallets.map { it.curve }.toSet().isNotEmpty() } if (userWallet != null && (sameWalletButNotFinishedBackup || otherWalletAndAlreadyCreated)) { startResetCardsFlow.emit(userWallet) return@doOnSuccess } delay(DELAY_SDK_DIALOG_CLOSE) tangemSdkManager.changeDisplayedCardIdNumbersCount(it) navigateToUpgradeFlow(it) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ViewPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ViewPhraseModel.kt$ViewPhraseModel${ it.copy( words = words.mapIndexed { index, s -> EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) }</ID>
|
||||
<ID>NoNameShadowing:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy(completeButtonProgress = false) }</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$uiState</ID>
|
||||
<ID>ReusedModifierInstance:AddExistingWalletImportContent.kt$OutlineTextFieldWithIcon( modifier = modifier .padding(horizontal = 16.dp) .fillMaxWidth(), value = state.passPhrase, onValueChange = state.passPhraseChange, iconResId = R.drawable.ic_information_24, iconColor = TangemTheme.colors.icon.informative, label = stringResourceSafe(id = R.string.common_passphrase), placeholder = stringResourceSafe(id = R.string.send_optional_field), onIconClick = state.onPassphraseInfoClick, keyboardOptions = KeyboardOptions( autoCorrectEnabled = false, keyboardType = KeyboardType.Password, ), )</ID>
|
||||
<ID>ReusedModifierInstance:HotAccessCodeRequestFullScreenContent.kt$AnimatedVisibility( modifier = modifier, visible = state.isShown, enter = fadeIn(), exit = fadeOut(), ) { Column( Modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), horizontalAlignment = Alignment.CenterHorizontally, ) { TangemTopAppBar( modifier = Modifier.statusBarsPadding(), startButton = TopAppBarButtonUM.Back(state.onDismiss), ) SpacerH(68.dp) Column( Modifier .weight(1f) .fillMaxWidth() .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), text = stringResourceSafe(R.string.access_code_check_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) SpacerH24() PinTextField( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), length = 6, isPasswordVisual = true, value = state.accessCode, 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, ) } } AnimatedVisibility( visible = state.useBiometricVisible, enter = fadeIn(), exit = fadeOut(), ) { SecondaryButton( modifier = Modifier .padding(16.dp) .fillMaxWidth() .navigationBarsPadding() .imePadding(), text = stringResourceSafe( id = R.string.welcome_unlock, stringResourceSafe(R.string.common_biometrics), ), onClick = state.useBiometricClick, ) } } }</ID>
|
||||
<ID>ReusedModifierInstance:HotWalletStepper.kt$TangemTopAppBar( startButton = if (state.showBackButton) { TopAppBarButtonUM.Back(onBackClick) } else { null }, endButton = when { state.showSkipButton -> TopAppBarButtonUM.Text( text = resourceReference(R.string.common_skip), onClicked = onSkipClick, ) state.showFeedbackButton -> TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_chat_24, onClicked = onFeedbackClick, ) else -> null }, title = state.title, containerColor = TangemTheme.colors.background.primary, modifier = modifier, titleAlignment = Alignment.CenterHorizontally, )</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:ManualBackupCheckModel.kt$ManualBackupCheckModel$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:ManualBackupPhraseModel.kt$ManualBackupPhraseModel$runCatching</ID>
|
||||
|
|
|
|||
|
|
@ -2,12 +2,16 @@ package com.tangem.features.hotwallet.accesscode
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.arkivanov.essenty.lifecycle.doOnResume
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
|
||||
import com.tangem.features.hotwallet.accesscode.ui.AccessCode
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
@ -31,10 +35,18 @@ internal class AccessCodeComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
EventEffect(event = state.requestFocus) {
|
||||
focusManager.clearFocus()
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
|
||||
DisableScreenshotsDisposableEffect()
|
||||
AccessCode(
|
||||
modifier = modifier,
|
||||
focusRequester = focusRequester,
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,13 @@ 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.components.fields.PinTextColor
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
|
|
@ -28,7 +31,6 @@ import com.tangem.hot.sdk.model.HotAuth
|
|||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -57,6 +59,7 @@ internal class AccessCodeModel @Inject constructor(
|
|||
private val shouldShowAskBiometryUseCase: ShouldShowAskBiometryUseCase,
|
||||
private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase,
|
||||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
||||
private val isAccessCodeSimpleUseCase: IsAccessCodeSimpleUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -118,16 +121,56 @@ internal class AccessCodeModel @Inject constructor(
|
|||
modelScope.launch {
|
||||
delay(timeMillis = SUCCESS_DISPLAY_DURATION_MS)
|
||||
|
||||
params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode)
|
||||
|
||||
uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
accessCode = "",
|
||||
)
|
||||
if (isAccessCodeSimpleUseCase(uiState.value.accessCode)) {
|
||||
showSimpleAccessCodeDialog()
|
||||
} else {
|
||||
setNewCode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setNewCode() {
|
||||
params.callbacks.onNewAccessCodeInput(params.userWalletId, uiState.value.accessCode)
|
||||
|
||||
uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
accessCode = "",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSimpleAccessCodeDialog() {
|
||||
uiMessageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.access_code_alert_validation_title),
|
||||
message = resourceReference(R.string.access_code_alert_validation_description),
|
||||
firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.access_code_alert_validation_cancel),
|
||||
onClick = {
|
||||
uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
accessCode = "",
|
||||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
requestFocus = triggeredEvent(Unit, ::consumeRequestFocusEvent),
|
||||
)
|
||||
}
|
||||
},
|
||||
),
|
||||
secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.access_code_alert_validation_ok),
|
||||
onClick = ::setNewCode,
|
||||
),
|
||||
isDismissable = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun consumeRequestFocusEvent() {
|
||||
uiState.update { currentState ->
|
||||
currentState.copy(requestFocus = consumedEvent())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun showErrorAndReset() {
|
||||
uiState.update { currentState ->
|
||||
currentState.copy(
|
||||
|
|
@ -152,11 +195,6 @@ internal class AccessCodeModel @Inject constructor(
|
|||
|
||||
tryToAskForBiometry()
|
||||
|
||||
userWalletsListRepository.saveWithoutLock(
|
||||
userWallet.copy(backedUp = true),
|
||||
canOverride = true,
|
||||
)
|
||||
|
||||
userWalletsListRepository.setLock(
|
||||
userWallet.walletId,
|
||||
UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()),
|
||||
|
|
@ -179,30 +217,25 @@ internal class AccessCodeModel @Inject constructor(
|
|||
hotWalletAccessor.unlockContextual(userWallet.hotWalletId)
|
||||
}
|
||||
|
||||
launch(NonCancellable) {
|
||||
var updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
var updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
auth = HotAuth.Password(accessCode.toCharArray()),
|
||||
)
|
||||
|
||||
if (walletsRepository.requireAccessCode().not()) {
|
||||
updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
auth = HotAuth.Password(accessCode.toCharArray()),
|
||||
auth = HotAuth.Biometry,
|
||||
)
|
||||
|
||||
if (walletsRepository.requireAccessCode().not()) {
|
||||
updatedHotWalletId = tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = unlockHotWallet,
|
||||
auth = HotAuth.Biometry,
|
||||
)
|
||||
}
|
||||
|
||||
userWalletsListRepository.saveWithoutLock(
|
||||
userWallet.copy(
|
||||
hotWalletId = updatedHotWalletId,
|
||||
backedUp = true,
|
||||
),
|
||||
canOverride = true,
|
||||
)
|
||||
|
||||
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
|
||||
}
|
||||
|
||||
userWalletsListRepository.saveWithoutLock(
|
||||
userWallet.copy(hotWalletId = updatedHotWalletId),
|
||||
canOverride = true,
|
||||
)
|
||||
|
||||
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
|
||||
|
||||
params.callbacks.onAccessCodeUpdated(params.userWalletId)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.features.hotwallet.accesscode.entity
|
||||
|
||||
import com.tangem.core.ui.components.fields.PinTextColor
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
|
||||
|
||||
internal data class AccessCodeUM(
|
||||
|
|
@ -8,6 +10,7 @@ internal data class AccessCodeUM(
|
|||
val accessCodeColor: PinTextColor,
|
||||
val onAccessCodeChange: (String) -> Unit,
|
||||
val isConfirmMode: Boolean,
|
||||
val requestFocus: StateEvent<Unit> = consumedEvent(),
|
||||
) {
|
||||
val accessCodeLength: Int = ACCESS_CODE_LENGTH
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import androidx.compose.foundation.background
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -20,7 +22,11 @@ import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
|
|||
|
||||
@Suppress("LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
|
||||
internal fun AccessCode(
|
||||
state: AccessCodeUM,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
|
|
@ -77,6 +83,7 @@ internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
|
|||
value = state.accessCode,
|
||||
pinTextColor = state.accessCodeColor,
|
||||
onValueChange = state.onAccessCodeChange,
|
||||
focusRequester = focusRequester,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.hotwallet.accesscoderequest
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.SignIn
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.ui.components.fields.PinTextColor
|
||||
|
|
@ -33,6 +35,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val result = MutableStateFlow<HotWalletPasswordRequester.Result?>(null)
|
||||
|
|
@ -108,6 +111,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
accessCode = "",
|
||||
useBiometricClick = {
|
||||
analyticsEventHandler.send(SignIn.ButtonBiometricSignIn())
|
||||
dismissState()
|
||||
result.value = HotWalletPasswordRequester.Result.UseBiometry
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
|||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -110,7 +111,7 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
title = resourceReference(R.string.access_code_alert_skip_ok),
|
||||
onClick = {
|
||||
if (userWalletId != null) {
|
||||
modelScope.launch {
|
||||
modelScope.launch(NonCancellable) {
|
||||
setAccessCodeSkippedUseCase(userWalletId, true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.wallet_import_seed_navtitle),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -24,7 +23,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.wallet_import_title),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -33,7 +31,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = false,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -42,7 +39,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = true,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.PushNotifications -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -51,7 +47,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.onboarding_title_notifications),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.SetupFinished -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -60,7 +55,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.common_done),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.im.port.model
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -37,6 +40,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
|
||||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: AddExistingWalletImportComponent.Params = paramsContainer.require()
|
||||
|
|
@ -60,6 +64,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
}
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ImportSeedPhraseScreenOpened())
|
||||
importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder(
|
||||
modelScope = modelScope,
|
||||
mnemonicRepository = mnemonicRepository,
|
||||
|
|
@ -72,6 +77,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
)
|
||||
},
|
||||
onPassphraseInfoClick = ::onPassphraseInfoClick,
|
||||
onImportClick = { analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImport()) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +107,23 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
}
|
||||
.onRight {
|
||||
setImportProgress(false)
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.Finished(
|
||||
source = AnalyticsParam.ScreensSources.ImportWallet.value,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.CreateWallet.WalletCreatedSuccessfully(
|
||||
source = AnalyticsParam.ScreensSources.ImportWallet.value,
|
||||
creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.SeedImport,
|
||||
seedPhraseLength = mnemonic.mnemonicComponents.size,
|
||||
passPhraseState = if (passphrase.isNullOrBlank()) {
|
||||
AnalyticsParam.EmptyFull.Empty
|
||||
} else {
|
||||
AnalyticsParam.EmptyFull.Full
|
||||
},
|
||||
),
|
||||
)
|
||||
params.callbacks.onWalletImported(userWallet.walletId)
|
||||
}
|
||||
}.onFailure {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class ImportSeedPhraseUiStateBuilder(
|
||||
private val modelScope: CoroutineScope,
|
||||
private val mnemonicRepository: MnemonicRepository,
|
||||
|
|
@ -24,6 +25,7 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit,
|
||||
private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit,
|
||||
private val onPassphraseInfoClick: () -> Unit,
|
||||
private val onImportClick: () -> Unit,
|
||||
) {
|
||||
private val wordsCheckJobHolder = JobHolder()
|
||||
private var importedMnemonic: Mnemonic? = null
|
||||
|
|
@ -57,6 +59,7 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
}
|
||||
|
||||
private fun onCreateWallet() {
|
||||
onImportClick()
|
||||
val mnemonic = importedMnemonic ?: return
|
||||
val passphrase = passphrase?.takeIf { it.isNotEmpty() }
|
||||
importWallet(mnemonic, passphrase)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.features.hotwallet.common.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -18,6 +20,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
|
||||
private const val DISABLED_COLORS_ALPHA = 0.5f
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
internal fun OptionBlock(
|
||||
|
|
@ -43,11 +46,11 @@ internal fun OptionBlock(
|
|||
}
|
||||
.padding(16.dp),
|
||||
) {
|
||||
Row {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.padding(end = 4.dp),
|
||||
text = title,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import com.tangem.common.core.TangemSdkError
|
|||
import com.tangem.common.routing.AppRoute
|
||||
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.analytics.models.Basic
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
|
|
@ -16,8 +16,7 @@ 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.ParamCardCurrencyConverter
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.error.SaveWalletError
|
||||
|
|
@ -53,6 +52,7 @@ internal class CreateHardwareWalletModel @Inject constructor(
|
|||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -66,20 +66,26 @@ internal class CreateHardwareWalletModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.CreateWalletScreenOpened)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.CreateWalletScreenOpened())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
trackingContextProxy.removeContext()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun onBuyTangemWalletClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWallet))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScanDeviceClick() {
|
||||
analyticsEventHandler.send(
|
||||
event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateWallet),
|
||||
)
|
||||
scanCard()
|
||||
}
|
||||
|
||||
|
|
@ -143,28 +149,11 @@ internal class CreateHardwareWalletModel @Inject constructor(
|
|||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported)
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
|
||||
isImported = isImported,
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
uiState.update { it.copy(isScanInProgress = isLoading) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.createhardwarewallet.ui
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -42,6 +44,7 @@ internal fun CreateHardwareWalletContent(state: CreateHardwareWalletUM, modifier
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
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.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
|
||||
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.SyncWalletWithRemoteUseCase
|
||||
import com.tangem.features.hotwallet.CreateMobileWalletComponent
|
||||
import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
|
|
@ -25,6 +33,7 @@ import javax.inject.Inject
|
|||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class CreateMobileWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
|
||||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
|
|
@ -32,8 +41,13 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val isHotWalletCreationSupported: IsHotWalletCreationSupported,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: CreateMobileWalletComponent.Params = paramsContainer.require()
|
||||
|
||||
internal val uiState: StateFlow<CreateMobileWalletUM>
|
||||
field = MutableStateFlow(
|
||||
CreateMobileWalletUM(
|
||||
|
|
@ -46,6 +60,12 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
|
||||
init {
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.Started(source = params.source),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.SeedPhrase.CreateMobileScreenOpened(source = params.source),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -54,10 +74,15 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onImportClick() {
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImportWallet())
|
||||
checkHotWalletCreationSupported(notSupported = { return })
|
||||
router.push(AppRoute.AddExistingWallet)
|
||||
}
|
||||
|
||||
private fun onCreateClick() {
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.CreateWallet.ButtonCreateWallet())
|
||||
checkHotWalletCreationSupported(notSupported = { return })
|
||||
|
||||
modelScope.launch {
|
||||
uiState.update {
|
||||
it.copy(createButtonLoading = true)
|
||||
|
|
@ -70,9 +95,20 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
|
||||
saveUserWalletUseCase(userWallet)
|
||||
|
||||
launch(NonCancellable) {
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Finished(source = params.source))
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.CreateWallet.WalletCreatedSuccessfully(
|
||||
source = params.source,
|
||||
creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.NewSeed,
|
||||
seedPhraseLength = SEED_PHRASE_LENGTH,
|
||||
passPhraseState = AnalyticsParam.EmptyFull.Empty,
|
||||
),
|
||||
)
|
||||
|
||||
launch(dispatchers.main + NonCancellable) {
|
||||
syncWalletWithRemoteUseCase(userWalletId = userWallet.walletId)
|
||||
}
|
||||
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
}.onFailure { throwable ->
|
||||
Timber.e(throwable)
|
||||
|
|
@ -81,4 +117,17 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun checkHotWalletCreationSupported(notSupported: () -> Unit) {
|
||||
if (!isHotWalletCreationSupported()) {
|
||||
uiMessageSender.send(
|
||||
hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()),
|
||||
)
|
||||
notSupported()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEED_PHRASE_LENGTH = 12
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import dagger.assisted.AssistedInject
|
|||
@Suppress("UnusedPrivateMember")
|
||||
internal class DefaultCreateMobileWalletComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Unit,
|
||||
@Assisted private val params: CreateMobileWalletComponent.Params,
|
||||
) : CreateMobileWalletComponent, AppComponentContext by context {
|
||||
|
||||
private val model: CreateMobileWalletModel = getOrCreateModel(params)
|
||||
|
|
@ -31,6 +31,9 @@ internal class DefaultCreateMobileWalletComponent @AssistedInject constructor(
|
|||
|
||||
@AssistedFactory
|
||||
interface Factory : CreateMobileWalletComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Unit): DefaultCreateMobileWalletComponent
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: CreateMobileWalletComponent.Params,
|
||||
): DefaultCreateMobileWalletComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.createmobilewallet.ui
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -41,6 +43,7 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.manualbackup.start.ui
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -23,7 +25,7 @@ import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStart
|
|||
@Composable
|
||||
internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
|
|
@ -33,51 +35,58 @@ internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modi
|
|||
bottom = 16.dp,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp,
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp,
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_info_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp,
|
||||
),
|
||||
text = stringResourceSafe(
|
||||
R.string.backup_info_description,
|
||||
state.seepPhraseLength.toString(),
|
||||
),
|
||||
text = stringResourceSafe(R.string.backup_info_title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(
|
||||
horizontal = 16.dp,
|
||||
vertical = 8.dp,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
FeatureBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.backup_info_save_title),
|
||||
description = stringResourceSafe(
|
||||
R.string.backup_info_save_description,
|
||||
state.seepPhraseLength.toString(),
|
||||
),
|
||||
text = stringResourceSafe(
|
||||
R.string.backup_info_description,
|
||||
state.seepPhraseLength.toString(),
|
||||
),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
FeatureBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.backup_info_save_title),
|
||||
description = stringResourceSafe(
|
||||
R.string.backup_info_save_description,
|
||||
state.seepPhraseLength.toString(),
|
||||
),
|
||||
iconRes = R.drawable.ic_lock_24,
|
||||
)
|
||||
FeatureBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.backup_info_keep_title),
|
||||
description = stringResourceSafe(R.string.backup_info_keep_description),
|
||||
iconRes = R.drawable.ic_settings_24,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
iconRes = R.drawable.ic_lock_24,
|
||||
)
|
||||
FeatureBlock(
|
||||
modifier = Modifier
|
||||
.padding(top = 24.dp),
|
||||
title = stringResourceSafe(R.string.backup_info_keep_title),
|
||||
description = stringResourceSafe(R.string.backup_info_keep_description),
|
||||
iconRes = R.drawable.ic_settings_24,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ interface HotWalletStepperComponent : ComposableContentComponent {
|
|||
val title: TextReference,
|
||||
val showBackButton: Boolean,
|
||||
val showSkipButton: Boolean,
|
||||
val showFeedbackButton: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
fun initialState() = StepperUM(
|
||||
|
|
@ -23,7 +22,6 @@ interface HotWalletStepperComponent : ComposableContentComponent {
|
|||
title = TextReference.EMPTY,
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ internal class DefaultHotWalletStepperComponent @AssistedInject constructor(
|
|||
modifier = modifier,
|
||||
onBackClick = model::onBackClick,
|
||||
onSkipClick = model::onSkipClick,
|
||||
onFeedbackClick = model::onFeedbackClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,9 +34,4 @@ internal class HotWalletStepperModel @Inject constructor(
|
|||
// TODO send analytics
|
||||
params.callback.onSkipClick()
|
||||
}
|
||||
|
||||
fun onFeedbackClick() {
|
||||
// TODO send analytics
|
||||
// openFeedback()
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,6 @@ internal fun HotWalletStepper(
|
|||
state: HotWalletStepperComponent.StepperUM,
|
||||
onBackClick: () -> Unit,
|
||||
onSkipClick: () -> Unit,
|
||||
onFeedbackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1)
|
||||
|
|
@ -47,20 +46,16 @@ internal fun HotWalletStepper(
|
|||
} else {
|
||||
null
|
||||
},
|
||||
endButton = when {
|
||||
state.showSkipButton -> TopAppBarButtonUM.Text(
|
||||
endButton = if (state.showSkipButton) {
|
||||
TopAppBarButtonUM.Text(
|
||||
text = resourceReference(R.string.common_skip),
|
||||
onClicked = onSkipClick,
|
||||
)
|
||||
state.showFeedbackButton -> TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_chat_24,
|
||||
onClicked = onFeedbackClick,
|
||||
)
|
||||
else -> null
|
||||
} else {
|
||||
null
|
||||
},
|
||||
title = state.title,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
modifier = modifier,
|
||||
titleAlignment = Alignment.CenterHorizontally,
|
||||
)
|
||||
|
||||
|
|
@ -95,11 +90,9 @@ private fun HotWalletStepper_Preview() {
|
|||
title = resourceReference(R.string.common_done),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
),
|
||||
onBackClick = {},
|
||||
onSkipClick = {},
|
||||
onFeedbackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ internal class DefaultUpgradeWalletComponent @AssistedInject constructor(
|
|||
private val resetCardsComponent = resetCardsComponentFactory.create(
|
||||
context = child("ResetCardsComponent"),
|
||||
params = ResetCardsComponent.Params(
|
||||
source = ResetCardsComponent.Params.Source.Upgrade,
|
||||
callbacks = model.resetCardsComponentCallbacks,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import com.tangem.common.doOnResult
|
|||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -18,7 +21,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.toWrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.card.BackupValidator
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
|
|
@ -60,6 +63,7 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
private val params = paramsContainer.require<UpgradeWalletComponent.Params>()
|
||||
|
|
@ -77,22 +81,26 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareUpgradeScreenOpened)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareUpgradeScreenOpened())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
trackingContextProxy.removeContext()
|
||||
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun onBuyTangemWalletClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Upgrade))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContinueClick() {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade)
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.Upgrade))
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade())
|
||||
scanCard()
|
||||
}
|
||||
|
||||
|
|
@ -131,12 +139,9 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
scanResponse: ScanResponse,
|
||||
onSuccess: suspend () -> Unit,
|
||||
) {
|
||||
// Check if user attempted to upgrade before but something went wrong and a full reset is required
|
||||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build()
|
||||
val isSameWalletButNotFinishedBackup = userWallet?.walletId == params.userWalletId &&
|
||||
BackupValidator.isValidFull(scanResponse.card).not()
|
||||
|
||||
if (userWallet != null && isSameWalletButNotFinishedBackup) {
|
||||
if (userWallet?.walletId == params.userWalletId) {
|
||||
startResetCardsFlow.emit(userWallet)
|
||||
return
|
||||
}
|
||||
|
|
@ -201,7 +206,7 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onComplete() {
|
||||
setLoading(false)
|
||||
scanCard()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.upgradewallet.ui
|
|||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -44,6 +46,7 @@ internal fun UpgradeWalletContent(state: UpgradeWalletUM, modifier: Modifier = M
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(
|
||||
start = 16.dp,
|
||||
top = 24.dp,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import com.tangem.features.hotwallet.WalletActivationComponent
|
|||
import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
||||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -61,7 +62,7 @@ internal class WalletActivationModel @Inject constructor(
|
|||
val pushNotificationsCallbacks = PushNotificationsCallbacks()
|
||||
val mobileWalletSetupFinishedModelCallbacks = MobileWalletSetupFinishedModelCallbacks()
|
||||
|
||||
val isStartingWithAccessCode = params.isBackupExists
|
||||
private val isStartingWithAccessCode = params.isBackupExists
|
||||
val stackNavigation = StackNavigation<WalletActivationRoute>()
|
||||
val startRoute = if (isStartingWithAccessCode) {
|
||||
WalletActivationRoute.SetAccessCode
|
||||
|
|
@ -70,18 +71,28 @@ internal class WalletActivationModel @Inject constructor(
|
|||
}
|
||||
val currentRoute: MutableStateFlow<WalletActivationRoute> = MutableStateFlow(startRoute)
|
||||
|
||||
private val source = AnalyticsParam.ScreensSources.Main
|
||||
private val action = WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction.Backup
|
||||
private val analyticsSource = AnalyticsParam.ScreensSources.Main
|
||||
private val analyticsAction = WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction.Backup
|
||||
|
||||
init {
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
if (startRoute is WalletActivationRoute.ManualBackupStart) {
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.RecoveryPhraseScreenInfo(
|
||||
source = source.value,
|
||||
action = action.value,
|
||||
),
|
||||
)
|
||||
when (startRoute) {
|
||||
is WalletActivationRoute.ManualBackupStart -> {
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.RecoveryPhraseScreenInfo(
|
||||
source = analyticsSource.value,
|
||||
action = analyticsAction.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
is WalletActivationRoute.SetAccessCode -> {
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.AccessCodeScreenOpened(
|
||||
source = analyticsSource.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +145,7 @@ internal class WalletActivationModel @Inject constructor(
|
|||
secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.access_code_alert_skip_ok),
|
||||
onClick = {
|
||||
modelScope.launch {
|
||||
modelScope.launch(NonCancellable) {
|
||||
setAccessCodeSkippedUseCase(userWalletId, true)
|
||||
}
|
||||
navigateToPushNotificationsOrNext()
|
||||
|
|
@ -160,8 +171,8 @@ internal class WalletActivationModel @Inject constructor(
|
|||
stackNavigation.push(WalletActivationRoute.ManualBackupPhrase)
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.RecoveryPhraseScreen(
|
||||
source = source.value,
|
||||
action = action.value,
|
||||
source = analyticsSource.value,
|
||||
action = analyticsAction.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -172,8 +183,8 @@ internal class WalletActivationModel @Inject constructor(
|
|||
stackNavigation.push(WalletActivationRoute.ManualBackupCheck)
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.RecoveryPhraseCheck(
|
||||
source = source.value,
|
||||
action = action.value,
|
||||
source = analyticsSource.value,
|
||||
action = analyticsAction.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -184,8 +195,8 @@ internal class WalletActivationModel @Inject constructor(
|
|||
stackNavigation.push(WalletActivationRoute.ManualBackupCompleted)
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.BackupCompleteScreen(
|
||||
source = source.value,
|
||||
action = action.value,
|
||||
source = analyticsSource.value,
|
||||
action = analyticsAction.value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -193,6 +204,9 @@ internal class WalletActivationModel @Inject constructor(
|
|||
|
||||
inner class ManualBackupCompletedModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks {
|
||||
override fun onContinueClick(userWalletId: UserWalletId) {
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.AccessCodeScreenOpened(source = analyticsSource.value),
|
||||
)
|
||||
stackNavigation.push(WalletActivationRoute.SetAccessCode)
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +215,9 @@ internal class WalletActivationModel @Inject constructor(
|
|||
|
||||
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
|
||||
override fun onNewAccessCodeInput(userWalletId: UserWalletId, accessCode: String) {
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.ReEnterAccessCodeScreen(source = analyticsSource.value),
|
||||
)
|
||||
stackNavigation.push(WalletActivationRoute.ConfirmAccessCode(accessCode))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is WalletActivationRoute.ManualBackupPhrase -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_BACKUP_PHRASE,
|
||||
|
|
@ -24,7 +23,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is WalletActivationRoute.ManualBackupCheck -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_BACKUP_CHECK,
|
||||
|
|
@ -32,7 +30,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is WalletActivationRoute.ManualBackupCompleted -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_BACKUP_COMPLETED,
|
||||
|
|
@ -40,7 +37,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
is WalletActivationRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_ACCESS_CODE,
|
||||
|
|
@ -48,7 +44,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = false,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
is WalletActivationRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_ACCESS_CODE,
|
||||
|
|
@ -56,7 +51,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = true,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
is WalletActivationRoute.PushNotifications -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_NOTIFICATIONS,
|
||||
|
|
@ -64,7 +58,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.onboarding_title_notifications),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
is WalletActivationRoute.SetupFinished -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_DONE,
|
||||
|
|
@ -72,7 +65,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_done),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.walletbackup.model
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -24,6 +26,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -36,11 +39,14 @@ internal class WalletBackupModel @Inject constructor(
|
|||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val router: Router,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: WalletBackupComponent.Params = paramsContainer.require()
|
||||
|
||||
private val isScreenOpenedEventSent: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
val uiState: StateFlow<WalletBackupUM>
|
||||
field = MutableStateFlow(
|
||||
WalletBackupUM(
|
||||
|
|
@ -67,20 +73,33 @@ internal class WalletBackupModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.BackupScreenOpened(isManualBackupEnabled = true))
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
getUserWalletUseCase.invokeFlow(params.userWalletId)
|
||||
.onEach { either ->
|
||||
either.fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = {
|
||||
updateBackupStatuses(it)
|
||||
ifRight = { userWallet ->
|
||||
if (!isScreenOpenedEventSent.get() && userWallet is UserWallet.Hot) {
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.BackupScreenOpened(
|
||||
isBackedUp = userWallet.backedUp,
|
||||
),
|
||||
)
|
||||
isScreenOpenedEventSent.set(true)
|
||||
}
|
||||
updateBackupStatuses(userWallet)
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
trackingContextProxy.removeContext()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun updateBackupStatuses(userWallet: UserWallet) {
|
||||
uiState.update { currentState ->
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
|
|
@ -111,13 +130,14 @@ internal class WalletBackupModel @Inject constructor(
|
|||
)
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Backup))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRecoveryPhraseClick() {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase)
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase())
|
||||
if (uiState.value.backedUp) {
|
||||
getUserWalletUseCase.invoke(params.userWalletId)
|
||||
.fold(
|
||||
|
|
@ -160,7 +180,7 @@ internal class WalletBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate)
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate())
|
||||
router.push(AppRoute.WalletHardwareBackup(params.userWalletId))
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import arrow.core.getOrElse
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -49,6 +51,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -114,10 +117,16 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened())
|
||||
showPurchaseBlockWithDelay()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
trackingContextProxy.removeContext()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun showPurchaseBlockWithDelay() {
|
||||
modelScope.launch {
|
||||
delay(SHOW_PURCHASE_BLOCK_DELAY)
|
||||
|
|
@ -126,7 +135,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onCreateNewWalletClick() {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet)
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet())
|
||||
router.push(AppRoute.CreateHardwareWallet)
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +143,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
val userWallet = getUserWalletUseCase.invoke(params.userWalletId)
|
||||
.getOrElse { error("Cannot find user wallet with id: ${params.userWalletId.stringValue}") }
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonUpgradeCurrent)
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonUpgradeCurrent())
|
||||
if (!userWallet.backedUp) {
|
||||
messageSender.send(makeBackupAtFirstAlertBS)
|
||||
} else {
|
||||
|
|
@ -160,6 +169,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.HardwareWallet))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,8 @@ private fun PurchaseBlock(onBuyClick: () -> Unit, modifier: Modifier = Modifier)
|
|||
text = stringResourceSafe(R.string.wallet_add_hardware_purchase),
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
SecondaryButton(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
<ID>MultilineLambdaItParameter:ChooseManagedTokenContent.kt${ add( CurrencyItemUM.Basic( id = ManagedCryptoCurrency.ID( value = "ID+$it", ), name = "Bitcoin", symbol = "BTC", icon = CurrencyIconState.Loading, networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, onExpandClick = {}, ), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ChooseManagedTokensModel.kt$ChooseManagedTokensModel${ it.copy( notificationUM = null, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CurrencyItemMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = false, onSelectedStateChange = { _, _ -> }, onLongTap = { _ -> }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CurrencyNetworksMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, onLongTap = onLongTap, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy( error = when (exception) { CustomTokenFormValidationException.ContractAddress.Invalid -> { resourceReference(R.string.custom_token_creation_error_invalid_contract_address) } }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy( error = when (exception) { is CustomTokenFormValidationException.Decimals.Empty -> { null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -> { resourceReference( R.string.custom_token_creation_error_wrong_decimals, wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), ) } }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", error = stringReference("Contract address is invalid"), placeholder = stringReference("0x1234567890"), ) }</ID>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,19 @@ internal sealed class CustomTokenAnalyticsEvent(
|
|||
),
|
||||
)
|
||||
|
||||
class AddTokenToAnotherAccount(
|
||||
currencySymbol: String,
|
||||
derivationPath: String,
|
||||
source: ManageTokensSource,
|
||||
) : CustomTokenAnalyticsEvent(
|
||||
event = "Button - Add Token To Another Account",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.TOKEN_PARAM to currencySymbol,
|
||||
AnalyticsParam.Key.DERIVATION to derivationPath,
|
||||
AnalyticsParam.Key.SOURCE to source.analyticsName,
|
||||
),
|
||||
)
|
||||
|
||||
class NetworkSelected(networkName: String, source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Network Selected",
|
||||
params = mapOf(
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ internal sealed class ManageTokensAnalyticEvent(
|
|||
AnalyticsParam.Key.SOURCE to source.analyticsName,
|
||||
),
|
||||
)
|
||||
data object ButtonLater : ManageTokensAnalyticEvent(
|
||||
class ButtonLater : ManageTokensAnalyticEvent(
|
||||
event = "Button - Later",
|
||||
params = mapOf(AnalyticsParam.SOURCE to ManageTokensSource.ONBOARDING.analyticsName),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.managetokens.component
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
|
||||
|
|
@ -16,7 +17,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent {
|
|||
val source: ManageTokensSource,
|
||||
val onSelectNetworkClick: (CustomTokenFormValues) -> Unit,
|
||||
val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit,
|
||||
val onCurrencyAdded: () -> Unit,
|
||||
val onCurrencyAdded: (currency: CryptoCurrency) -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, CustomTokenFormComponent>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.core.decompose.context.childByContext
|
|||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenComponent
|
||||
|
|
@ -232,7 +233,16 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun dismissAndNotify() {
|
||||
private fun dismissAndNotify(currency: CryptoCurrency) {
|
||||
val account = addedToAccount
|
||||
if (account is Account.CryptoPortfolio && !account.isMainAccount) {
|
||||
val event = CustomTokenAnalyticsEvent.AddTokenToAnotherAccount(
|
||||
currencySymbol = currency.symbol,
|
||||
derivationPath = currency.network.derivationPath.value.orEmpty(),
|
||||
source = params.source,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
params.onCurrencyAdded(addedToAccount)
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
return@resource
|
||||
}
|
||||
|
||||
params.onCurrencyAdded()
|
||||
params.onCurrencyAdded(currency)
|
||||
}
|
||||
|
||||
private fun selectNetwork() {
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ internal class OnboardingManageTokensModel @Inject constructor(
|
|||
}
|
||||
},
|
||||
) {
|
||||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater)
|
||||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater())
|
||||
|
||||
useCasesFacade.saveManagedTokensUseCase(
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.markets.entry
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface MarketsEntryComponent {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenListComponent : ComposableContentComponent {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.yieldSupply)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
<ID>MultilineLambdaItParameter:AddToPortfolioBSContentUMFactory.kt$AddToPortfolioBSContentUMFactory${ if (it != selectedWalletId) { onAnotherWalletSelect(it) onWalletSelectorVisibilityChange(false) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioBottomSheet.kt${ Content( modifier = Modifier.fillMaxWidth(), state = it, ) WalletSelectorBottomSheet(it.walletSelectorConfig) }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioManager.kt$AddToPortfolioManager${ it.toMutableMap().apply { this[userWalletId] = if (isAddAction) { this[userWalletId].orEmpty() + network } else { this[userWalletId].orEmpty() - network } } }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ PortfolioData.CryptoCurrencyData( userWallet = selectedPortfolio.userWallet, status = addedToken, actions = it.states, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ Timber.e(it) params.callback.onDismiss() }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ tokenActionsData.emit(it) navigation.replaceAll(AddToPortfolioRoutes.TokenActions) }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddTokenModel.kt$AddTokenModel${ processError(error = it) uiState.value = um.toggleProgress(false) return@launch }</ID>
|
||||
|
|
@ -58,7 +57,6 @@
|
|||
<ID>MultilineLambdaItParameter:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager${ when (val status = it.status) { is PaginationStatus.Paginating -> { if (status.lastResult is BatchFetchResult.Success) { it.data.size == 1 } else { null } } is PaginationStatus.EndOfPagination -> { it.data.size == 1 } else -> null } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListItem.kt${ if (Random.nextBoolean()) { it.first.inc() to PriceChangeType.UP } else { it.first.dec() to PriceChangeType.DOWN } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListLazyColumn.kt${ (it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first() ?.let { rawId -> CryptoCurrency.RawID(rawId) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it == BottomSheetState.EXPANDED) { analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it) { analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(tokenFound = false)) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it.isNotEmpty()) { activeListManager.getBatchKeysByItemIds(visibleItemIds.value) } else { null } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it.list !is ListUM.Content) { visibleItemIds.value = emptyList() } }</ID>
|
||||
|
|
@ -118,12 +116,6 @@
|
|||
<ID>NoNameShadowing:MarketsTokenDetailsModel.kt$MarketsTokenDetailsModel${ it.copy( chartState = it.chartState.copy( status = MarketsTokenDetailsUM.ChartState.Status.ERROR, ), body = if (it.body is MarketsTokenDetailsUM.Body.Error) { MarketsTokenDetailsUM.Body.Nothing } else { it.body }, ) }</ID>
|
||||
<ID>NoNameShadowing:MyPortfolioUMFactory.kt$MyPortfolioUMFactory${ networkIds.contains(it.status.currency.network.backendId) }</ID>
|
||||
<ID>NoNameShadowing:NewMarketsPortfolioDelegate.kt$NewMarketsPortfolioDelegate$portfolio</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager$val isInInitialLoadingErrorState = batchFlow.state .map { it.status is PaginationStatus.InitialLoadingError } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, initialValue = false, )</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager$val isSearchNotFoundState = batchFlow.state .map { currentSearchText().isNullOrEmpty().not() && it.status is PaginationStatus.EndOfPagination && it.data.isEmpty() } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, initialValue = false, )</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsListModel.kt$MarketsListModel$val isVisibleOnScreen = MutableStateFlow(false)</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsListUMStateManager.kt$MarketsListUMStateManager$val isInSearchStateFlow = state.map { it.searchBar.isActive }.distinctUntilChanged()</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsTokenDetailsModel.kt$MarketsTokenDetailsModel$val isVisibleOnScreen = MutableStateFlow(false)</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:TokenActionsHandler.kt$TokenActionsHandler$private val isDemoCardUseCase: IsDemoCardUseCase</ID>
|
||||
<ID>NullableToStringCall:MarketsListItemUM.kt$MarketsListItemUM$marketCap.toString()</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:MarketsListModel.kt$MarketsListModel$activeListManager</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:MarketsListUMStateManager.kt$MarketsListUMStateManager$state</ID>
|
||||
|
|
|
|||
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