Updated on 2026-08-14
This commit is contained in:
commit
c2cd7e6b4c
675 changed files with 9577 additions and 4375 deletions
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:PortfolioFetcher.kt$PortfolioFetcher.Mode.All$val onlyMultiCurrency: Boolean</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$/** * for some Feature specific filtering * combine and update with your Feature data and [PortfolioFetcher.data] */ val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean></ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorComponent.kt$PortfolioSelectorController$val isAccountMode: Flow<Boolean></ID>
|
||||
<ID>UseSumOfInsteadOfFlatMapSize:PortfolioFetcher.kt$PortfolioFetcher.Data$flatten()</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -26,7 +26,7 @@ interface PortfolioFetcher {
|
|||
|
||||
val isSingleChoice: Boolean = balances.values
|
||||
.map { it.accountsBalance.accountStatuses }
|
||||
.flatten().size == 1
|
||||
.sumOf { it.size } == 1
|
||||
|
||||
fun isSingleChoice(walletId: UserWalletId): Boolean = balances[walletId]
|
||||
?.accountsBalance
|
||||
|
|
@ -43,7 +43,7 @@ interface PortfolioFetcher {
|
|||
}
|
||||
|
||||
sealed interface Mode {
|
||||
data class All(val onlyMultiCurrency: Boolean) : Mode
|
||||
data class All(val isOnlyMultiCurrency: Boolean) : Mode
|
||||
data class Wallet(val walletId: UserWalletId) : Mode
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ interface PortfolioSelectorController {
|
|||
* combine and update with your Feature data and [PortfolioFetcher.data]
|
||||
*/
|
||||
val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean>
|
||||
suspend fun isAccountModeSync(): Boolean
|
||||
|
||||
fun selectAccount(accountId: AccountId?)
|
||||
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, AccountStatus>?>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:DefaultPortfolioSelectorController.kt$DefaultPortfolioSelectorController$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:PortfolioSelectorModel.kt$PortfolioSelectorModel$private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase</ID>
|
||||
</CurrentIssues>
|
||||
<CurrentIssues/>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,100 @@
|
|||
package com.tangem.features.account.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.domain.models.account.AccountName
|
||||
import com.tangem.domain.models.account.CryptoPortfolioIcon
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
|
||||
sealed class AccountSettingsAnalyticEvents(
|
||||
category: String = "Settings / Account",
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category, event, params) {
|
||||
|
||||
class AccountSettingsScreenOpened : AccountSettingsAnalyticEvents(
|
||||
event = "Account Settings Screen Opened",
|
||||
)
|
||||
|
||||
class ButtonManageTokens : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Manage Tokens",
|
||||
)
|
||||
|
||||
class ButtonArchiveAccount : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Archive Account",
|
||||
)
|
||||
|
||||
class ButtonArchiveAccountConfirmation : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Archive Account Confirmation",
|
||||
)
|
||||
|
||||
class ButtonCancelAccountArchivation : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Cancel Account Archivation",
|
||||
)
|
||||
|
||||
class AccountArchived : AccountSettingsAnalyticEvents(
|
||||
event = "Account Archived",
|
||||
)
|
||||
|
||||
class ButtonEdit : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Edit",
|
||||
)
|
||||
|
||||
class AccountEditScreenOpened : AccountSettingsAnalyticEvents(
|
||||
event = "Account Edit Screen Opened",
|
||||
)
|
||||
|
||||
class ButtonSave(
|
||||
val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Save",
|
||||
params = buildMap {
|
||||
val accountName = when (name) {
|
||||
is AccountName.Custom -> name.value
|
||||
AccountName.DefaultMain -> "DefaultMain"
|
||||
}
|
||||
put("Name", accountName)
|
||||
put("Color", icon.color.name)
|
||||
put("Icon", icon.value.name)
|
||||
},
|
||||
)
|
||||
|
||||
class ButtonAddNewAccount(
|
||||
val name: AccountName,
|
||||
val icon: CryptoPortfolioIcon,
|
||||
val derivationIndex: Int,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Button - Add New Account",
|
||||
params = buildMap {
|
||||
val accountName = when (name) {
|
||||
is AccountName.Custom -> name.value
|
||||
AccountName.DefaultMain -> "DefaultMain"
|
||||
}
|
||||
put("Name", accountName)
|
||||
put("Color", icon.color.name)
|
||||
put("Icon", icon.value.name)
|
||||
put("Derivation", derivationIndex.toString())
|
||||
},
|
||||
)
|
||||
|
||||
class AccountError(
|
||||
val source: Source,
|
||||
val error: String,
|
||||
) : AccountSettingsAnalyticEvents(
|
||||
event = "Account Error",
|
||||
params = buildMap {
|
||||
put("Error", error)
|
||||
},
|
||||
)
|
||||
|
||||
enum class Source(val value: String) {
|
||||
NEW_ACCOUNT("New Account"), EDIT("Edit"), ARCHIVE("Archive")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun AccountCreateEditComponent.Params.toAnalyticSource() = when (this) {
|
||||
is AccountCreateEditComponent.Params.Create -> Source.NEW_ACCOUNT
|
||||
is AccountCreateEditComponent.Params.Edit -> Source.EDIT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.features.account.analytics
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
sealed class WalletSettingsAccountAnalyticEvents(
|
||||
category: String = "Settings / Wallet Settings",
|
||||
event: String,
|
||||
params: Map<String, String> = emptyMap(),
|
||||
) : AnalyticsEvent(category, event, params) {
|
||||
|
||||
class AccountCreated : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Account Created",
|
||||
)
|
||||
|
||||
class AccountRecovered : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Account Recovered",
|
||||
)
|
||||
|
||||
class ArchivedAccountsScreenOpened : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Archived Accounts Screen Opened",
|
||||
)
|
||||
|
||||
class ButtonRecoverAccount : WalletSettingsAccountAnalyticEvents(
|
||||
event = "Button - Recover Account",
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.features.account.archived
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -18,6 +19,7 @@ import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase
|
|||
import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.features.account.ArchivedAccountListComponent
|
||||
import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUM
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder
|
||||
import com.tangem.features.account.archived.entity.AccountArchivedUMBuilder.Companion.toggleProgress
|
||||
|
|
@ -40,6 +42,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
private val recoverCryptoPortfolioUseCase: RecoverCryptoPortfolioUseCase,
|
||||
private val getArchivedAccountsUseCase: GetArchivedAccountsUseCase,
|
||||
private val umBuilder: AccountArchivedUMBuilder,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -52,6 +55,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
private val getArchivedAccountsJob = JobHolder()
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened())
|
||||
getArchivedAccounts()
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +100,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount())
|
||||
uiState.update { it.toggleProgress(accountId, isLoading = true) }
|
||||
val result = withContext(dispatchers.default) {
|
||||
recoverCryptoPortfolioUseCase(accountId)
|
||||
|
|
@ -120,7 +125,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
|
||||
messageSender.send(
|
||||
DialogMessage(
|
||||
title = resourceReference(R.string.account_recover_limit_dialog_title),
|
||||
title = resourceReference(R.string.common_something_went_wrong),
|
||||
message = resourceReference(
|
||||
id = R.string.account_recover_limit_dialog_description,
|
||||
formatArgs = wrappedList(AccountList.MAX_ACCOUNTS_COUNT.toString()),
|
||||
|
|
@ -148,6 +153,7 @@ internal class ArchivedAccountListModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun showSuccessRecoverMessage() {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountRecovered())
|
||||
val message = resourceReference(R.string.account_recover_success_message)
|
||||
messageSender.send(ToastMessage(message = message))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.annotation.StringRes
|
|||
import com.tangem.common.ui.account.AccountNameUM
|
||||
import com.tangem.common.ui.account.toDomain
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.core.analytics.models.ExceptionAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
|
|
@ -25,6 +26,9 @@ import com.tangem.domain.models.account.CryptoPortfolioIcon
|
|||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.features.account.AccountCreateEditComponent
|
||||
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
|
||||
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents.Companion.toAnalyticSource
|
||||
import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUM
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
|
||||
|
|
@ -54,6 +58,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
private val addCryptoPortfolioUseCase: AddCryptoPortfolioUseCase,
|
||||
private val getUnoccupiedAccountIndexUseCase: GetUnoccupiedAccountIndexUseCase,
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<AccountCreateEditComponent.Params>()
|
||||
|
|
@ -65,6 +70,8 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
init {
|
||||
if (params is AccountCreateEditComponent.Params.Create) {
|
||||
updateDerivationInfo(userWalletId = params.userWalletId)
|
||||
} else {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountEditScreenOpened())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +112,12 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val icon = state.account.portfolioIcon.toDomain()
|
||||
val index = state.account.derivationInfo.index ?: return
|
||||
val derivationIndex = DerivationIndex(value = index).getOrNull() ?: return
|
||||
val event = AccountSettingsAnalyticEvents.ButtonAddNewAccount(
|
||||
name = name,
|
||||
icon = icon,
|
||||
derivationIndex = derivationIndex.value,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
uiState.value = uiState.value.toggleProgress(showProgress = true)
|
||||
val result = addCryptoPortfolioUseCase(
|
||||
|
|
@ -118,12 +131,22 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
result
|
||||
.onLeft(::handleAddAccountError)
|
||||
.onRight {
|
||||
analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated())
|
||||
showMessage(R.string.account_create_success_message)
|
||||
router.pop()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) {
|
||||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = params.toAnalyticSource(),
|
||||
error = when (error) {
|
||||
is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag
|
||||
is AddCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty()
|
||||
},
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
|
||||
val isDuplicateAccountNamesError = (error as? AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
|
||||
?.cause is AccountList.Error.DuplicateAccountNames
|
||||
when {
|
||||
|
|
@ -141,6 +164,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
val icon = state.account.portfolioIcon.toDomain()
|
||||
val isNewName = name != params.account.accountName
|
||||
val isNewIcon = icon != params.account.portfolioIcon
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon))
|
||||
|
||||
uiState.value = uiState.value.toggleProgress(showProgress = true)
|
||||
val result = updateCryptoPortfolioUseCase(
|
||||
|
|
@ -159,6 +183,15 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) {
|
||||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = params.toAnalyticSource(),
|
||||
error = when (error) {
|
||||
is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag
|
||||
is UpdateCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty()
|
||||
UpdateCryptoPortfolioUseCase.Error.NothingToUpdate -> error::class.simpleName.orEmpty()
|
||||
},
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
val isDuplicateAccountNamesError = (error as? UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet)
|
||||
?.cause is AccountList.Error.DuplicateAccountNames
|
||||
when {
|
||||
|
|
@ -274,7 +307,7 @@ internal class AccountCreateEditModel @Inject constructor(
|
|||
private fun showSomethingWrong() {
|
||||
val dialogMessage = DialogMessage(
|
||||
title = resourceReference(R.string.common_something_went_wrong),
|
||||
message = resourceReference(R.string.account_could_not_create),
|
||||
message = resourceReference(R.string.account_generic_error_dialog_message),
|
||||
)
|
||||
messageSender.send(dialogMessage)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
package com.tangem.features.account.createedit.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -30,6 +25,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.compose.ui.util.fastForEachIndexed
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.account.*
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
|
|
@ -186,9 +183,9 @@ private fun AccountColor(colorsState: AccountCreateEditUM.Colors) {
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(contentPadding),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
|
||||
) {
|
||||
colorsState.list.forEach { color ->
|
||||
colorsState.list.fastForEach { color ->
|
||||
val isSelected = color == colorsState.selected
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
@ -236,8 +233,9 @@ private fun AccountIcons(iconsState: AccountCreateEditUM.Icons) {
|
|||
FlowRow(
|
||||
maxItemsInEachRow = 6,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
iconsState.list.forEachIndexed { index, icon ->
|
||||
iconsState.list.fastForEachIndexed { index, icon ->
|
||||
val isSelected = icon == iconsState.selected
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.account.details
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -20,6 +21,7 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.wallet.isMultiCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.account.AccountDetailsComponent
|
||||
import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents
|
||||
import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon
|
||||
import com.tangem.features.account.details.entity.AccountDetailsUM
|
||||
import com.tangem.features.account.details.entity.AccountDetailsUM.ArchiveMode
|
||||
|
|
@ -37,6 +39,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase,
|
||||
singleAccountSupplier: SingleAccountSupplier,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -48,12 +51,14 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
private val accountId = params.account.accountId
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened())
|
||||
singleAccountSupplier(SingleAccountProducer.Params(accountId))
|
||||
.onEach { account -> uiState.update { buildUI(account) } }
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
private fun onEditAccountClick(account: Account) {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit())
|
||||
router.push(AppRoute.EditAccount(account))
|
||||
}
|
||||
|
||||
|
|
@ -62,17 +67,21 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
source = AppRoute.ManageTokens.Source.SETTINGS,
|
||||
portfolioId = PortfolioId(account.accountId),
|
||||
)
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonManageTokens())
|
||||
router.push(route)
|
||||
}
|
||||
|
||||
private fun onArchiveAccountClick() {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccount())
|
||||
confirmArchiveDialog()
|
||||
}
|
||||
|
||||
private fun confirmArchiveDialog() {
|
||||
val secondAction = EventMessageAction(
|
||||
title = resourceReference(R.string.common_cancel),
|
||||
onClick = {},
|
||||
onClick = {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation())
|
||||
},
|
||||
)
|
||||
val firstAction = EventMessageAction(
|
||||
title = resourceReference(R.string.account_details_archive_action),
|
||||
|
|
@ -90,6 +99,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun archiveCryptoPortfolio() = modelScope.launch {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation())
|
||||
uiState.update { it.toggleProgress(true) }
|
||||
archiveCryptoPortfolioUseCase(accountId)
|
||||
.onLeft { error ->
|
||||
|
|
@ -97,6 +107,7 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
uiState.update { it.toggleProgress(false) }
|
||||
}
|
||||
.onRight {
|
||||
analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountArchived())
|
||||
val message = resourceReference(R.string.account_archive_success_message)
|
||||
messageSender.send(ToastMessage(message = message))
|
||||
router.pop()
|
||||
|
|
@ -104,21 +115,18 @@ internal class AccountDetailsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun failedArchiveDialog(error: ArchiveCryptoPortfolioUseCase.Error) {
|
||||
val titleRes = when (error) {
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
|
||||
-> R.string.common_something_went_wrong
|
||||
is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus,
|
||||
-> R.string.account_could_not_archive_referral_program_title
|
||||
}
|
||||
val event = AccountSettingsAnalyticEvents.AccountError(
|
||||
source = AccountSettingsAnalyticEvents.Source.ARCHIVE,
|
||||
error = error.tag,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
val titleRes = R.string.common_something_went_wrong
|
||||
val messageRes = when (error) {
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountListRequirementsNotMet,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountNotFound,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.CriticalTechError.AccountsNotCreated,
|
||||
is ArchiveCryptoPortfolioUseCase.Error.DataOperationFailed,
|
||||
-> R.string.account_could_not_archive
|
||||
-> R.string.account_generic_error_dialog_message
|
||||
is ArchiveCryptoPortfolioUseCase.Error.ActiveReferralStatus,
|
||||
-> R.string.account_could_not_archive_referral_program_message
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor(
|
|||
|
||||
private fun List<UserWallet>.filterWallets(mode: Mode): List<UserWallet> = this.filter { wallet ->
|
||||
when (mode) {
|
||||
is Mode.All -> if (mode.onlyMultiCurrency) wallet.isMultiCurrency else true
|
||||
is Mode.All -> if (mode.isOnlyMultiCurrency) wallet.isMultiCurrency else true
|
||||
is Mode.Wallet -> wallet.walletId == mode.walletId
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ internal class DefaultPortfolioSelectorController @Inject constructor(
|
|||
|
||||
override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true }
|
||||
|
||||
override suspend fun isAccountModeSync(): Boolean = isAccountsModeEnabledUseCase.invokeSync()
|
||||
|
||||
override fun selectAccount(accountId: AccountId?) {
|
||||
_selectedAccount.tryEmit(accountId)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val bottomSheetVariant: Boolean = false</ID>
|
||||
<ID>BooleanPropertyNaming:AskBiometryUM.kt$AskBiometryUM$val showProgress: Boolean = false</ID>
|
||||
<ID>BooleanPropertyNaming:DefaultAskBiometryComponent.kt$DefaultAskBiometryComponent$val bsShown by bsShown.collectAsStateWithLifecycle()</ID>
|
||||
<ID>MultilineLambdaItParameter:AskBiometryModel.kt$AskBiometryModel${ uiMessageSender.send( SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")), ) }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -41,10 +41,10 @@ internal class DefaultAskBiometryComponent @AssistedInject constructor(
|
|||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state by model.uiState.collectAsStateWithLifecycle()
|
||||
val bsShown by bsShown.collectAsStateWithLifecycle()
|
||||
val bsConfig = remember(this, bsShown) {
|
||||
val isBSShown by bsShown.collectAsStateWithLifecycle()
|
||||
val bsConfig = remember(this, isBSShown) {
|
||||
TangemBottomSheetConfig(
|
||||
isShown = bsShown,
|
||||
isShown = isBSShown,
|
||||
onDismissRequest = ::dismiss,
|
||||
content = TangemBottomSheetConfigContent.Empty,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
|
||||
private val _uiState = MutableStateFlow(
|
||||
AskBiometryUM(
|
||||
bottomSheetVariant = params.isBottomSheetVariant,
|
||||
isBottomSheetVariant = params.isBottomSheetVariant,
|
||||
onAllowClick = ::onAllowClick,
|
||||
onDontAllowClick = ::dontAllow,
|
||||
onDismiss = ::dismiss,
|
||||
|
|
@ -85,7 +85,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
|
||||
_uiState.update { it.copy(showProgress = true) }
|
||||
_uiState.update { it.copy(shouldShowProgress = true) }
|
||||
|
||||
/*
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
uiMessageSender.send(
|
||||
SnackbarMessage(stringReference("No selected user wallet")),
|
||||
)
|
||||
_uiState.update { it.copy(showProgress = false) }
|
||||
_uiState.update { it.copy(shouldShowProgress = false) }
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ internal class AskBiometryModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
if (_uiState.value.bottomSheetVariant) {
|
||||
if (_uiState.value.isBottomSheetVariant) {
|
||||
dismissBSFlow.emit(Unit)
|
||||
delay(timeMillis = 500)
|
||||
}
|
||||
|
|
@ -142,9 +142,9 @@ internal class AskBiometryModel @Inject constructor(
|
|||
userWalletId = userWallet.walletId,
|
||||
lockMethod = UserWalletsListRepository.LockMethod.Biometric,
|
||||
changeUnsecured = false,
|
||||
).onLeft {
|
||||
).onLeft { error ->
|
||||
uiMessageSender.send(
|
||||
SnackbarMessage(stringReference("Something went wrong. Please contact support: $it")),
|
||||
SnackbarMessage(stringReference("Something went wrong. Please contact support: $error")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ internal fun AskBiometry(state: AskBiometryUM, modifier: Modifier = Modifier) {
|
|||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
) {
|
||||
if (state.bottomSheetVariant) {
|
||||
if (state.isBottomSheetVariant) {
|
||||
Header(onCloseClick = state.onDismiss)
|
||||
}
|
||||
|
||||
|
|
@ -128,12 +128,12 @@ private fun Footer(state: AskBiometryUM, modifier: Modifier = Modifier) {
|
|||
) {
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showProgress = state.showProgress,
|
||||
showProgress = state.shouldShowProgress,
|
||||
text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics),
|
||||
onClick = state.onAllowClick,
|
||||
)
|
||||
|
||||
if (state.bottomSheetVariant.not()) {
|
||||
if (state.isBottomSheetVariant.not()) {
|
||||
SpacerH12()
|
||||
|
||||
SecondaryButton(
|
||||
|
|
@ -199,7 +199,7 @@ private fun Preview() {
|
|||
private fun PreviewBS() {
|
||||
TangemThemePreview {
|
||||
AskBiometry(
|
||||
state = AskBiometryUM(bottomSheetVariant = true),
|
||||
state = AskBiometryUM(isBottomSheetVariant = true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,8 @@ package com.tangem.features.biometry.impl.ui.state
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
internal data class AskBiometryUM(
|
||||
val bottomSheetVariant: Boolean = false,
|
||||
val showProgress: Boolean = false,
|
||||
val isBottomSheetVariant: Boolean = false,
|
||||
val shouldShowProgress: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val onAllowClick: () -> Unit = {},
|
||||
val onDontAllowClick: () -> Unit = {},
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.hotWallet)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
|
|||
|
|
@ -2,15 +2,20 @@ package com.tangem.features.createwalletselection
|
|||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.core.ui.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.analytics.Shop
|
||||
import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
|
||||
import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase
|
||||
import com.tangem.features.createwalletselection.entity.CreateWalletSelectionUM
|
||||
import com.tangem.features.createwalletselection.impl.R
|
||||
|
|
@ -28,9 +33,12 @@ import javax.inject.Inject
|
|||
internal class CreateWalletSelectionModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val router: Router,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val isHotWalletCreationSupported: IsHotWalletCreationSupported,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
) : Model() {
|
||||
|
||||
internal val uiState: StateFlow<CreateWalletSelectionUM>
|
||||
|
|
@ -93,7 +101,20 @@ internal class CreateWalletSelectionModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onMobileWalletClick() {
|
||||
router.push(AppRoute.CreateMobileWallet)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet(
|
||||
source = AnalyticsParam.ScreensSources.AddNewWallet.value,
|
||||
),
|
||||
)
|
||||
if (!isHotWalletCreationSupported()) {
|
||||
uiMessageSender.send(
|
||||
hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.AddNewWallet.value))
|
||||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
|
|
@ -101,8 +122,7 @@ internal class CreateWalletSelectionModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.AddNewWallet))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,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,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.hotWallet)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.configToggles)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" ?>
|
||||
<SmellBaseline>
|
||||
<ManuallySuppressedIssues/>
|
||||
<CurrentIssues>
|
||||
<ID>BooleanPropertyNaming:CreateWalletStartUM.kt$CreateWalletStartUM$val showScanSecondaryButton: Boolean</ID>
|
||||
<ID>MultilineLambdaItParameter:CreateWalletStartContent.kt${ FeatureItem( iconResId = it.iconResId, text = it.text, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CreateWalletStartModel.kt$CreateWalletStartModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { appRouter.replaceAll(AppRoute.Wallet) } } } }</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
@ -6,8 +6,9 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -18,12 +19,13 @@ import com.tangem.core.navigation.url.UrlOpener
|
|||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.analytics.ParamCardCurrencyConverter
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
|
|
@ -51,14 +53,16 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appRouter: AppRouter,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val isHotWalletCreationSupported: IsHotWalletCreationSupported,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<CreateWalletStartComponent.Params>()
|
||||
|
|
@ -84,7 +88,7 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
),
|
||||
),
|
||||
imageResId = R.drawable.img_hardware_wallet,
|
||||
showScanSecondaryButton = true,
|
||||
shouldShowScanSecondaryButton = true,
|
||||
onPrimaryButtonClick = ::onBuyClick,
|
||||
primaryButtonText = resourceReference(R.string.details_buy_wallet),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
|
|
@ -112,7 +116,7 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
),
|
||||
),
|
||||
imageResId = R.drawable.img_mobile_wallet,
|
||||
showScanSecondaryButton = false,
|
||||
shouldShowScanSecondaryButton = false,
|
||||
onPrimaryButtonClick = ::onStartWithMobileWalletClick,
|
||||
primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title),
|
||||
|
|
@ -125,15 +129,38 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
},
|
||||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(
|
||||
event = IntroductionProcess.CreateWalletIntroScreenOpened(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
analyticsEventHandler.send(
|
||||
event = IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.CreateWalletIntro),
|
||||
)
|
||||
scanCard()
|
||||
}
|
||||
|
||||
private fun onStartWithMobileWalletClick() {
|
||||
router.push(AppRoute.CreateMobileWallet)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.ButtonMobileWallet(
|
||||
source = AnalyticsParam.ScreensSources.CreateWalletIntro.value,
|
||||
),
|
||||
)
|
||||
if (!isHotWalletCreationSupported()) {
|
||||
uiMessageSender.send(
|
||||
hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
router.push(AppRoute.CreateMobileWallet(AnalyticsParam.ScreensSources.CreateWalletIntro.value))
|
||||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.CreateWalletIntro))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
|
|
@ -182,11 +209,11 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
}
|
||||
|
||||
saveWalletUseCase(userWallet = userWallet).fold(
|
||||
ifLeft = {
|
||||
ifLeft = { error ->
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
setLoading(false)
|
||||
when (it) {
|
||||
is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet")
|
||||
when (error) {
|
||||
is SaveWalletError.DataError -> Timber.e(error.toString(), "Unable to save user wallet")
|
||||
is SaveWalletError.WalletAlreadySaved -> {
|
||||
userWalletsListRepository.unlock(
|
||||
userWalletId = userWallet.walletId,
|
||||
|
|
@ -199,28 +226,11 @@ internal class CreateWalletStartModel @Inject constructor(
|
|||
},
|
||||
ifRight = {
|
||||
setLoading(false)
|
||||
sendSignedInCardAnalyticsEvent(scanResponse = scanResponse, isImported = userWallet.isImported)
|
||||
appRouter.replaceAll(AppRoute.Wallet)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse, isImported: Boolean) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
walletsCount = userWalletsListRepository.userWalletsSync().size.toString(),
|
||||
isImported = isImported,
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setLoading(isLoading: Boolean) {
|
||||
uiState.update { it.copy(isScanInProgress = isLoading) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ internal data class CreateWalletStartUM(
|
|||
val featureItems: ImmutableList<FeatureItem>,
|
||||
val imageResId: Int,
|
||||
val isScanInProgress: Boolean,
|
||||
val showScanSecondaryButton: Boolean,
|
||||
val shouldShowScanSecondaryButton: Boolean,
|
||||
val primaryButtonText: TextReference,
|
||||
val onPrimaryButtonClick: () -> Unit,
|
||||
val otherMethodDescription: TextReference,
|
||||
|
|
|
|||
|
|
@ -120,10 +120,10 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi
|
|||
horizontalArrangement = Arrangement.Center,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
state.featureItems.forEach {
|
||||
state.featureItems.forEach { item ->
|
||||
FeatureItem(
|
||||
iconResId = it.iconResId,
|
||||
text = it.text,
|
||||
iconResId = item.iconResId,
|
||||
text = item.text,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi
|
|||
)
|
||||
},
|
||||
bottomContent = {
|
||||
if (state.showScanSecondaryButton) {
|
||||
if (state.shouldShowScanSecondaryButton) {
|
||||
SecondaryButtonIconEnd(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -232,7 +232,7 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi
|
|||
minImageHeight = 160.dp,
|
||||
)
|
||||
}
|
||||
if (!state.showScanSecondaryButton) {
|
||||
if (!state.shouldShowScanSecondaryButton) {
|
||||
FlowRow(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
|
|
@ -425,7 +425,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide
|
|||
),
|
||||
),
|
||||
imageResId = R.drawable.img_hardware_wallet,
|
||||
showScanSecondaryButton = true,
|
||||
shouldShowScanSecondaryButton = true,
|
||||
onPrimaryButtonClick = { },
|
||||
primaryButtonText = resourceReference(R.string.details_buy_wallet),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
|
|
@ -455,7 +455,7 @@ private class CreateWalletStartStateProvider : CollectionPreviewParameterProvide
|
|||
),
|
||||
),
|
||||
imageResId = R.drawable.img_mobile_wallet,
|
||||
showScanSecondaryButton = false,
|
||||
shouldShowScanSecondaryButton = false,
|
||||
onPrimaryButtonClick = { },
|
||||
primaryButtonText = resourceReference(R.string.welcome_create_wallet_mobile_title),
|
||||
otherMethodTitle = resourceReference(R.string.welcome_create_wallet_use_hardware_title),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
<ID>MultilineLambdaItParameter:DetailsModel.kt$DetailsModel${ it.copy( selectFeedbackEmailTypeBSConfig = it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:PreviewUserWalletListComponent.kt$PreviewUserWalletListComponent${ it.copy( balance = UserWalletItemUM.Balance.Loaded( value = "1.000 BTC", isFlickering = true, ), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:UserWalletSaver.kt$UserWalletSaver${ val message = it.message if (!message.isNullOrEmpty()) { messageSender.send(SnackbarMessage(message)) } }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:UserWalletListModel.kt$UserWalletListModel$private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)</ID>
|
||||
<ID>RedundantSuspendModifier:UserWalletSaver.kt$UserWalletSaver$suspend</ID>
|
||||
<ID>UnnecessaryLet:ItemsBuilder.kt$ItemsBuilder$let(::add)</ID>
|
||||
</CurrentIssues>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import android.content.res.Resources
|
|||
import arrow.core.getOrElse
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.AppInstanceIdProvider
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -13,13 +16,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package com.tangem.features.details.model
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.ui.userwallet.handle
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.SignIn
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
|
|
@ -39,6 +42,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
private val userWalletSaver: UserWalletSaver,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val unlockWalletUseCase: UnlockWalletUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val isWalletSavingInProgress: MutableStateFlow<Boolean> = MutableStateFlow(value = false)
|
||||
|
|
@ -87,6 +91,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
|
||||
private fun onAddNewWalletClick() {
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.SignIn))
|
||||
router.push(AppRoute.CreateWalletSelection)
|
||||
} else {
|
||||
withProgress(isWalletSavingInProgress) {
|
||||
|
|
@ -105,6 +110,7 @@ internal class UserWalletListModel @Inject constructor(
|
|||
error.handle(
|
||||
onUserCancelled = {},
|
||||
onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) },
|
||||
analyticsEventHandler = analyticsEventHandler,
|
||||
showMessage = messageSender::send,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,7 +185,9 @@ private fun Footer(model: DetailsFooterUM, modifier: Modifier = Modifier) {
|
|||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing6)
|
||||
.testTag(DetailsScreenTestTags.VERSION_NAME),
|
||||
text = model.appVersion,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.feed.entry
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.features.feed.entry.BottomSheetState
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface FeedEntryComponent {
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.news)
|
||||
|
||||
// FIXME [REDACTED_TASK_KEY]
|
||||
// Remove the "Buy" and "Sell" actions from the redux middleware.
|
||||
|
|
@ -86,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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.features.feed.di
|
||||
|
||||
import com.tangem.core.decompose.di.ModelComponent
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.features.feed.model.feed.FeedComponentModel
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(ModelComponent::class)
|
||||
internal interface ModelModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(FeedComponentModel::class)
|
||||
fun bindsFeedComponentModel(model: FeedComponentModel): Model
|
||||
}
|
||||
|
|
@ -0,0 +1,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.tangem.features.feed.ui.feed.state
|
||||
|
||||
import com.tangem.utils.Provider
|
||||
|
||||
internal class SearchBarStateFactory(
|
||||
private val currentStateProvider: Provider<FeedListUM>,
|
||||
private val onStateUpdate: (FeedListUM) -> Unit,
|
||||
) {
|
||||
|
||||
val searchQuery: String
|
||||
get() = currentStateProvider().searchBar.query
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
val currentState = currentStateProvider()
|
||||
onStateUpdate(
|
||||
currentState.copy(
|
||||
searchBar = currentState.searchBar.copy(
|
||||
query = query,
|
||||
isActive = query.isNotEmpty(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,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(),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,9 +6,6 @@
|
|||
<ID>BooleanPropertyNaming:HomeUM.kt$HomeUM$val scanInProgress: Boolean</ID>
|
||||
<ID>MultilineLambdaItParameter:HomeModel.kt$HomeModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:StoriesProgressBar.kt${ when (index) { currentStep -> it.fillMaxWidth(progress.value) in 0 until currentStep -> it.fillMaxWidth(fraction = 1f) else -> it } }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isFirstStepLaunched = remember { mutableStateOf(false) }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isLaunched = remember { mutableStateOf(false) }</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:StoriesAnimation.kt$val isSecondStepLaunched = remember { mutableStateOf(false) }</ID>
|
||||
<ID>ReusedModifierInstance:HomeButtonsV2.kt$StoriesButton( modifier = modifier, text = stringResourceSafe(id = R.string.common_get_started), useDarkerColors = false, onClick = onGetStartedClick, )</ID>
|
||||
</CurrentIssues>
|
||||
</SmellBaseline>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import com.tangem.common.routing.AppRouter
|
|||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn
|
||||
import com.tangem.core.analytics.models.Basic.SignedIn.SignInType
|
||||
import com.tangem.core.analytics.models.Basic.SignedInLegacy
|
||||
import com.tangem.core.analytics.models.Basic.SignedInLegacy.SignInType
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -93,7 +93,7 @@ internal class HomeModel @Inject constructor(
|
|||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(IntroductionProcess.ScreenOpened)
|
||||
analyticsEventHandler.send(IntroductionProcess.ScreenOpened())
|
||||
observeUserCountryChanges()
|
||||
|
||||
when (params.launchMode) {
|
||||
|
|
@ -127,20 +127,20 @@ internal class HomeModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onScanClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard)
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCardLegacy())
|
||||
scanCard()
|
||||
}
|
||||
|
||||
private fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards)
|
||||
analyticsEventHandler.send(Shop.ScreenOpened)
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
|
||||
analyticsEventHandler.send(Shop.ScreenOpened())
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSearchTokensClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList)
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
|
||||
router.push(AppRoute.ManageTokens(Source.STORIES))
|
||||
}
|
||||
|
||||
|
|
@ -212,7 +212,7 @@ internal class HomeModel @Inject constructor(
|
|||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
if (currency != null) {
|
||||
analyticsEventHandler.send(
|
||||
SignedIn(
|
||||
SignedInLegacy(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = SignInType.Card,
|
||||
|
|
|
|||
|
|
@ -4,5 +4,9 @@ import com.tangem.core.decompose.factory.ComponentFactory
|
|||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface CreateMobileWalletComponent : ComposableContentComponent {
|
||||
interface Factory : ComponentFactory<Unit, CreateMobileWalletComponent>
|
||||
data class Params(
|
||||
val source: String,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, CreateMobileWalletComponent>
|
||||
}
|
||||
|
|
@ -11,17 +11,13 @@
|
|||
<ID>BooleanPropertyNaming:ForgetWalletUM.kt$ForgetWalletUM$val secondCheckboxChecked: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:HotAccessCodeRequestUM.kt$HotAccessCodeRequestUM$val useBiometricVisible: Boolean = true</ID>
|
||||
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showBackButton: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showFeedbackButton: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:HotWalletStepperComponent.kt$HotWalletStepperComponent.StepperUM$val showSkipButton: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonEnabled: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM$val completeButtonProgress: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:ManualBackupCheckUM.kt$ManualBackupCheckUM.WordField$val error: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:MobileWalletSetupFinishedContent.kt$var showConfetti by remember { mutableStateOf(false) }</ID>
|
||||
<ID>BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId && it.card.wallets.map { it.curve }.toSet().isNotEmpty() }</ID>
|
||||
<ID>BooleanPropertyNaming:UpgradeWalletModel.kt$UpgradeWalletModel$val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not() }</ID>
|
||||
<ID>BooleanPropertyNaming:WalletBackupUM.kt$WalletBackupUM$val backedUp: Boolean</ID>
|
||||
<ID>BooleanPropertyNaming:WalletHardwareBackupUM.kt$WalletHardwareBackupUM$val showPurchaseBlock: Boolean = false</ID>
|
||||
<ID>MaxChainedCallsOnSameLine:UpgradeWalletModel.kt$UpgradeWalletModel$it.card.wallets.map { it.curve }.toSet().isNotEmpty()</ID>
|
||||
<ID>MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ Timber.e(it) setImportProgress(false) }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddExistingWalletImportModel.kt$AddExistingWalletImportModel${ setImportProgress(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { uiMessageSender.send( SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), ) } } }</ID>
|
||||
<ID>MultilineLambdaItParameter:CreateHardwareWalletModel.kt$CreateHardwareWalletModel${ delay(HIDE_PROGRESS_DELAY) setLoading(false) when (it) { is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, unlockMethod = UserWalletsListRepository.UnlockMethod.Scan(scanResponse), ).onRight { router.replaceAll(AppRoute.Wallet) } } } }</ID>
|
||||
|
|
@ -48,14 +44,12 @@
|
|||
<ID>MultilineLambdaItParameter:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.filterIndexed { index, _ -> WORD_FIELD_INDICES.contains(index + 1) }.toImmutableList(), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManualBackupPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ManualBackupPhraseModel.kt$ManualBackupPhraseModel${ it.copy( words = seedPhrasePrivateInfo.mnemonic.mnemonicComponents.mapIndexed { index, s -> EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:UpgradeWalletModel.kt$UpgradeWalletModel${ // Check if user attempted to upgrade before but something went wrong and a full reset is required val userWallet = coldUserWalletBuilderFactory.create(it).build() val sameWalletButNotFinishedBackup by lazy { userWallet?.walletId == params.userWalletId && BackupValidator.isValidFull(it.card).not() } val otherWalletAndAlreadyCreated by lazy { userWallet?.walletId != params.userWalletId && it.card.wallets.map { it.curve }.toSet().isNotEmpty() } if (userWallet != null && (sameWalletButNotFinishedBackup || otherWalletAndAlreadyCreated)) { startResetCardsFlow.emit(userWallet) return@doOnSuccess } delay(DELAY_SDK_DIALOG_CLOSE) tangemSdkManager.changeDisplayedCardIdNumbersCount(it) navigateToUpgradeFlow(it) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ViewPhraseContent.kt${ EnumeratedTwoColumnGridItem( index = it + 1, mnemonic = "word${it + 1}", ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ViewPhraseModel.kt$ViewPhraseModel${ it.copy( words = words.mapIndexed { index, s -> EnumeratedTwoColumnGridItem(index + 1, s) }.toImmutableList(), ) }</ID>
|
||||
<ID>NoNameShadowing:ManualBackupCheckModel.kt$ManualBackupCheckModel${ it.copy(completeButtonProgress = false) }</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$uiState</ID>
|
||||
<ID>ReusedModifierInstance:AddExistingWalletImportContent.kt$OutlineTextFieldWithIcon( modifier = modifier .padding(horizontal = 16.dp) .fillMaxWidth(), value = state.passPhrase, onValueChange = state.passPhraseChange, iconResId = R.drawable.ic_information_24, iconColor = TangemTheme.colors.icon.informative, label = stringResourceSafe(id = R.string.common_passphrase), placeholder = stringResourceSafe(id = R.string.send_optional_field), onIconClick = state.onPassphraseInfoClick, keyboardOptions = KeyboardOptions( autoCorrectEnabled = false, keyboardType = KeyboardType.Password, ), )</ID>
|
||||
<ID>ReusedModifierInstance:HotAccessCodeRequestFullScreenContent.kt$AnimatedVisibility( modifier = modifier, visible = state.isShown, enter = fadeIn(), exit = fadeOut(), ) { Column( Modifier .fillMaxSize() .background(TangemTheme.colors.background.primary), horizontalAlignment = Alignment.CenterHorizontally, ) { TangemTopAppBar( modifier = Modifier.statusBarsPadding(), startButton = TopAppBarButtonUM.Back(state.onDismiss), ) SpacerH(68.dp) Column( Modifier .weight(1f) .fillMaxWidth() .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), text = stringResourceSafe(R.string.access_code_check_title), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) SpacerH24() PinTextField( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), length = 6, isPasswordVisual = true, value = state.accessCode, pinTextColor = state.accessCodeColor, onValueChange = state.onAccessCodeChange, ) SpacerH(20.dp) AnimatedVisibility( modifier = Modifier.animateEnterExit( enter = slideInVertically( tween(), initialOffsetY = { it + 200 }, ) + fadeIn(tween()), exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()), ), visible = state.wrongAccessCodeText != null, enter = fadeIn(), exit = fadeOut(), ) { val wrongAccessCodeText = state.wrongAccessCodeText ?: return@AnimatedVisibility Text( text = wrongAccessCodeText.resolveReference(), textAlign = TextAlign.Center, style = TangemTheme.typography.caption2.copy( lineBreak = LineBreak.Heading, ), color = TangemTheme.colors.text.warning, ) } } AnimatedVisibility( visible = state.useBiometricVisible, enter = fadeIn(), exit = fadeOut(), ) { SecondaryButton( modifier = Modifier .padding(16.dp) .fillMaxWidth() .navigationBarsPadding() .imePadding(), text = stringResourceSafe( id = R.string.welcome_unlock, stringResourceSafe(R.string.common_biometrics), ), onClick = state.useBiometricClick, ) } } }</ID>
|
||||
<ID>ReusedModifierInstance:HotWalletStepper.kt$TangemTopAppBar( startButton = if (state.showBackButton) { TopAppBarButtonUM.Back(onBackClick) } else { null }, endButton = when { state.showSkipButton -> TopAppBarButtonUM.Text( text = resourceReference(R.string.common_skip), onClicked = onSkipClick, ) state.showFeedbackButton -> TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_chat_24, onClicked = onFeedbackClick, ) else -> null }, title = state.title, containerColor = TangemTheme.colors.background.primary, modifier = modifier, titleAlignment = Alignment.CenterHorizontally, )</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:AddExistingWalletImportModel.kt$AddExistingWalletImportModel$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:ManualBackupCheckModel.kt$ManualBackupCheckModel$runCatching</ID>
|
||||
<ID>SuspendFunSwallowedCancellation:ManualBackupPhraseModel.kt$ManualBackupPhraseModel$runCatching</ID>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.features.hotwallet.accesscoderequest
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.event.SignIn
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.ui.components.fields.PinTextColor
|
||||
|
|
@ -33,6 +35,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val result = MutableStateFlow<HotWalletPasswordRequester.Result?>(null)
|
||||
|
|
@ -108,6 +111,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
onAccessCodeChange = ::onAccessCodeChange,
|
||||
accessCode = "",
|
||||
useBiometricClick = {
|
||||
analyticsEventHandler.send(SignIn.ButtonBiometricSignIn())
|
||||
dismissState()
|
||||
result.value = HotWalletPasswordRequester.Result.UseBiometry
|
||||
},
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.tangem.features.hotwallet.stepper.api.HotWalletStepperComponent
|
|||
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks
|
||||
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
|
@ -110,7 +111,7 @@ internal class AddExistingWalletModel @Inject constructor(
|
|||
title = resourceReference(R.string.access_code_alert_skip_ok),
|
||||
onClick = {
|
||||
if (userWalletId != null) {
|
||||
modelScope.launch {
|
||||
modelScope.launch(NonCancellable) {
|
||||
setAccessCodeSkippedUseCase(userWalletId, true)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.wallet_import_seed_navtitle),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.BackupCompleted -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -24,7 +23,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.wallet_import_title),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -33,7 +31,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = false,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -42,7 +39,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = true,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.PushNotifications -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -51,7 +47,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.onboarding_title_notifications),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
|
||||
is AddExistingWalletRoute.SetupFinished -> HotWalletStepperComponent.StepperUM(
|
||||
|
|
@ -60,7 +55,6 @@ internal class AddExistingWalletStepperStateManager {
|
|||
title = resourceReference(R.string.common_done),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.tangem.features.hotwallet.addexistingwallet.im.port.model
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.decompose.di.GlobalUiMessageSender
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -37,6 +40,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
|
||||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: AddExistingWalletImportComponent.Params = paramsContainer.require()
|
||||
|
|
@ -60,6 +64,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
}
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ImportSeedPhraseScreenOpened())
|
||||
importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder(
|
||||
modelScope = modelScope,
|
||||
mnemonicRepository = mnemonicRepository,
|
||||
|
|
@ -72,6 +77,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
)
|
||||
},
|
||||
onPassphraseInfoClick = ::onPassphraseInfoClick,
|
||||
onImportClick = { analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImport()) },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -101,6 +107,23 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
}
|
||||
.onRight {
|
||||
setImportProgress(false)
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.Finished(
|
||||
source = AnalyticsParam.ScreensSources.ImportWallet.value,
|
||||
),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.CreateWallet.WalletCreatedSuccessfully(
|
||||
source = AnalyticsParam.ScreensSources.ImportWallet.value,
|
||||
creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.SeedImport,
|
||||
seedPhraseLength = mnemonic.mnemonicComponents.size,
|
||||
passPhraseState = if (passphrase.isNullOrBlank()) {
|
||||
AnalyticsParam.EmptyFull.Empty
|
||||
} else {
|
||||
AnalyticsParam.EmptyFull.Full
|
||||
},
|
||||
),
|
||||
)
|
||||
params.callbacks.onWalletImported(userWallet.walletId)
|
||||
}
|
||||
}.onFailure {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class ImportSeedPhraseUiStateBuilder(
|
||||
private val modelScope: CoroutineScope,
|
||||
private val mnemonicRepository: MnemonicRepository,
|
||||
|
|
@ -24,6 +25,7 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit,
|
||||
private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit,
|
||||
private val onPassphraseInfoClick: () -> Unit,
|
||||
private val onImportClick: () -> Unit,
|
||||
) {
|
||||
private val wordsCheckJobHolder = JobHolder()
|
||||
private var importedMnemonic: Mnemonic? = null
|
||||
|
|
@ -57,6 +59,7 @@ internal class ImportSeedPhraseUiStateBuilder(
|
|||
}
|
||||
|
||||
private fun onCreateWallet() {
|
||||
onImportClick()
|
||||
val mnemonic = importedMnemonic ?: return
|
||||
val passphrase = passphrase?.takeIf { it.isNotEmpty() }
|
||||
importWallet(mnemonic, passphrase)
|
||||
|
|
|
|||
|
|
@ -1,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,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
package com.tangem.features.hotwallet.createmobilewallet
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.decompose.ui.UiMessageSender
|
||||
import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog
|
||||
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
|
||||
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.domain.wallets.usecase.SyncWalletWithRemoteUseCase
|
||||
import com.tangem.features.hotwallet.CreateMobileWalletComponent
|
||||
import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
|
|
@ -25,6 +33,7 @@ import javax.inject.Inject
|
|||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class CreateMobileWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
|
||||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
|
|
@ -32,8 +41,13 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val isHotWalletCreationSupported: IsHotWalletCreationSupported,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: CreateMobileWalletComponent.Params = paramsContainer.require()
|
||||
|
||||
internal val uiState: StateFlow<CreateMobileWalletUM>
|
||||
field = MutableStateFlow(
|
||||
CreateMobileWalletUM(
|
||||
|
|
@ -46,6 +60,12 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
|
||||
init {
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.Started(source = params.source),
|
||||
)
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.SeedPhrase.CreateMobileScreenOpened(source = params.source),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
|
@ -54,10 +74,15 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onImportClick() {
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.SeedPhrase.ButtonImportWallet())
|
||||
checkHotWalletCreationSupported(notSupported = { return })
|
||||
router.push(AppRoute.AddExistingWallet)
|
||||
}
|
||||
|
||||
private fun onCreateClick() {
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.CreateWallet.ButtonCreateWallet())
|
||||
checkHotWalletCreationSupported(notSupported = { return })
|
||||
|
||||
modelScope.launch {
|
||||
uiState.update {
|
||||
it.copy(createButtonLoading = true)
|
||||
|
|
@ -70,9 +95,20 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
|
||||
saveUserWalletUseCase(userWallet)
|
||||
|
||||
launch(NonCancellable) {
|
||||
analyticsEventHandler.send(OnboardingAnalyticsEvent.Onboarding.Finished(source = params.source))
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.CreateWallet.WalletCreatedSuccessfully(
|
||||
source = params.source,
|
||||
creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.NewSeed,
|
||||
seedPhraseLength = SEED_PHRASE_LENGTH,
|
||||
passPhraseState = AnalyticsParam.EmptyFull.Empty,
|
||||
),
|
||||
)
|
||||
|
||||
launch(dispatchers.main + NonCancellable) {
|
||||
syncWalletWithRemoteUseCase(userWalletId = userWallet.walletId)
|
||||
}
|
||||
|
||||
router.replaceAll(AppRoute.Wallet)
|
||||
}.onFailure { throwable ->
|
||||
Timber.e(throwable)
|
||||
|
|
@ -81,4 +117,17 @@ internal class CreateMobileWalletModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun checkHotWalletCreationSupported(notSupported: () -> Unit) {
|
||||
if (!isHotWalletCreationSupported()) {
|
||||
uiMessageSender.send(
|
||||
hotWalletCreationNotSupportedDialog(isHotWalletCreationSupported.getLeastVersionName()),
|
||||
)
|
||||
notSupported()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEED_PHRASE_LENGTH = 12
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ import dagger.assisted.AssistedInject
|
|||
@Suppress("UnusedPrivateMember")
|
||||
internal class DefaultCreateMobileWalletComponent @AssistedInject constructor(
|
||||
@Assisted private val context: AppComponentContext,
|
||||
@Assisted private val params: Unit,
|
||||
@Assisted private val params: CreateMobileWalletComponent.Params,
|
||||
) : CreateMobileWalletComponent, AppComponentContext by context {
|
||||
|
||||
private val model: CreateMobileWalletModel = getOrCreateModel(params)
|
||||
|
|
@ -31,6 +31,9 @@ internal class DefaultCreateMobileWalletComponent @AssistedInject constructor(
|
|||
|
||||
@AssistedFactory
|
||||
interface Factory : CreateMobileWalletComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Unit): DefaultCreateMobileWalletComponent
|
||||
override fun create(
|
||||
context: AppComponentContext,
|
||||
params: CreateMobileWalletComponent.Params,
|
||||
): DefaultCreateMobileWalletComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -3,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),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ interface HotWalletStepperComponent : ComposableContentComponent {
|
|||
val title: TextReference,
|
||||
val showBackButton: Boolean,
|
||||
val showSkipButton: Boolean,
|
||||
val showFeedbackButton: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
fun initialState() = StepperUM(
|
||||
|
|
@ -23,7 +22,6 @@ interface HotWalletStepperComponent : ComposableContentComponent {
|
|||
title = TextReference.EMPTY,
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ internal class DefaultHotWalletStepperComponent @AssistedInject constructor(
|
|||
modifier = modifier,
|
||||
onBackClick = model::onBackClick,
|
||||
onSkipClick = model::onSkipClick,
|
||||
onFeedbackClick = model::onFeedbackClick,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,9 +34,4 @@ internal class HotWalletStepperModel @Inject constructor(
|
|||
// TODO send analytics
|
||||
params.callback.onSkipClick()
|
||||
}
|
||||
|
||||
fun onFeedbackClick() {
|
||||
// TODO send analytics
|
||||
// openFeedback()
|
||||
}
|
||||
}
|
||||
|
|
@ -31,7 +31,6 @@ internal fun HotWalletStepper(
|
|||
state: HotWalletStepperComponent.StepperUM,
|
||||
onBackClick: () -> Unit,
|
||||
onSkipClick: () -> Unit,
|
||||
onFeedbackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1)
|
||||
|
|
@ -47,20 +46,16 @@ internal fun HotWalletStepper(
|
|||
} else {
|
||||
null
|
||||
},
|
||||
endButton = when {
|
||||
state.showSkipButton -> TopAppBarButtonUM.Text(
|
||||
endButton = if (state.showSkipButton) {
|
||||
TopAppBarButtonUM.Text(
|
||||
text = resourceReference(R.string.common_skip),
|
||||
onClicked = onSkipClick,
|
||||
)
|
||||
state.showFeedbackButton -> TopAppBarButtonUM.Icon(
|
||||
iconRes = R.drawable.ic_chat_24,
|
||||
onClicked = onFeedbackClick,
|
||||
)
|
||||
else -> null
|
||||
} else {
|
||||
null
|
||||
},
|
||||
title = state.title,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
modifier = modifier,
|
||||
titleAlignment = Alignment.CenterHorizontally,
|
||||
)
|
||||
|
||||
|
|
@ -95,11 +90,9 @@ private fun HotWalletStepper_Preview() {
|
|||
title = resourceReference(R.string.common_done),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
),
|
||||
onBackClick = {},
|
||||
onSkipClick = {},
|
||||
onFeedbackClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ internal class DefaultUpgradeWalletComponent @AssistedInject constructor(
|
|||
private val resetCardsComponent = resetCardsComponentFactory.create(
|
||||
context = child("ResetCardsComponent"),
|
||||
params = ResetCardsComponent.Params(
|
||||
source = ResetCardsComponent.Params.Source.Upgrade,
|
||||
callbacks = model.resetCardsComponentCallbacks,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import com.tangem.common.doOnResult
|
|||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -18,7 +21,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.toWrappedList
|
||||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.domain.card.BackupValidator
|
||||
import com.tangem.domain.card.analytics.IntroductionProcess
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
|
|
@ -60,6 +63,7 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
private val params = paramsContainer.require<UpgradeWalletComponent.Params>()
|
||||
|
|
@ -77,22 +81,26 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareUpgradeScreenOpened)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareUpgradeScreenOpened())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
trackingContextProxy.removeContext()
|
||||
clearHotWalletContextualUnlockUseCase.invoke(params.userWalletId)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun onBuyTangemWalletClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Upgrade))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onContinueClick() {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade)
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard(AnalyticsParam.ScreensSources.Upgrade))
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonStartUpgrade())
|
||||
scanCard()
|
||||
}
|
||||
|
||||
|
|
@ -131,12 +139,9 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
scanResponse: ScanResponse,
|
||||
onSuccess: suspend () -> Unit,
|
||||
) {
|
||||
// Check if user attempted to upgrade before but something went wrong and a full reset is required
|
||||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build()
|
||||
val isSameWalletButNotFinishedBackup = userWallet?.walletId == params.userWalletId &&
|
||||
BackupValidator.isValidFull(scanResponse.card).not()
|
||||
|
||||
if (userWallet != null && isSameWalletButNotFinishedBackup) {
|
||||
if (userWallet?.walletId == params.userWalletId) {
|
||||
startResetCardsFlow.emit(userWallet)
|
||||
return
|
||||
}
|
||||
|
|
@ -201,7 +206,7 @@ internal class UpgradeWalletModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onComplete() {
|
||||
setLoading(false)
|
||||
scanCard()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is WalletActivationRoute.ManualBackupPhrase -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_BACKUP_PHRASE,
|
||||
|
|
@ -24,7 +23,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is WalletActivationRoute.ManualBackupCheck -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_BACKUP_CHECK,
|
||||
|
|
@ -32,7 +30,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = true,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = true,
|
||||
)
|
||||
is WalletActivationRoute.ManualBackupCompleted -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_BACKUP_COMPLETED,
|
||||
|
|
@ -40,7 +37,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_backup),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
is WalletActivationRoute.SetAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_ACCESS_CODE,
|
||||
|
|
@ -48,7 +44,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = false,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
is WalletActivationRoute.ConfirmAccessCode -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_ACCESS_CODE,
|
||||
|
|
@ -56,7 +51,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.access_code_navtitle),
|
||||
showBackButton = true,
|
||||
showSkipButton = true,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
is WalletActivationRoute.PushNotifications -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_NOTIFICATIONS,
|
||||
|
|
@ -64,7 +58,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.onboarding_title_notifications),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
is WalletActivationRoute.SetupFinished -> HotWalletStepperComponent.StepperUM(
|
||||
currentStep = STEP_DONE,
|
||||
|
|
@ -72,7 +65,6 @@ internal class WalletActivationStepperStateManager @Inject constructor() {
|
|||
title = resourceReference(R.string.common_done),
|
||||
showBackButton = false,
|
||||
showSkipButton = false,
|
||||
showFeedbackButton = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.features.hotwallet.walletbackup.model
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -24,6 +26,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
|||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
|
|
@ -36,11 +39,14 @@ internal class WalletBackupModel @Inject constructor(
|
|||
private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val router: Router,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
private val params: WalletBackupComponent.Params = paramsContainer.require()
|
||||
|
||||
private val isScreenOpenedEventSent: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
val uiState: StateFlow<WalletBackupUM>
|
||||
field = MutableStateFlow(
|
||||
WalletBackupUM(
|
||||
|
|
@ -67,20 +73,33 @@ internal class WalletBackupModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.BackupScreenOpened(isManualBackupEnabled = true))
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
getUserWalletUseCase.invokeFlow(params.userWalletId)
|
||||
.onEach { either ->
|
||||
either.fold(
|
||||
ifLeft = {
|
||||
Timber.e("Error on getting user wallet: $it")
|
||||
},
|
||||
ifRight = {
|
||||
updateBackupStatuses(it)
|
||||
ifRight = { userWallet ->
|
||||
if (!isScreenOpenedEventSent.get() && userWallet is UserWallet.Hot) {
|
||||
analyticsEventHandler.send(
|
||||
event = WalletSettingsAnalyticEvents.BackupScreenOpened(
|
||||
isBackedUp = userWallet.backedUp,
|
||||
),
|
||||
)
|
||||
isScreenOpenedEventSent.set(true)
|
||||
}
|
||||
updateBackupStatuses(userWallet)
|
||||
},
|
||||
)
|
||||
}.launchIn(modelScope)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
trackingContextProxy.removeContext()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun updateBackupStatuses(userWallet: UserWallet) {
|
||||
uiState.update { currentState ->
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
|
|
@ -111,13 +130,14 @@ internal class WalletBackupModel @Inject constructor(
|
|||
)
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Backup))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRecoveryPhraseClick() {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase)
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonRecoveryPhrase())
|
||||
if (uiState.value.backedUp) {
|
||||
getUserWalletUseCase.invoke(params.userWalletId)
|
||||
.fold(
|
||||
|
|
@ -160,7 +180,7 @@ internal class WalletBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onHardwareWalletClick() {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate)
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonHardwareUpdate())
|
||||
router.push(AppRoute.WalletHardwareBackup(params.userWalletId))
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import arrow.core.getOrElse
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.analytics.utils.TrackingContextProxy
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
|
|
@ -49,6 +51,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
private val urlOpener: UrlOpener,
|
||||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val messageSender: UiMessageSender,
|
||||
private val trackingContextProxy: TrackingContextProxy,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
) : Model() {
|
||||
|
||||
|
|
@ -114,10 +117,16 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened)
|
||||
trackingContextProxy.addHotWalletContext()
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.HardwareBackupScreenOpened())
|
||||
showPurchaseBlockWithDelay()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
trackingContextProxy.removeContext()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun showPurchaseBlockWithDelay() {
|
||||
modelScope.launch {
|
||||
delay(SHOW_PURCHASE_BLOCK_DELAY)
|
||||
|
|
@ -126,7 +135,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onCreateNewWalletClick() {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet)
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonCreateNewWallet())
|
||||
router.push(AppRoute.CreateHardwareWallet)
|
||||
}
|
||||
|
||||
|
|
@ -134,7 +143,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
val userWallet = getUserWalletUseCase.invoke(params.userWalletId)
|
||||
.getOrElse { error("Cannot find user wallet with id: ${params.userWalletId.stringValue}") }
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonUpgradeCurrent)
|
||||
analyticsEventHandler.send(WalletSettingsAnalyticEvents.ButtonUpgradeCurrent())
|
||||
if (!userWallet.backedUp) {
|
||||
messageSender.send(makeBackupAtFirstAlertBS)
|
||||
} else {
|
||||
|
|
@ -160,6 +169,7 @@ internal class WalletHardwareBackupModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun onBuyClick() {
|
||||
analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.HardwareWallet))
|
||||
modelScope.launch {
|
||||
generateBuyTangemCardLinkUseCase.invoke().let { urlOpener.openUrl(it) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
<ID>MultilineLambdaItParameter:ChooseManagedTokenContent.kt${ add( CurrencyItemUM.Basic( id = ManagedCryptoCurrency.ID( value = "ID+$it", ), name = "Bitcoin", symbol = "BTC", icon = CurrencyIconState.Loading, networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, onExpandClick = {}, ), ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:ChooseManagedTokensModel.kt$ChooseManagedTokensModel${ it.copy( notificationUM = null, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CurrencyItemMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = false, onSelectedStateChange = { _, _ -> }, onLongTap = { _ -> }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CurrencyNetworksMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = isItemsEditable, onSelectedStateChange = onSelectedStateChange, onLongTap = onLongTap, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy( error = when (exception) { CustomTokenFormValidationException.ContractAddress.Invalid -> { resourceReference(R.string.custom_token_creation_error_invalid_contract_address) } }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy( error = when (exception) { is CustomTokenFormValidationException.Decimals.Empty -> { null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -> { resourceReference( R.string.custom_token_creation_error_wrong_decimals, wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), ) } }, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", error = stringReference("Contract address is invalid"), placeholder = stringReference("0x1234567890"), ) }</ID>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,19 @@ internal sealed class CustomTokenAnalyticsEvent(
|
|||
),
|
||||
)
|
||||
|
||||
class AddTokenToAnotherAccount(
|
||||
currencySymbol: String,
|
||||
derivationPath: String,
|
||||
source: ManageTokensSource,
|
||||
) : CustomTokenAnalyticsEvent(
|
||||
event = "Button - Add Token To Another Account",
|
||||
params = mapOf(
|
||||
AnalyticsParam.Key.TOKEN_PARAM to currencySymbol,
|
||||
AnalyticsParam.Key.DERIVATION to derivationPath,
|
||||
AnalyticsParam.Key.SOURCE to source.analyticsName,
|
||||
),
|
||||
)
|
||||
|
||||
class NetworkSelected(networkName: String, source: ManageTokensSource) : CustomTokenAnalyticsEvent(
|
||||
event = "Custom Token Network Selected",
|
||||
params = mapOf(
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ internal sealed class ManageTokensAnalyticEvent(
|
|||
AnalyticsParam.Key.SOURCE to source.analyticsName,
|
||||
),
|
||||
)
|
||||
data object ButtonLater : ManageTokensAnalyticEvent(
|
||||
class ButtonLater : ManageTokensAnalyticEvent(
|
||||
event = "Button - Later",
|
||||
params = mapOf(AnalyticsParam.SOURCE to ManageTokensSource.ONBOARDING.analyticsName),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.features.managetokens.component
|
|||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
|
||||
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork
|
||||
|
|
@ -16,7 +17,7 @@ internal interface CustomTokenFormComponent : ComposableContentComponent {
|
|||
val source: ManageTokensSource,
|
||||
val onSelectNetworkClick: (CustomTokenFormValues) -> Unit,
|
||||
val onSelectDerivationPathClick: (CustomTokenFormValues) -> Unit,
|
||||
val onCurrencyAdded: () -> Unit,
|
||||
val onCurrencyAdded: (currency: CryptoCurrency) -> Unit,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, CustomTokenFormComponent>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.core.decompose.context.childByContext
|
|||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
|
||||
import com.tangem.features.managetokens.component.AddCustomTokenComponent
|
||||
|
|
@ -232,7 +233,16 @@ internal class DefaultAddCustomTokenComponent @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun dismissAndNotify() {
|
||||
private fun dismissAndNotify(currency: CryptoCurrency) {
|
||||
val account = addedToAccount
|
||||
if (account is Account.CryptoPortfolio && !account.isMainAccount) {
|
||||
val event = CustomTokenAnalyticsEvent.AddTokenToAnotherAccount(
|
||||
currencySymbol = currency.symbol,
|
||||
derivationPath = currency.network.derivationPath.value.orEmpty(),
|
||||
source = params.source,
|
||||
)
|
||||
analyticsEventHandler.send(event)
|
||||
}
|
||||
params.onCurrencyAdded(addedToAccount)
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -364,7 +364,7 @@ internal class CustomTokenFormModel @Inject constructor(
|
|||
return@resource
|
||||
}
|
||||
|
||||
params.onCurrencyAdded()
|
||||
params.onCurrencyAdded(currency)
|
||||
}
|
||||
|
||||
private fun selectNetwork() {
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ internal class OnboardingManageTokensModel @Inject constructor(
|
|||
}
|
||||
},
|
||||
) {
|
||||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater)
|
||||
analyticsEventHandler.send(ManageTokensAnalyticEvent.ButtonLater())
|
||||
|
||||
useCasesFacade.saveManagedTokensUseCase(
|
||||
currenciesToAdd = manageTokensListManager.currenciesToAdd.value,
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Stable
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.features.markets.entry
|
||||
|
||||
enum class BottomSheetState {
|
||||
EXPANDED,
|
||||
COLLAPSED,
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface MarketsEntryComponent {
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.features.markets.entry.BottomSheetState
|
||||
|
||||
@Stable
|
||||
interface MarketsTokenListComponent : ComposableContentComponent {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
<ID>MultilineLambdaItParameter:AddToPortfolioBSContentUMFactory.kt$AddToPortfolioBSContentUMFactory${ if (it != selectedWalletId) { onAnotherWalletSelect(it) onWalletSelectorVisibilityChange(false) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioBottomSheet.kt${ Content( modifier = Modifier.fillMaxWidth(), state = it, ) WalletSelectorBottomSheet(it.walletSelectorConfig) }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioManager.kt$AddToPortfolioManager${ it.toMutableMap().apply { this[userWalletId] = if (isAddAction) { this[userWalletId].orEmpty() + network } else { this[userWalletId].orEmpty() - network } } }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ PortfolioData.CryptoCurrencyData( userWallet = selectedPortfolio.userWallet, status = addedToken, actions = it.states, ) }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ Timber.e(it) params.callback.onDismiss() }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddToPortfolioModel.kt$AddToPortfolioModel${ tokenActionsData.emit(it) navigation.replaceAll(AddToPortfolioRoutes.TokenActions) }</ID>
|
||||
<ID>MultilineLambdaItParameter:AddTokenModel.kt$AddTokenModel${ processError(error = it) uiState.value = um.toggleProgress(false) return@launch }</ID>
|
||||
|
|
@ -58,7 +57,6 @@
|
|||
<ID>MultilineLambdaItParameter:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager${ when (val status = it.status) { is PaginationStatus.Paginating -> { if (status.lastResult is BatchFetchResult.Success) { it.data.size == 1 } else { null } } is PaginationStatus.EndOfPagination -> { it.data.size == 1 } else -> null } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListItem.kt${ if (Random.nextBoolean()) { it.first.inc() to PriceChangeType.UP } else { it.first.dec() to PriceChangeType.DOWN } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListLazyColumn.kt${ (it.key as? String)?.split(TOKEN_LAZY_LIST_ID_SEPARATOR)?.first() ?.let { rawId -> CryptoCurrency.RawID(rawId) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it == BottomSheetState.EXPANDED) { analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it) { analyticsEventHandler.send(MarketsListAnalyticsEvent.TokenSearched(tokenFound = false)) } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it.isNotEmpty()) { activeListManager.getBatchKeysByItemIds(visibleItemIds.value) } else { null } }</ID>
|
||||
<ID>MultilineLambdaItParameter:MarketsListModel.kt$MarketsListModel${ if (it.list !is ListUM.Content) { visibleItemIds.value = emptyList() } }</ID>
|
||||
|
|
@ -118,12 +116,6 @@
|
|||
<ID>NoNameShadowing:MarketsTokenDetailsModel.kt$MarketsTokenDetailsModel${ it.copy( chartState = it.chartState.copy( status = MarketsTokenDetailsUM.ChartState.Status.ERROR, ), body = if (it.body is MarketsTokenDetailsUM.Body.Error) { MarketsTokenDetailsUM.Body.Nothing } else { it.body }, ) }</ID>
|
||||
<ID>NoNameShadowing:MyPortfolioUMFactory.kt$MyPortfolioUMFactory${ networkIds.contains(it.status.currency.network.backendId) }</ID>
|
||||
<ID>NoNameShadowing:NewMarketsPortfolioDelegate.kt$NewMarketsPortfolioDelegate$portfolio</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager$val isInInitialLoadingErrorState = batchFlow.state .map { it.status is PaginationStatus.InitialLoadingError } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, initialValue = false, )</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsListBatchFlowManager.kt$MarketsListBatchFlowManager$val isSearchNotFoundState = batchFlow.state .map { currentSearchText().isNullOrEmpty().not() && it.status is PaginationStatus.EndOfPagination && it.data.isEmpty() } .distinctUntilChanged() .stateIn( scope = modelScope, started = SharingStarted.Eagerly, initialValue = false, )</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsListModel.kt$MarketsListModel$val isVisibleOnScreen = MutableStateFlow(false)</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsListUMStateManager.kt$MarketsListUMStateManager$val isInSearchStateFlow = state.map { it.searchBar.isActive }.distinctUntilChanged()</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:MarketsTokenDetailsModel.kt$MarketsTokenDetailsModel$val isVisibleOnScreen = MutableStateFlow(false)</ID>
|
||||
<ID>NonBooleanPropertyPrefixedWithIs:TokenActionsHandler.kt$TokenActionsHandler$private val isDemoCardUseCase: IsDemoCardUseCase</ID>
|
||||
<ID>NullableToStringCall:MarketsListItemUM.kt$MarketsListItemUM$marketCap.toString()</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:MarketsListModel.kt$MarketsListModel$activeListManager</ID>
|
||||
<ID>PropertyUsedBeforeDeclaration:MarketsListUMStateManager.kt$MarketsListUMStateManager$state</ID>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 don’t 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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
18
features/news/news-details/api/build.gradle.kts
Normal file
18
features/news/news-details/api/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
43
features/news/news-details/impl/build.gradle.kts
Normal file
43
features/news/news-details/impl/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue