Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-22 16:53:59 +08:00
parent 0dd4d45b62
commit 479a67f3be
19 changed files with 257 additions and 61 deletions

View file

@ -43,8 +43,14 @@ internal object CardDomainModule {
@Provides
@ViewModelScoped
fun provideGetCardWasScannedUseCase(cardRepository: CardRepository): GetCardWasScannedUseCase {
return GetCardWasScannedUseCase(cardRepository = cardRepository)
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
return WasCardScannedUseCase(cardRepository = cardRepository)
}
@Provides
@ViewModelScoped
fun provideSetCardWasScannedUseCase(cardRepository: CardRepository): SetCardWasScannedUseCase {
return SetCardWasScannedUseCase(cardRepository = cardRepository)
}
@Provides

View file

@ -0,0 +1,34 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.datasource.local.card.DefaultUsedCardsStore
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.card.UsedCardsStore
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object CardDataModule {
@Provides
@Singleton
fun provideUsedCardsStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): UsedCardsStore {
return DefaultUsedCardsStore(
store = SharedPreferencesDataStore(
preferencesName = "tapPrefs",
context = context,
adapter = moshi.adapter(
Types.newParameterizedType(List::class.java, UsedCardInfo::class.java),
),
),
)
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.datasource.local.card
data class UsedCardInfo(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
val isActivationFinished: Boolean = false,
)

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.card
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface UsedCardsStore {
fun get(): Flow<List<UsedCardInfo>>
suspend fun getSyncOrNull(): List<UsedCardInfo>?
suspend fun store(item: List<UsedCardInfo>)
}
internal class DefaultUsedCardsStore(
store: SharedPreferencesDataStore<List<UsedCardInfo>>,
) : UsedCardsStore, KeylessDataStoreDecorator<List<UsedCardInfo>>(
wrappedDataStore = store,
key = "usedCardsInfo_v2",
)

View file

@ -1,32 +1,21 @@
package com.tangem.datasource.local.datastore.core
import kotlinx.coroutines.flow.Flow
internal abstract class KeylessDataStoreDecorator<Value : Any>(
wrappedDataStore: StringKeyDataStore<Value>,
) : StringKeyDataStoreDecorator<Unit, Value>(wrappedDataStore) {
private val key: String = DEFAULT_STRING_KEY,
) : StringKeyDataStoreDecorator<String, Value>(wrappedDataStore) {
override fun provideStringKey(key: Unit): String {
return STRING_KEY
}
override fun provideStringKey(key: String) = key
open fun get(): Flow<Value> {
return get(Unit)
}
open fun get() = get(key)
open suspend fun getSyncOrNull(): Value? {
return getSyncOrNull(Unit)
}
open suspend fun getSyncOrNull() = getSyncOrNull(key)
open suspend fun store(item: Value) {
store(Unit, item)
}
open suspend fun store(item: Value) = store(key, item)
open suspend fun isEmpty(): Boolean {
return getSyncOrNull() == null
}
open suspend fun isEmpty() = getSyncOrNull() == null
private companion object {
const val STRING_KEY = "key"
const val DEFAULT_STRING_KEY = "key"
}
}

View file

@ -17,6 +17,7 @@ dependencies {
implementation(deps.tangem.card.android)
implementation(deps.tangem.card.core)
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.data.source.preferences)

View file

@ -1,16 +1,53 @@
package com.tangem.data.card
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.card.UsedCardsStore
import com.tangem.domain.card.repository.CardRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal class DefaultCardRepository(
private val preferencesDataSource: PreferencesDataSource,
private val usedCardsStore: UsedCardsStore,
private val dispatchers: CoroutineDispatcherProvider,
) : CardRepository {
override suspend fun wasCardScanned(cardId: String): Boolean {
return withContext(dispatchers.io) { preferencesDataSource.usedCardsPrefStorage.wasScanned(cardId) }
override fun wasCardScanned(cardId: String): Flow<Boolean> {
return channelFlow {
launch(dispatchers.io) {
usedCardsStore.get()
.collect { savedCards ->
send(element = savedCards.any { it.cardId == cardId })
}
}
withContext(dispatchers.io) {
if (usedCardsStore.getSyncOrNull() == null) {
send(element = false)
}
}
}
}
override suspend fun setCardWasScanned(cardId: String) {
withContext(dispatchers.io) {
usedCardsStore.store(
item = usedCardsStore.getSyncOrNull()
?.updateCard(cardId)
?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)),
)
}
}
private fun List<UsedCardInfo>.updateCard(cardId: String): List<UsedCardInfo> {
return map { cardInfo ->
if (cardInfo.cardId == cardId) {
cardInfo.copy(isScanned = true)
} else {
cardInfo
}
}
}
}

View file

@ -4,6 +4,7 @@ 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.datasource.local.card.UsedCardsStore
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -32,9 +33,9 @@ internal object CardDataModule {
@Provides
@Singleton
fun provideCardRepository(
preferencesDataSource: PreferencesDataSource,
usedCardsStore: UsedCardsStore,
dispatchers: CoroutineDispatcherProvider,
): CardRepository {
return DefaultCardRepository(preferencesDataSource = preferencesDataSource, dispatchers = dispatchers)
return DefaultCardRepository(usedCardsStore = usedCardsStore, dispatchers = dispatchers)
}
}

View file

@ -1,8 +0,0 @@
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,8 @@
package com.tangem.domain.card
import com.tangem.domain.card.repository.CardRepository
class SetCardWasScannedUseCase(private val cardRepository: CardRepository) {
suspend operator fun invoke(cardId: String) = cardRepository.setCardWasScanned(cardId)
}

View file

@ -0,0 +1,9 @@
package com.tangem.domain.card
import com.tangem.domain.card.repository.CardRepository
import kotlinx.coroutines.flow.Flow
class WasCardScannedUseCase(private val cardRepository: CardRepository) {
operator fun invoke(cardId: String): Flow<Boolean> = cardRepository.wasCardScanned(cardId)
}

View file

@ -1,6 +1,10 @@
package com.tangem.domain.card.repository
import kotlinx.coroutines.flow.Flow
interface CardRepository {
suspend fun wasCardScanned(cardId: String): Boolean
fun wasCardScanned(cardId: String): Flow<Boolean>
suspend fun setCardWasScanned(cardId: String)
}

View file

@ -0,0 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.state
import androidx.compose.runtime.Immutable
@Immutable
internal sealed class WalletAlertState {
data class WalletAlreadySignedHashes(val onUnderstandClick: () -> Unit) : WalletAlertState()
}

View file

@ -13,4 +13,6 @@ internal sealed class WalletEvent {
data class ShowToast(val text: TextReference) : WalletEvent()
data class CopyAddress(val address: String) : WalletEvent()
data class ShowWalletAlreadySignedHashesMessage(val onUnderstandClick: () -> Unit) : WalletEvent()
}

View file

@ -0,0 +1,34 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState
@Composable
internal fun WalletAlert(config: WalletAlertState, onDismiss: () -> Unit) {
when (config) {
is WalletAlertState.WalletAlreadySignedHashes -> {
WalletAlreadySignedHashesAlert(config = config, onDismiss = onDismiss)
}
}
}
@Composable
private fun WalletAlreadySignedHashesAlert(config: WalletAlertState.WalletAlreadySignedHashes, onDismiss: () -> Unit) {
BasicDialog(
message = stringResource(id = R.string.alert_signed_hashes_message),
confirmButton = DialogButton(
title = stringResource(id = R.string.common_understand),
onClick = {
config.onUnderstandClick()
onDismiss()
},
),
onDismissDialog = onDismiss,
title = stringResource(id = R.string.warning_important_security_info, "\u26A0"),
dismissButton = DialogButton(title = stringResource(id = R.string.common_cancel), onClick = onDismiss),
)
}

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.text.AnnotatedString
import com.tangem.core.ui.event.EventEffect
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState
import com.tangem.feature.wallet.presentation.wallet.state.WalletEvent
@Composable
@ -18,6 +19,7 @@ internal fun WalletEventEffect(
snackbarHostState: SnackbarHostState,
event: StateEvent<WalletEvent>,
onAutoScrollSet: () -> Unit,
onAlertConfigSet: (WalletAlertState) -> Unit,
) {
val context = LocalContext.current
val resources = LocalContext.current.resources
@ -39,6 +41,11 @@ internal fun WalletEventEffect(
is WalletEvent.CopyAddress -> {
clipboardManager.setText(AnnotatedString(value.address))
}
is WalletEvent.ShowWalletAlreadySignedHashesMessage -> {
onAlertConfigSet(
WalletAlertState.WalletAlreadySignedHashes(onUnderstandClick = value.onUnderstandClick),
)
}
}
},
)

