Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-15 11:22:40 +03:00
commit c2cd7e6b4c
675 changed files with 9577 additions and 4375 deletions

View file

@ -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&lt;(UserWallet, AccountStatus) -&gt; Boolean&gt;</ID>
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$val isAccountMode: Flow&lt;Boolean&gt;</ID>
<ID>UseSumOfInsteadOfFlatMapSize:PortfolioFetcher.kt$PortfolioFetcher.Data$flatten()</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -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
}

View file

@ -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>?>

View file

@ -1,10 +1,5 @@
<?xml version="1.0" ?>
<SmellBaseline>
<ManuallySuppressedIssues/>
<CurrentIssues>
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isAccountMode: Flow&lt;Boolean&gt; by lazy { isAccountsModeEnabledUseCase() }</ID>
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isEnabled: MutableStateFlow&lt;(UserWallet, AccountStatus) -&gt; Boolean&gt; = MutableStateFlow { _, _ -&gt; 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>

View file

@ -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
}
}
}

View file

@ -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",
)
}

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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,

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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)
}

View file

@ -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>

View file

@ -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,
)

View file

@ -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")),
)
}
}

View file

@ -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),
)
}
}

View file

@ -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 = {},

View file

@ -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)

View file

@ -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) }
}

View file

@ -5,6 +5,7 @@ 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.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -137,11 +138,12 @@ private fun WalletBlock(
vertical = 12.dp,
),
) {
Row {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier
.weight(1f, fill = false)
.padding(end = 8.dp),
.weight(1f, fill = false),
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,

View file

@ -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)

View file

@ -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 -&gt; Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -&gt; { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { appRouter.replaceAll(AppRoute.Wallet) } } } }</ID>
</CurrentIssues>
</SmellBaseline>

View file

@ -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) }
}

View file

@ -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,

View file

@ -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),

View file

@ -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&lt;Boolean&gt; = MutableStateFlow(value = false)</ID>
<ID>RedundantSuspendModifier:UserWalletSaver.kt$UserWalletSaver$suspend</ID>
<ID>UnnecessaryLet:ItemsBuilder.kt$ItemsBuilder$let(::add)</ID>
</CurrentIssues>

View file

@ -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,14 @@ 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.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 +33,7 @@ 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.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.version.AppVersionProvider
import kotlinx.collections.immutable.ImmutableList
@ -60,6 +65,9 @@ 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,
) : Model() {
private val params: DetailsComponent.Params = paramsContainer.require()
@ -216,7 +224,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())
}
}
}

View file

@ -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,
)
}

View file

@ -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,

View file

@ -1,6 +0,0 @@
package com.tangem.features.feed.entry
enum class BottomSheetState {
EXPANDED,
COLLAPSED,
}

View file

@ -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 {

View file

@ -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,6 +87,7 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.utils)
/* Common */
implementation(projects.common.ui)

View file

@ -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,13 @@ 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.ui.EntryBottomSheetContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -35,7 +40,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
popCallback = { onChildBack() },
)
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, Any>> = childStack(
private val stack: Value<ChildStack<FeedEntryChildFactory.Child, ComposableModularContentComponent>> = childStack(
key = "main",
source = stackNavigation,
serializer = FeedEntryChildFactory.Child.serializer(),
@ -59,7 +64,16 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor(
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
bottomSheetState // TODO will be continued in next tasks.
val stackState by stack.subscribeAsState()
BackHandler(enabled = bottomSheetState.value == BottomSheetState.EXPANDED) {
onChildBack()
}
EntryBottomSheetContent(
stackState = stackState,
onHeaderSizeChange = onHeaderSizeChange,
)
}
private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) {

View file

@ -3,6 +3,7 @@ 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.core.ui.decompose.ComposableModularContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.feed.components.feed.DefaultFeedComponent
@ -11,8 +12,9 @@ import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListCo
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
@ -43,7 +45,7 @@ internal class FeedEntryChildFactory {
child: Child,
appComponentContext: AppComponentContext,
onTokenClick: (TokenMarketParams, AppCurrency) -> Unit,
): Any {
): ComposableModularContentComponent {
return when (child) {
is Child.TokenDetails -> {
DefaultMarketsTokenDetailsComponent(

View file

@ -1,23 +1,37 @@
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.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.ui.feed.FeedListContent
import com.tangem.features.feed.ui.feed.FeedListHeader
internal class DefaultFeedComponent(
appComponentContext: AppComponentContext,
) : ComposableModularContentComponent, AppComponentContext by appComponentContext {
private val feedComponentModel = getOrCreateModel<FeedComponentModel>()
@Composable
override fun Title() {
val state by feedComponentModel.state.collectAsStateWithLifecycle()
FeedListHeader(state.searchBar)
}
@Composable
override fun Content(modifier: Modifier) {
val state by feedComponentModel.state.collectAsStateWithLifecycle()
FeedListContent(
modifier = modifier,
state = state,
)
}
@Composable
override fun Footer() {
}
override fun Footer() = Unit
}

View file

@ -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 ComponentModule {
@Binds
@Singleton
fun bindFeedEntryComponent(factory: DefaultFeedEntryComponent.Factory): FeedEntryComponent.Factory
}

View file

@ -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
}

View file

@ -0,0 +1,103 @@
package com.tangem.features.feed.model.feed
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
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.news.usecase.FetchTrendingNewsUseCase
import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase
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.toPersistentHashMap
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
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,
) : Model() {
private val _state = MutableStateFlow(initialState())
val state = _state.asStateFlow()
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 } },
)
}
init {
modelScope.launch(dispatchers.default) {
fetchTrendingNewsUseCase()
subscribeOnTrendingNews()
}
_state.update { feedListUM ->
feedListUM.copy(
searchBar = _state.value.searchBar.copy(onQueryChange = searchBarStateFactory::onSearchQueryChange),
)
}
}
private suspend fun subscribeOnTrendingNews() {
manageTrendingNewsUseCase().collect { articles ->
trendingNewsStateFactory.updateTrendingNewsState(articles)
}
}
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)
} // TODO in [REDACTED_TASK_KEY] add correct sorting
}.toPersistentHashMap(),
currentSortByType = SortByTypeUM.TopGainers,
),
)
}
private fun getCurrentDate(): String {
val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault())
return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate)
}
}

View file

@ -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))
}
},
)
}

View file

@ -22,25 +22,25 @@ 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.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.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,51 +48,44 @@ 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.feed.state.*
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.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 FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.drawBehind { drawRect(background) },
) {
SearchBar(
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())
}
}
}
.padding(bottom = 4.dp),
state = state.searchBar,
)
SpacerH(20.dp)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.padding(horizontal = 20.dp),
text = stringResourceSafe(R.string.feed_market_and_news),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
@ -100,7 +93,7 @@ internal fun FeedList(state: FeedListUM, onHeaderSizeChange: (Dp) -> Unit, modif
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
.padding(horizontal = 20.dp),
text = state.currentDate,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.tertiary,
@ -156,6 +149,17 @@ private fun MarketBlock(marketChartConfig: MarketChartConfig, feedListCallbacks:
@Composable
private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) {
if (marketChartConfig.marketCharts.isNotEmpty()) {
Header(
title = {
Text(
text = stringResourceSafe(R.string.markets_common_title),
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
)
},
onSeeAllClick = { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) },
)
LazyRow(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
@ -175,17 +179,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 +200,38 @@ 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()
}
}
}
}
@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 +257,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 +272,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 +284,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 +307,7 @@ private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) {
.fillMaxWidth()
.padding(horizontal = 20.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
title()
@ -310,7 +326,10 @@ private fun Charts(
onItemClick: (MarketsListItemUM) -> Unit,
modifier: Modifier = Modifier,
) {
BlockCard(modifier) {
BlockCard(
modifier = modifier,
colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action),
) {
Column(
modifier = Modifier
.fillMaxWidth()
@ -380,9 +399,6 @@ private val LinearGradientSecondPart = Color(0xFFE05AED)
@Composable
private fun FeedListPreview() {
TangemThemePreview {
FeedList(
state = createFeedPreviewState(),
onHeaderSizeChange = {},
)
FeedListContent(state = createFeedPreviewState())
}
}

View file

@ -0,0 +1,100 @@
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.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
import com.tangem.features.feed.ui.market.components.MarketsListItemPlaceholder
@Composable
internal fun MarketLoadingBlock() {
RectangleShimmer(
modifier = Modifier
.padding(start = 16.dp)
.size(width = 104.dp, height = 18.dp),
)
SpacerH(12.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(32.dp)
}
@Composable
internal fun MarketPulseLoadingBlock() {
RectangleShimmer()
SpacerH(8.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(12.dp)
ChartsLoading(modifier = Modifier.padding(horizontal = 16.dp))
SpacerH(32.dp)
}
@Composable
internal fun NewsLoadingBlock() {
Column {
RectangleShimmer()
SpacerH(12.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()
}
}
}
}
@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 {
NewsLoadingBlock()
SpacerH(10.dp)
MarketLoadingBlock()
SpacerH(10.dp)
MarketPulseLoadingBlock()
}
}
}

View file

@ -1,11 +1,11 @@
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.news.ArticleConfigUM
import com.tangem.common.ui.news.ArticleTagUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
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.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
@ -38,7 +38,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),
@ -159,21 +159,18 @@ internal object FeedListPreviewDataProvider {
),
)
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")),
)
}

