Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-24 22:35:58 +08:00
parent e3c0b0b311
commit fcdfe622f9
27 changed files with 384 additions and 54 deletions

View file

@ -28,7 +28,9 @@ dependencies {
implementation(projects.domain.card)
implementation(project(":domain:wallets"))
implementation(project(":domain:wallets:models"))
implementation(projects.domain.settings)
implementation(projects.domain.tokens)
implementation(project(":common"))
implementation(project(":core:analytics"))
implementation(projects.core.analytics.models)
@ -42,6 +44,7 @@ dependencies {
implementation(project(":libs:auth"))
implementation(project(":data:source:preferences"))
implementation(projects.data.card)
implementation(projects.data.settings)
implementation(projects.data.tokens)
implementation(projects.data.common)

View file

@ -1,9 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.card.GetAccessCodeSavingStatusUseCase
import com.tangem.domain.card.GetBiometricsStatusUseCase
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.SetAccessCodeRequestPolicyUseCase
import com.tangem.domain.card.*
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
import dagger.Module
@ -43,4 +41,10 @@ internal object CardDomainModule {
): GetAccessCodeSavingStatusUseCase {
return GetAccessCodeSavingStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository)
}
@Provides
@Singleton
fun provideGetCardWasScannedUseCase(cardRepository: CardRepository): GetCardWasScannedUseCase {
return GetCardWasScannedUseCase(cardRepository = cardRepository)
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.di.domain
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
internal object SettingsDomainModule {
@Provides
@ViewModelScoped
fun providesGetWalletsUseCase(settingsRepository: SettingsRepository): IsUserAlreadyRateAppUseCase {
return IsUserAlreadyRateAppUseCase(settingsRepository = settingsRepository)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.common.di.domain.wallets
package com.tangem.tap.di.domain
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
@ -11,7 +11,7 @@ import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
object WalletsDomainModule {
internal object WalletsDomainModule {
@Provides
@ViewModelScoped

View file

@ -13,10 +13,14 @@ android {
dependencies {
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
implementation(deps.tangem.card.android)
implementation(deps.tangem.card.core)
implementation(projects.core.utils)
implementation(projects.data.source.preferences)
implementation(projects.domain.card)
implementation(projects.domain.models)
}

View file

@ -0,0 +1,16 @@
package com.tangem.data.card
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.card.repository.CardRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultCardRepository(
private val preferencesDataSource: PreferencesDataSource,
private val dispatchers: CoroutineDispatcherProvider,
) : CardRepository {
override suspend fun wasCardScanned(cardId: String): Boolean {
return withContext(dispatchers.io) { preferencesDataSource.usedCardsPrefStorage.wasScanned(cardId) }
}
}

View file

@ -1,9 +1,12 @@
package com.tangem.data.card.di
import com.tangem.data.card.DefaultCardRepository
import com.tangem.data.card.DefaultCardSdkConfigRepository
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -25,4 +28,13 @@ internal object CardDataModule {
preferencesDataSource = preferencesDataSource,
)
}
@Provides
@Singleton
fun provideCardRepository(
preferencesDataSource: PreferencesDataSource,
dispatchers: CoroutineDispatcherProvider,
): CardRepository {
return DefaultCardRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers)
}
}

1
data/settings/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,26 @@
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.data.settings"
}
dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
implementation(deps.kotlin.coroutines)
implementation(projects.core.utils)
implementation(projects.domain.settings)
implementation(projects.data.source.preferences)
}

View file

@ -0,0 +1,18 @@
package com.tangem.data.settings
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
internal class DefaultSettingsRepository(
private val preferencesDataSource: PreferencesDataSource,
private val dispatchers: CoroutineDispatcherProvider,
) : SettingsRepository {
override suspend fun isUserAlreadyRateApp(): Boolean {
return withContext(dispatchers.io) {
preferencesDataSource.appRatingLaunchObserver.isReadyToShow()
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.data.settings.di
import com.tangem.data.settings.DefaultSettingsRepository
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object SettingsDataModule {
@Provides
@Singleton
fun provideSettingsRepository(
preferencesDataSource: PreferencesDataSource,
dispatchers: CoroutineDispatcherProvider,
): SettingsRepository {
return DefaultSettingsRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.card
import com.tangem.domain.card.repository.CardRepository
class GetCardWasScannedUseCase(private val cardRepository: CardRepository) {
suspend operator fun invoke(cardId: String): Boolean = cardRepository.wasCardScanned(cardId)
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.card.repository
interface CardRepository {
suspend fun wasCardScanned(cardId: String): Boolean
}

View file

@ -26,4 +26,20 @@ interface CardTypesResolver {
fun getPrimaryToken(): Token?
fun getBackupCardsCount(): Int
fun isReleaseFirmwareType(): Boolean
fun getRemainingSignatures(): Int?
fun getCardId(): String
fun isTestCard(): Boolean
fun isAttestationFailed(): Boolean
fun hasWalletSignedHashes(): Boolean
fun hasBackup(): Boolean
fun isBackupForbidden(): Boolean
}

View file

@ -10,6 +10,7 @@ import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.operations.attestation.Attestation
internal class TangemCardTypesResolver(
private val card: CardDTO,
@ -73,6 +74,27 @@ internal class TangemCardTypesResolver(
override fun getBackupCardsCount(): Int = card.wallets.size
override fun isReleaseFirmwareType(): Boolean = card.firmwareVersion.type == FirmwareVersion.FirmwareType.Release
override fun getRemainingSignatures(): Int? = card.wallets.firstOrNull()?.remainingSignatures
override fun getCardId(): String = card.cardId
override fun isTestCard(): Boolean = card.isTestCard
override fun isAttestationFailed(): Boolean = card.attestation.status == Attestation.Status.Failed
override fun hasWalletSignedHashes(): Boolean {
return card.wallets.any {
val totalSignedHashes = it.totalSignedHashes ?: 0
totalSignedHashes > 0
}
}
override fun hasBackup(): Boolean = card.backupStatus != CardDTO.BackupStatus.NoBackup
override fun isBackupForbidden(): Boolean = !(card.settings.isBackupAllowed || card.settings.isHDWalletAllowed)
private fun Blockchain.Companion.fromBlockchainName(blockchainName: String): Blockchain {
// workaround for BSC (BNB) notes cards
return when (blockchainName) {

1
domain/settings/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,4 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}

View file

@ -0,0 +1,8 @@
package com.tangem.domain.settings
import com.tangem.domain.settings.repositories.SettingsRepository
class IsUserAlreadyRateAppUseCase(private val settingsRepository: SettingsRepository) {
suspend operator fun invoke(): Boolean = settingsRepository.isUserAlreadyRateApp()
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.settings.repositories
interface SettingsRepository {
suspend fun isUserAlreadyRateApp(): Boolean
}

View file

@ -37,20 +37,21 @@ dependencies {
kapt(deps.hilt.kapt)
/** Core modules */
implementation(project(":core:featuretoggles"))
implementation(project(":core:navigation"))
implementation(project(":core:ui"))
implementation(projects.core.featuretoggles)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.utils)
/** Feature Apis */
implementation(project(":features:wallet:api"))
/** Domain modules */
implementation(project(":common"))
implementation(projects.common)
implementation(projects.domain.card)
implementation(project(":domain:legacy"))
implementation(project(":domain:models"))
implementation(project(":domain:wallets"))
implementation(project(":domain:wallets:models"))
implementation(projects.domain.legacy)
implementation(projects.domain.models)
implementation(projects.domain.settings)
implementation(projects.domain.tokens)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** Feature Apis */
implementation(projects.features.wallet.api)
}

View file

@ -15,7 +15,6 @@ import androidx.navigation.compose.rememberNavController
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.NavigationStateHolder
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.WalletFragment
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel
@ -35,29 +34,27 @@ internal class DefaultWalletRouter(private val navigationStateHolder: Navigation
override fun Initialize(fragmentManager: FragmentManager) {
this.fragmentManager = fragmentManager
TangemTheme {
NavHost(
navController = rememberNavController().apply { navController = this },
startDestination = WalletScreens.WALLET.name,
) {
composable(WalletScreens.WALLET.name) {
val viewModel = hiltViewModel<WalletViewModel>().apply { router = this@DefaultWalletRouter }
LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel)
NavHost(
navController = rememberNavController().apply { navController = this },
startDestination = WalletScreens.WALLET.name,
) {
composable(WalletScreens.WALLET.name) {
val viewModel = hiltViewModel<WalletViewModel>().apply { router = this@DefaultWalletRouter }
LocalLifecycleOwner.current.lifecycle.addObserver(observer = viewModel)
WalletScreen(state = viewModel.uiState)
}
WalletScreen(state = viewModel.uiState)
}
composable(WalletScreens.ORGANIZE_TOKENS.name) {
BackHandler(onBack = ::popBackStack)
composable(WalletScreens.ORGANIZE_TOKENS.name) {
BackHandler(onBack = ::popBackStack)
val viewModel: OrganizeTokensViewModel = hiltViewModel<OrganizeTokensViewModel>()
.apply { router = this@DefaultWalletRouter }
val viewModel: OrganizeTokensViewModel = hiltViewModel<OrganizeTokensViewModel>()
.apply { router = this@DefaultWalletRouter }
OrganizeTokensScreen(
modifier = Modifier.systemBarsPadding(),
state = viewModel.uiState,
)
}
OrganizeTokensScreen(
modifier = Modifier.systemBarsPadding(),
state = viewModel.uiState,
)
}
}
}

View file

@ -63,12 +63,29 @@ sealed class WalletNotification(open val state: NotificationState) {
),
)
/**
* "Remaining signatures left" notification
*
* @param count number of remaining signatures
*/
class RemainingSignaturesLeft(count: Int) : WalletNotification(
state = NotificationState.Simple(
title = TextReference.Res(id = R.string.common_warning),
subtitle = TextReference.Res(
id = R.string.warning_low_signatures_format,
formatArgs = WrappedList(data = listOf(count)),
),
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
)
/**
* "Wallet already signed hashes" notification
*
* @property onClick lambda be invoked when notification is clicked
*/
data class WalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification(
data class AlreadyToppedUpAndSignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
title = TextReference.Res(id = R.string.common_warning),
subtitle = TextReference.Res(id = R.string.alert_card_signed_transactions),
@ -83,7 +100,7 @@ sealed class WalletNotification(open val state: NotificationState) {
*
* @property onClick lambda be invoked when notification is clicked
*/
data class MultiWalletAlreadySignedHashes(override val onClick: () -> Unit) : Clickable, WalletNotification(
data class SignedTransactionsInThePast(override val onClick: () -> Unit) : Clickable, WalletNotification(
state = NotificationState.Clickable(
title = TextReference.Res(
id = R.string.warning_important_security_info,

View file

@ -37,7 +37,7 @@ internal class WalletStateFactory(
walletsListConfig = createWalletsListConfig(wallets),
pullToRefreshConfig = createPullToRefreshConfig(),
contentItems = persistentListOf(),
notifications = persistentListOf(), // TODO: create notifications
notifications = persistentListOf(),
bottomSheet = WalletBottomSheetConfig(
// TODO: check notifications
isShow = false,
@ -58,7 +58,7 @@ internal class WalletStateFactory(
walletsListConfig = createWalletsListConfig(wallets),
pullToRefreshConfig = createPullToRefreshConfig(),
contentItems = persistentListOf(),
notifications = persistentListOf(), // TODO: create notifications
notifications = persistentListOf(),
bottomSheet = WalletBottomSheetConfig(
// TODO: check notifications
isShow = false,

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.*
@ -40,6 +41,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockGroupTitle
import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TransactionsBlockTitle
import com.tangem.feature.wallet.presentation.wallet.ui.decorations.walletContentItemDecoration
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
/**
@ -54,13 +56,13 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimat
@Composable
internal fun WalletScreen(state: WalletStateHolder) {
BackHandler(onBack = state.onBackClick)
val walletsListState = rememberLazyListState()
Scaffold(
topBar = { WalletTopBar(config = state.topBarConfig) },
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
val walletsListState = rememberLazyListState()
val changeableItemModifier = Modifier.changeWalletAnimator(walletsListState)
val pullRefreshState = rememberPullRefreshState(
refreshing = state.pullToRefreshConfig.isRefreshing,
@ -153,8 +155,8 @@ internal fun WalletScreen(state: WalletStateHolder) {
}
}
PullRefreshIndicator(
refreshing = state.pullToRefreshConfig.isRefreshing,
PullToRefreshIndicator(
isRefreshing = state.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
@ -164,6 +166,21 @@ internal fun WalletScreen(state: WalletStateHolder) {
state.bottomSheet?.let { bottomSheetConfig ->
if (bottomSheetConfig.isShow) WalletBottomSheet(config = bottomSheetConfig)
}
LaunchedEffect(key1 = walletsListState, key2 = state.walletsListConfig.onWalletChange) {
snapshotFlow { walletsListState.layoutInfo.visibleItemsInfo }
.collect(collector = ScrollOffsetCollector(callback = state.walletsListConfig.onWalletChange))
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun PullToRefreshIndicator(isRefreshing: Boolean, state: PullRefreshState, modifier: Modifier = Modifier) {
PullRefreshIndicator(
refreshing = isRefreshing,
state = state,
modifier = modifier,
)
}
@Composable

View file

@ -11,8 +11,6 @@ import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.tooling.preview.Preview
@ -20,7 +18,6 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletsListConfig
import com.tangem.feature.wallet.presentation.wallet.ui.utils.ScrollOffsetCollector
/**
* Wallets list component
@ -47,11 +44,6 @@ internal fun WalletsList(config: WalletsListConfig, lazyListState: LazyListState
WalletCard(state = state, modifier = Modifier.width(itemWidth))
}
}
LaunchedEffect(lazyListState) {
snapshotFlow { lazyListState.layoutInfo.visibleItemsInfo }
.collect(collector = ScrollOffsetCollector(callback = config.onWalletChange))
}
}
@Preview

View file

@ -0,0 +1,104 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import com.tangem.domain.common.CardTypesResolver
import com.tangem.feature.wallet.presentation.wallet.state.WalletNotification
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/**
[REDACTED_AUTHOR]
*/
@Suppress("LongParameterList")
internal class WalletNotificationsListFactory(
private val wasCardScannedCallback: suspend (String) -> Boolean,
private val isUserAlreadyRateAppCallback: suspend () -> Boolean,
private val onWalletAlreadySignedHashesClick: () -> Unit,
private val onMultiWalletAlreadySignedHashesClick: () -> Unit,
private val onBackupCardClick: () -> Unit,
private val hasUnreachableNetworksCallback: () -> Boolean,
private val onLikeTangemAppClick: () -> Unit,
) {
fun create(cardTypesResolver: CardTypesResolver): Flow<ImmutableList<WalletNotification>> {
// TODO: [REDACTED_JIRA] order
return flow {
emit(
buildList {
if (cardTypesResolver.isTestCard()) {
add(element = WalletNotification.TestCard)
return@buildList
}
addRemainingSignaturesLeftNotifications(cardTypesResolver)
if (!cardTypesResolver.isReleaseFirmwareType()) {
add(element = WalletNotification.DevCard)
} else {
addReleaseSpecialNotifications(cardTypesResolver)
}
// TODO: [REDACTED_JIRA] - DemoModeDatasource
if (hasUnreachableNetworksCallback()) {
add(element = WalletNotification.UnreachableNetworks)
}
if (!cardTypesResolver.isBackupForbidden() && !cardTypesResolver.hasBackup()) {
add(element = WalletNotification.BackupCard(onClick = onBackupCardClick))
}
/* TODO: [REDACTED_JIRA]
* List<WalletStoreModel>
* .filter { store ->
* store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
* }
*/
if (isUserAlreadyRateAppCallback()) {
add(element = WalletNotification.LikeTangemApp(onClick = onLikeTangemAppClick))
}
}.toImmutableList(),
)
}
}
private fun MutableList<WalletNotification>.addRemainingSignaturesLeftNotifications(
cardTypesResolver: CardTypesResolver,
) {
val remainingSignatures = cardTypesResolver.getRemainingSignatures()
if (remainingSignatures != null && remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT) {
add(element = WalletNotification.RemainingSignaturesLeft(remainingSignatures))
}
}
private suspend fun MutableList<WalletNotification>.addReleaseSpecialNotifications(
cardTypesResolver: CardTypesResolver,
) {
// TODO: [REDACTED_JIRA] !isDemo
if (!wasCardScannedCallback(cardTypesResolver.getCardId()) && cardTypesResolver.isMultiwalletAllowed()) {
if (cardTypesResolver.isBackupForbidden() && cardTypesResolver.hasWalletSignedHashes()) {
add(
element = WalletNotification.SignedTransactionsInThePast(
onClick = onMultiWalletAlreadySignedHashesClick,
),
)
} else if (cardTypesResolver.hasWalletSignedHashes()) {
add(
element = WalletNotification.AlreadyToppedUpAndSignedHashes(
onClick = onWalletAlreadySignedHashesClick,
),
)
}
}
if (cardTypesResolver.isAttestationFailed()) {
add(element = WalletNotification.CardVerificationFailed)
}
}
private companion object {
const val MAX_REMAINING_SIGNATURES_COUNT = 10
}
}

View file

@ -79,9 +79,10 @@ include(":domain:legacy")
include(":domain:core")
include(":domain:card")
include(":domain:settings")
include(":domain:tokens")
include(":domain:wallets")
include(":domain:wallets:models")
include(":domain:tokens")
// endregion Domain modules
// region Data modules
@ -89,4 +90,5 @@ include(":data:common")
include(":data:card")
include(":data:tokens")
include(":data:source:preferences")
// endregion Data modules
// endregion Data modules
include(":data:settings")