View file

@ -12,10 +12,7 @@ import androidx.compose.material3.FabPosition
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ -30,6 +27,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData
import com.tangem.feature.wallet.presentation.wallet.state.WalletAlertState
import com.tangem.feature.wallet.presentation.wallet.state.WalletMultiCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletSingleCurrencyState
import com.tangem.feature.wallet.presentation.wallet.state.WalletState
@ -72,12 +70,19 @@ internal fun WalletScreen(state: WalletState) {
onAutoScrollReset = { isAutoScroll.value = false },
)
var alertConfig by remember { mutableStateOf<WalletAlertState?>(value = null) }
WalletEventEffect(
walletsListState = walletsListState,
snackbarHostState = snackbarHostState,
event = state.event,
onAutoScrollSet = { isAutoScroll.value = true },
onAlertConfigSet = { alertConfig = it },
)
alertConfig?.let {
WalletAlert(config = it, onDismiss = { alertConfig = null })
}
}
is WalletState.Initial -> Unit
}

View file

@ -1,6 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels
import com.tangem.domain.card.GetCardWasScannedUseCase
import com.tangem.domain.card.WasCardScannedUseCase
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
@ -10,14 +10,16 @@ import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNoti
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlin.collections.count
/**
* Wallet notifications list factory
*
* @property isDemoCardUseCase use case that check if card is demo
* @property isUserAlreadyRateAppUseCase use case that check if card is user already rate app
* @property getCardWasScannedUseCase use case that check if card was scanned
* @property wasCardScannedUseCase use case that check if card was scanned
* @property clickIntents screen click intents
*
[REDACTED_AUTHOR]
@ -25,7 +27,7 @@ import kotlinx.coroutines.flow.flow
internal class WalletNotificationsListFactory(
private val isDemoCardUseCase: IsDemoCardUseCase,
private val isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase,
private val getCardWasScannedUseCase: GetCardWasScannedUseCase,
private val wasCardScannedUseCase: WasCardScannedUseCase,
private val clickIntents: WalletClickIntents,
) {
@ -33,8 +35,9 @@ internal class WalletNotificationsListFactory(
cardTypesResolver: CardTypesResolver,
cryptoCurrencyList: List<CryptoCurrencyStatus>,
): Flow<ImmutableList<WalletNotification>> {
return flow {
emit(
return wasCardScannedUseCase.invoke(cardTypesResolver.getCardId())
.distinctUntilChanged()
.map {
buildList {
addCriticalNotifications(cardTypesResolver)
@ -42,11 +45,9 @@ internal class WalletNotificationsListFactory(
addRateTheAppNotification()
addWarningNotifications(cardTypesResolver, cryptoCurrencyList)
}
.toImmutableList(),
)
}
addWarningNotifications(cardTypesResolver, cryptoCurrencyList, it)
}.toImmutableList()
}
}
private fun MutableList<WalletNotification>.addCriticalNotifications(cardTypesResolver: CardTypesResolver) {
@ -112,9 +113,10 @@ internal class WalletNotificationsListFactory(
)
}
private suspend fun MutableList<WalletNotification>.addWarningNotifications(
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver,
cryptoCurrencyList: List<CryptoCurrencyStatus>,
wasCardScanned: Boolean,
) {
addIf(
element = WalletNotification.Warning.MissingBackup(
@ -133,14 +135,22 @@ internal class WalletNotificationsListFactory(
if (cardTypesResolver.isBackupForbidden()) {
addIf(
element = WalletNotification.Warning.NumberOfSignedHashesIncorrect,
condition = checkSignedHashes(cardTypesResolver = cardTypesResolver, isDemo = isDemo),
condition = checkSignedHashes(
cardTypesResolver = cardTypesResolver,
isDemo = isDemo,
wasCardScanned,
),
)
} else {
addIf(
element = WalletNotification.Warning.MultiWalletSignedHashesIncorrect(
onClick = clickIntents::onMultiWalletSignedHashesNotificationClick,
),
condition = checkSignedHashes(cardTypesResolver = cardTypesResolver, isDemo = isDemo),
condition = checkSignedHashes(
cardTypesResolver = cardTypesResolver,
isDemo = isDemo,
wasCardScanned,
),
)
}
} else {
@ -159,7 +169,7 @@ internal class WalletNotificationsListFactory(
addIf(
element = WalletNotification.Warning.NumberOfSignedHashesIncorrect,
condition = checkSignedHashes(cardTypesResolver, isDemo),
condition = checkSignedHashes(cardTypesResolver, isDemo, wasCardScanned),
)
}
}
@ -176,9 +186,13 @@ internal class WalletNotificationsListFactory(
return any { it.value is CryptoCurrencyStatus.NoAccount }
}
private suspend fun checkSignedHashes(cardTypesResolver: CardTypesResolver, isDemo: Boolean): Boolean {
return cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.hasWalletSignedHashes() &&
!isDemo && !getCardWasScannedUseCase(cardId = cardTypesResolver.getCardId())
private fun checkSignedHashes(
cardTypesResolver: CardTypesResolver,
isDemo: Boolean,
wasCardScanned: Boolean,
): Boolean {
return cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.hasWalletSignedHashes() && !isDemo &&
!wasCardScanned
}
private companion object {

View file

@ -106,7 +106,8 @@ internal class WalletViewModel @Inject constructor(
private val reduxStateHolder: ReduxStateHolder,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventsHandler: AnalyticsEventHandler,
getCardWasScannedUseCase: GetCardWasScannedUseCase,
private val setCardWasScannedUseCase: SetCardWasScannedUseCase,
wasCardScannedUseCase: WasCardScannedUseCase,
isUserAlreadyRateAppUseCase: IsUserAlreadyRateAppUseCase,
isDemoCardUseCase: IsDemoCardUseCase,
// endregion Parameters
@ -119,7 +120,7 @@ internal class WalletViewModel @Inject constructor(
private var isBalanceHidden = true
private val notificationsListFactory = WalletNotificationsListFactory(
getCardWasScannedUseCase = getCardWasScannedUseCase,
wasCardScannedUseCase = wasCardScannedUseCase,
isUserAlreadyRateAppUseCase = isUserAlreadyRateAppUseCase,
isDemoCardUseCase = isDemoCardUseCase,
clickIntents = this,
@ -413,7 +414,21 @@ internal class WalletViewModel @Inject constructor(
}
override fun onMultiWalletSignedHashesNotificationClick() {
// TODO: [REDACTED_JIRA]
val state = uiState as? WalletState.ContentState ?: return
uiState = stateFactory.getStateAndTriggerEvent(
state = uiState,
event = WalletEvent.ShowWalletAlreadySignedHashesMessage(
onUnderstandClick = {
viewModelScope.launch(dispatchers.main) {
setCardWasScannedUseCase(
cardId = getWallet(index = state.walletsListConfig.selectedWalletIndex).cardId,
)
}
},
),
setUiState = { uiState = it },
)
}
override fun onLikeTangemAppClick() {