View file

@ -14,7 +14,7 @@ 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,
)
@ -28,6 +28,12 @@ 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
}
internal data class MarketChartConfig(
val marketCharts: ImmutableMap<SortByTypeUM, MarketChartUM>,
val currentSortByType: SortByTypeUM = SortByTypeUM.TopGainers,
@ -49,7 +55,7 @@ internal sealed interface MarketChartUM {
data class LoadingError(val onRetryClicked: () -> Unit) : MarketChartUM
}
data class SortChartConfigUM(
internal data class SortChartConfigUM(
val sortByType: SortByTypeUM,
val isSelected: Boolean,
)

View file

@ -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(),
),
),
)
}
}

View file

@ -0,0 +1,83 @@
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.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.news.ShortArticle
import com.tangem.utils.Provider
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
internal class TrendingNewsStateFactory(
private val currentStateProvider: Provider<FeedListUM>,
private val onStateUpdate: (FeedListUM) -> Unit,
) {
fun updateTrendingNewsState(news: List<ShortArticle>) {
val trendingArticleIndex = news.indexOfFirst { it.isTrending }
val trendingArticle = if (trendingArticleIndex != -1) news[trendingArticleIndex] else null
val commonArticles = if (trendingArticleIndex != -1) {
news.toMutableList().apply { removeAt(trendingArticleIndex) }
} else {
news
}
val currentState = currentStateProvider()
onStateUpdate(
currentState.copy(
trendingArticle = trendingArticle?.let { article ->
ArticleConfigUM(
id = article.id,
title = article.title,
score = article.score,
isTrending = true,
tags = article.categories.map { category ->
LabelUM(text = TextReference.Str(category.name))
}.plus(
article.relatedTokens.map { token ->
LabelUM(
text = TextReference.Str(token.symbol),
leadingContent = LabelLeadingContentUM.Token(
iconUrl = getTokenIconUrlFromDefaultHost(
tokenId = CryptoCurrency.RawID(token.id),
),
),
)
},
).toPersistentSet(),
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
isViewed = article.viewed,
)
},
news = NewsUM.Content(
commonArticles.map { article ->
ArticleConfigUM(
id = article.id,
title = article.title,
score = article.score,
isTrending = false,
tags = article.categories.map { category ->
LabelUM(text = TextReference.Str(category.name))
}.plus(
article.relatedTokens.map { token ->
LabelUM(
text = TextReference.Str(token.symbol),
leadingContent = LabelLeadingContentUM.Token(
iconUrl = getTokenIconUrlFromDefaultHost(
tokenId = CryptoCurrency.RawID(token.id),
),
),
)
},
).toPersistentSet(),
createdAt = "1 min ago", // TODO in [REDACTED_TASK_KEY]
isViewed = article.viewed,
)
}.toPersistentList(),
),
),
)
}
}

View file

@ -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 -&gt; Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -&gt; appRouter.replaceAll(AppRoute.Wallet) } }</ID>
<ID>MultilineLambdaItParameter:StoriesProgressBar.kt${ when (index) { currentStep -&gt; it.fillMaxWidth(progress.value) in 0 until currentStep -&gt; it.fillMaxWidth(fraction = 1f) else -&gt; 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>

View file

@ -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,

View file

@ -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>
}

View file

@ -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 &amp;&amp; it.card.wallets.map { it.curve }.toSet().isNotEmpty() }</ID>
<ID>BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId &amp;&amp; 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 -&gt; Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -&gt; { 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 -&gt; Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -&gt; { 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, _ -&gt; 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 -&gt; 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 &amp;&amp; BackupValidator.isValidFull(it.card).not() } val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId &amp;&amp; it.card.wallets.map { it.curve }.toSet().isNotEmpty() } if (userWallet != null &amp;&amp; (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 -&gt; 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 -&gt; TopAppBarButtonUM.Text( text = resourceReference(R.string.common_skip), onClicked = onSkipClick, ) state.showFeedbackButton -&gt; TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_chat_24, onClicked = onFeedbackClick, ) else -&gt; 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>

View file

@ -11,6 +11,7 @@ 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 +29,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 +57,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 +119,45 @@ 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(onAccessCodeChange = ::onAccessCodeChange)
}
},
),
secondAction = EventMessageAction(
title = resourceReference(R.string.access_code_alert_validation_ok),
onClick = ::setNewCode,
),
),
)
}
private suspend fun showErrorAndReset() {
uiState.update { currentState ->
currentState.copy(
@ -152,11 +182,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 +204,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)
}
}

View file

@ -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
},

View file

@ -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)
}
}

View file

@ -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,
)
}
}

View file

@ -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 {

View file

@ -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)

View file

@ -1,6 +1,7 @@
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.fillMaxWidth
@ -43,11 +44,12 @@ internal fun OptionBlock(
}
.padding(16.dp),
) {
Row {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier
.weight(1f, fill = false)
.padding(end = 4.dp),
.weight(1f, fill = false),
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,

View file

@ -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) }
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -3,9 +3,12 @@ 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
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -22,8 +25,8 @@ import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStart
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
Box(
modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.padding(
@ -33,53 +36,57 @@ internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modi
bottom = 16.dp,
),
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 8.dp,
Column(modifier = Modifier.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
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(top = 16.dp),
text = stringResourceSafe(R.string.common_continue),

View file

@ -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,
)
}
}

View file

@ -34,7 +34,6 @@ internal class DefaultHotWalletStepperComponent @AssistedInject constructor(
modifier = modifier,
onBackClick = model::onBackClick,
onSkipClick = model::onSkipClick,
onFeedbackClick = model::onFeedbackClick,
)
}

View file

@ -34,9 +34,4 @@ internal class HotWalletStepperModel @Inject constructor(
// TODO send analytics
params.callback.onSkipClick()
}
fun onFeedbackClick() {
// TODO send analytics
// openFeedback()
}
}

View file

@ -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 = {},
)
}
}

View file

@ -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,
),
)

View file

@ -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()
}
}
}

View file

@ -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
@ -70,16 +71,16 @@ 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,
source = analyticsSource.value,
action = analyticsAction.value,
),
)
}
@ -134,7 +135,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 +161,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 +173,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 +185,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 +194,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 +205,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))
}

View file

@ -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,
)
}
}

View file

@ -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))
}
}

View file

@ -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) }
}

View file

@ -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 = { _, _ -&gt; }, onLongTap = { _ -&gt; }, ) }</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 -&gt; { 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 -&gt; { null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -&gt; { 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>

View file

@ -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(

View file

@ -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),
)

View file

@ -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>

View file

@ -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()
}

View file

@ -364,7 +364,7 @@ internal class CustomTokenFormModel @Inject constructor(
return@resource
}
params.onCurrencyAdded()
params.onCurrencyAdded(currency)
}
private fun selectNetwork() {

View file

@ -287,7 +287,7 @@ internal class OnboardingManageTokensModel @Inject constructor(
}
},
) {
analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater)
analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater())
useCasesFacade.saveManagedTokensUseCase(
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,

View file

@ -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

View file

@ -1,6 +0,0 @@
package com.tangem.features.markets.entry
enum class BottomSheetState {
EXPANDED,
COLLAPSED,
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 -&gt; { if (status.lastResult is BatchFetchResult.Success) { it.data.size == 1 } else { null } } is PaginationStatus.EndOfPagination -&gt; { it.data.size == 1 } else -&gt; 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 -&gt; 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() &amp;&amp; it.status is PaginationStatus.EndOfPagination &amp;&amp; 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>

View file

@ -11,6 +11,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
@ -21,7 +22,6 @@ import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalytics
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory

View file

@ -15,10 +15,10 @@ 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.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent

View file

@ -15,10 +15,10 @@ import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent

View file

@ -4,6 +4,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.popToFirst
import com.arkivanov.decompose.router.stack.pushNew
import com.arkivanov.decompose.router.stack.replaceAll
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
@ -47,6 +48,7 @@ internal class AddToPortfolioModel @Inject constructor(
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2,
private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency,
private val messageSender: UiMessageSender,
private val analyticsEventHandler: AnalyticsEventHandler,
val portfolioSelectorController: PortfolioSelectorController,
) : Model(),
ChooseNetworkComponent.Callbacks by callbackDelegate,
@ -93,6 +95,7 @@ internal class AddToPortfolioModel @Inject constructor(
.map { it.availableToAddData }
.distinctUntilChanged()
.stateIn(this)
val isAccountMode = portfolioSelectorController.isAccountModeSync()
// use snapshot data, looks like we dont need to remap at runtime
val data = featureDataFlow.value
@ -117,6 +120,7 @@ internal class AddToPortfolioModel @Inject constructor(
// force select a portfolio, triggers [selectedPortfolio]
portfolioSelectorController.selectAccount(accountId)
} else {
logAccountSelector(isAccountMode)
navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector)
}
@ -165,6 +169,7 @@ internal class AddToPortfolioModel @Inject constructor(
.onEach {
middleNavigationJob?.cancel()
middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this)
logAccountSelector(isAccountMode)
navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector)
}
.launchIn(this)
@ -194,6 +199,12 @@ internal class AddToPortfolioModel @Inject constructor(
.launchIn(modelScope)
}
private fun logAccountSelector(isAccountMode: Boolean) {
if (isAccountMode) {
analyticsEventHandler.send(eventBuilder.popupToChooseAccount())
}
}
private fun changeNetworkNavigationFlow(): Flow<SelectedNetwork> {
return setupNetworkFlow(selectedPortfolio)
.onEach { newNetwork ->
@ -260,6 +271,7 @@ internal class AddToPortfolioModel @Inject constructor(
data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null
val availableToAddAccount =
availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null
if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged())
SelectedPortfolio(
isAccountMode = isAccountMode,
userWallet = availableToAddWallets.userWallet,

View file

@ -1,5 +1,6 @@
package com.tangem.features.markets.portfolio.add.impl.model
import com.tangem.common.ui.addtoken.AddTokenUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
@ -10,13 +11,13 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.add.api.SelectedNetwork
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent
import com.tangem.features.markets.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress
import com.tangem.common.ui.addtoken.AddTokenUM
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
@ -74,7 +75,8 @@ internal class AddTokenModel @Inject constructor(
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames))
val cryptoCurrency = selectedNetwork.cryptoCurrency
val accountId = selectedPortfolio.account.account.account.accountId
val account = selectedPortfolio.account.account.account
val accountId = account.accountId
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
.onLeft {
processError(error = it)
@ -90,6 +92,11 @@ internal class AddTokenModel @Inject constructor(
if (status == null) {
processError(error = null)
} else {
when (account) {
is Account.CryptoPortfolio -> if (!account.isMainAccount) {
analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount())
}
}
params.callbacks.onTokenAdded(status.status)
}
uiState.value = um.toggleProgress(false)

View file

@ -62,10 +62,7 @@ internal class TokenActionsModel @Inject constructor(
)
private fun handledQuickAction(handledAction: HandledQuickAction) {
val event = analyticsEventBuilder.quickActionClick(
actionUM = handledAction.action,
blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name,
)
val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action)
analyticsEventHandler.send(event)
val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive
if (!isReceive) return

View file

@ -1,5 +1,6 @@
package com.tangem.features.markets.portfolio.add.impl.model
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
@ -15,6 +16,7 @@ import javax.inject.Inject
@ModelScoped
internal class TokenActionsUiBuilder @Inject constructor(
paramsContainer: ParamsContainer,
private val analyticsEventHandler: AnalyticsEventHandler,
) {
private val params = paramsContainer.require<TokenActionsComponent.Params>()
@ -32,7 +34,10 @@ internal class TokenActionsUiBuilder @Inject constructor(
)
return TokenActionsUM(
token = tokenUM,
onLaterClick = { params.callbacks.onLaterClick() },
onLaterClick = {
analyticsEventHandler.send(params.eventBuilder.getTokenLater())
params.callbacks.onLaterClick()
},
quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler),
)
}

View file

@ -31,7 +31,7 @@ internal class DefaultAddToPortfolioManager @AssistedInject constructor(
override val allAvailableNetworks: Flow<List<TokenMarketInfo.Network>> = _allAvailableNetworks.asSharedFlow()
override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(onlyMultiCurrency = true),
mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true),
scope = scope,
)

View file

@ -21,6 +21,14 @@ internal class PortfolioAnalyticsEvent(
),
)
fun popupToChooseAccount() = PortfolioAnalyticsEvent(
event = "Popup to choose account",
)
fun addToNotMainAccount() = PortfolioAnalyticsEvent(
event = "Button - Add (token not to main Account)",
)
fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected")
fun addToPortfolioContinue(blockchainNames: List<String>) = PortfolioAnalyticsEvent(
@ -47,5 +55,19 @@ internal class PortfolioAnalyticsEvent(
put("blockchain", blockchainName)
},
)
fun getTokenActionClick(actionUM: TokenActionsBSContentUM.Action) = PortfolioAnalyticsEvent(
event = when (actionUM) {
TokenActionsBSContentUM.Action.Buy -> "Popup Get token - Button Buy"
TokenActionsBSContentUM.Action.Receive -> "Popup Get token - Button Receive"
TokenActionsBSContentUM.Action.Exchange -> "Popup Get token - Button Exchange"
TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake"
else -> "error"
},
)
fun getTokenLater() = PortfolioAnalyticsEvent(
event = "Popup Get token - Button Later",
)
}
}

View file

@ -193,7 +193,10 @@ internal class MarketsPortfolioModel @Inject constructor(
NewAddToPortfolioManager.State.NothingToAdd -> AddButtonState.Unavailable
}
},
onAddClick = { bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) },
onAddClick = {
analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked())
bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio)
},
)
newMarketsPortfolioDelegate.combineData()
.onEach { _state.value = it }

View file

@ -16,12 +16,12 @@ import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRoute.MarketsTokenDetails.AnalyticsParams
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.markets.toSerializableParam
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.MarketsTokenListComponent
import com.tangem.features.markets.tokenlist.impl.model.MarketsListModel
import com.tangem.features.markets.tokenlist.impl.ui.MarketsList

View file

@ -9,7 +9,7 @@ internal sealed class MarketsListAnalyticsEvent(
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets", event = event, params = params) {
data object BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened")
class BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened")
data class SortBy(
val sortByTypeUM: SortByTypeUM,
@ -33,11 +33,11 @@ internal sealed class MarketsListAnalyticsEvent(
),
)
data object StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo")
class StakingPromoShown : MarketsListAnalyticsEvent(event = "Notice - Staking Promo")
data object StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed")
class StakingPromoClosed : MarketsListAnalyticsEvent(event = "Staking Promo Closed")
data object StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info")
class StakingMoreInfoClicked : MarketsListAnalyticsEvent(event = "Staking More Info")
data class TokenSearched(val tokenFound: Boolean) : MarketsListAnalyticsEvent(
event = "Token Searched",
@ -46,5 +46,5 @@ internal sealed class MarketsListAnalyticsEvent(
),
)
data object ShowTokens : MarketsListAnalyticsEvent(event = "Button - Show Tokens")
class ShowTokens : MarketsListAnalyticsEvent(event = "Button - Show Tokens")
}

View file

@ -6,6 +6,7 @@ import arrow.core.getOrElse
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.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
@ -17,7 +18,6 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
import com.tangem.domain.settings.usercountry.models.UserCountry
import com.tangem.domain.settings.usercountry.models.UserCountryError
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
@ -69,9 +69,9 @@ internal class MarketsListModel @Inject constructor(
visibleItemsChanged = { visibleItemIds.value = it },
onRetryButtonClicked = { activeListManager.reload() },
onTokenClick = { onTokenUIClicked(it) },
onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked) },
onStakingNotificationClick = { analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingMoreInfoClicked()) },
onStakingNotificationCloseClick = { onStakingNotificationCloseClick() },
onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens) },
onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) },
)
private val mainMarketsListManager = MarketsListBatchFlowManager(
@ -150,7 +150,7 @@ internal class MarketsListModel @Inject constructor(
if (marketsListUMStateManager.state.value.stakingNotificationMaxApy == null &&
stakingNotificationMaxApy != null
) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown)
analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoShown())
}
marketsListUMStateManager.onUiItemsChanged(
@ -267,9 +267,9 @@ internal class MarketsListModel @Inject constructor(
}
private fun initAnalytics() {
containerBottomSheetState.onEach {
if (it == BottomSheetState.EXPANDED) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened)
containerBottomSheetState.onEach { bottomSheetState ->
if (bottomSheetState == BottomSheetState.EXPANDED) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened())
}
}.launchIn(modelScope)
@ -303,7 +303,7 @@ internal class MarketsListModel @Inject constructor(
}
private fun onStakingNotificationCloseClick() {
analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed)
analyticsEventHandler.send(MarketsListAnalyticsEvent.StakingPromoClosed())
modelScope.launch {
promoRepository.setMarketsStakingNotificationHideClicked()
}

View file

@ -30,6 +30,7 @@ import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.buttons.SecondarySmallButton
import com.tangem.core.ui.components.buttons.SmallButtonConfig
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
@ -46,7 +47,6 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListLazyColumn
import com.tangem.features.markets.tokenlist.impl.ui.components.MarketsListSortByBottomSheet

View file

@ -0,0 +1,18 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.features.news.details.api"
}
dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Compose */
implementation(deps.compose.runtime)
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.news.details.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface NewsDetailsComponent : ComposableContentComponent {
data class Params(val selectedArticleId: Int = 0)
interface Factory : ComponentFactory<Params, NewsDetailsComponent>
}

View file

@ -0,0 +1,43 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
android {
namespace = "com.tangem.features.news.details.impl"
}
dependencies {
/* AndroidX */
implementation(deps.lifecycle.compose)
implementation(deps.androidx.activity.compose)
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.material3)
/** Core modules */
implementation(projects.core.ui)
implementation(projects.core.utils)
implementation(projects.core.decompose)
implementation(projects.common.ui)
implementation(projects.common.routing)
/** Feature modules */
implementation(projects.features.news.newsDetails.api)
implementation(projects.domain.models)
/** Other dependencies */
implementation(deps.kotlin.immutable.collections)
implementation(deps.arrow.core)
implementation(deps.timber)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.news.details.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.news.details.api.NewsDetailsComponent
import com.tangem.features.news.details.impl.ui.NewsDetailsContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultNewsDetailsComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted params: NewsDetailsComponent.Params,
) : NewsDetailsComponent, AppComponentContext by context {
private val model: NewsDetailsModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val uiState by model.uiState.collectAsState()
NewsDetailsContent(
state = uiState,
onBackClick = model::onBackClick,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : NewsDetailsComponent.Factory {
override fun create(
context: AppComponentContext,
params: NewsDetailsComponent.Params,
): DefaultNewsDetailsComponent
}
}

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