Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-29 13:50:16 +00:00
commit 6d0b295ca3
219 changed files with 3720 additions and 1687 deletions

View file

@ -1,9 +1,13 @@
package com.tangem.data.card
import androidx.datastore.preferences.core.MutablePreferences
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectList
import com.tangem.datasource.local.preferences.utils.getObjectListSync
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.card.repository.CardRepository
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
@ -21,23 +25,106 @@ internal class DefaultCardRepository(
}
override suspend fun setCardWasScanned(cardId: String) {
appPreferencesStore.editData { mutablePreferences ->
val usedCards: List<UsedCardInfo>? = mutablePreferences.getObjectList(
key = PreferencesKeys.USED_CARDS_INFO_KEY,
)
appPreferencesStore.editUsedCards(cardId) { it.copy(isScanned = true) }
}
val updatedUsedCards = usedCards?.updateCard(cardId)
?: listOf(UsedCardInfo(cardId = cardId, isScanned = true))
override suspend fun startCardActivation(cardId: String) {
appPreferencesStore.editUsedCards(cardId) { it.copy(isActivationStarted = true) }
}
mutablePreferences.setObjectList(
key = PreferencesKeys.USED_CARDS_INFO_KEY,
value = updatedUsedCards,
)
override suspend fun finishCardActivation(cardId: String) {
appPreferencesStore.editUsedCards(cardId) {
it.copy(isActivationStarted = true, isActivationFinished = true)
}
}
private fun List<UsedCardInfo>.updateCard(cardId: String): List<UsedCardInfo> {
val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId, isScanned = true)
return addOrReplace(item = card.copy(isScanned = true), predicate = { it.cardId == cardId })
override suspend fun finishCardsActivation(cardIds: List<String>) {
appPreferencesStore.editData { mutablePreferences ->
val usedCards = mutablePreferences.getUsedCards()
val updatedUsedCards = cardIds.map { cardId ->
usedCards.updateCard(cardId) {
it.copy(isActivationStarted = true, isActivationFinished = true)
}
}
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
}
}
override suspend fun isActivationStarted(cardId: String): Boolean {
return getUsedCardSync(cardId)?.isActivationStarted ?: false
}
override suspend fun isActivationFinished(cardId: String): Boolean {
return getUsedCardSync(cardId)?.isActivationFinished ?: false
}
override suspend fun isActivationInProgress(cardId: String): Boolean {
val card = getUsedCardSync(cardId) ?: return false
return card.isActivationStarted && !card.isActivationFinished
}
override suspend fun isTangemTOSAccepted(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, default = false)
}
override suspend fun isStart2CoinTOSAccepted(cardId: String): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.getStart2CoinTOSAcceptedKey(region = getRegion(cardId)),
default = false,
)
}
override suspend fun acceptTangemTOS() {
return appPreferencesStore.store(key = PreferencesKeys.IS_TANGEM_TOS_ACCEPTED_KEY, true)
}
override suspend fun acceptStart2CoinTOS(cardId: String) {
appPreferencesStore.store(
key = PreferencesKeys.getStart2CoinTOSAcceptedKey(region = getRegion(cardId)),
value = true,
)
}
private suspend fun AppPreferencesStore.editUsedCards(cardId: String, update: (UsedCardInfo) -> UsedCardInfo) {
editData { mutablePreferences ->
val usedCards = mutablePreferences.getUsedCards()
val updatedUsedCards = usedCards.updateCard(cardId = cardId, update = update)
mutablePreferences.setObjectList(key = PreferencesKeys.USED_CARDS_INFO_KEY, value = updatedUsedCards)
}
}
private fun MutablePreferences.getUsedCards(): List<UsedCardInfo> {
return with(appPreferencesStore) {
getObjectListOrDefault(key = PreferencesKeys.USED_CARDS_INFO_KEY, default = mutableListOf())
}
}
private fun List<UsedCardInfo>.updateCard(
cardId: String,
update: (UsedCardInfo) -> UsedCardInfo,
): List<UsedCardInfo> {
val card = find { it.cardId == cardId } ?: UsedCardInfo(cardId = cardId)
return addOrReplace(item = update(card), predicate = { it.cardId == cardId })
}
private suspend fun getUsedCardSync(cardId: String): UsedCardInfo? {
return appPreferencesStore.getObjectListSync<UsedCardInfo>(PreferencesKeys.USED_CARDS_INFO_KEY)
.firstOrNull { it.cardId == cardId }
}
private fun getRegion(cardId: String): String? {
if (cardId.isEmpty()) return null
return when (cardId[1]) {
'0' -> "fr"
'1' -> "ch"
'2' -> "at"
else -> null
}
}
}

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

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,30 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
id("configuration")
}
android {
namespace = "com.tangem.data.onboarding"
}
dependencies {
// region AndroidX libraries
implementation(deps.androidx.datastore)
// endregion
// region DI
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
// endregion
// region Core modules
implementation(projects.core.datasource)
// endregion
// region Domain modules
implementation(projects.domain.onboarding)
// endregion
}

View file

@ -0,0 +1,33 @@
package com.tangem.data.onboarding
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.onboarding.repository.OnboardingRepository
import kotlinx.coroutines.flow.Flow
/**
* Default implementation of [OnboardingRepository]
*
* @property appPreferencesStore app preferences store
*
[REDACTED_AUTHOR]
*/
internal class DefaultOnboardingRepository(
private val appPreferencesStore: AppPreferencesStore,
) : OnboardingRepository {
override fun wasTwinsOnboardingShown(): Flow<Boolean> {
return appPreferencesStore.get(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, default = false)
}
override suspend fun wasTwinsOnboardingShownSync(): Boolean {
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, default = false)
}
override suspend fun saveTwinsOnboardingShown() {
appPreferencesStore.store(key = PreferencesKeys.WAS_TWINS_ONBOARDING_SHOWN, value = true)
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.data.onboarding.di
import com.tangem.data.onboarding.DefaultOnboardingRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.onboarding.repository.OnboardingRepository
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 OnboardingRepositoryModule {
@Provides
@Singleton
fun provideOnboardingRepository(appPreferencesStore: AppPreferencesStore): OnboardingRepository {
return DefaultOnboardingRepository(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -5,6 +5,10 @@ plugins {
id("configuration")
}
android {
namespace = "com.tangem.data.source.preferences"
}
dependencies {
implementation(deps.androidx.core.ktx)
implementation(deps.moshi)

View file

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.tangem.data.source.preferences" />

View file

@ -3,10 +3,6 @@ package com.tangem.data.source.preferences
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.data.source.preferences.adapters.BigDecimalAdapter
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import javax.inject.Inject
// 🔥FIXME: Only logic to work with preferences must be here, must be separated to repositories
@ -14,22 +10,11 @@ import javax.inject.Inject
@Deprecated("Create repository instead")
class PreferencesDataSource @Inject internal constructor(applicationContext: Context) {
val usedCardsPrefStorage: UsedCardsPrefStorage
val disclaimerPrefStorage: DisclaimerPrefStorage
private val preferences: SharedPreferences =
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
private val moshiConverter = MoshiJsonConverter(
adapters = listOf(BigDecimalAdapter()) + MoshiJsonConverter.getTangemSdkAdapters(),
typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters(),
)
init {
incrementLaunchCounter()
usedCardsPrefStorage = UsedCardsPrefStorage(preferences, moshiConverter)
usedCardsPrefStorage.migrate()
disclaimerPrefStorage = DisclaimerPrefStorage(preferences)
}
var shouldShowSaveUserWalletScreen: Boolean
@ -56,14 +41,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
putBoolean(OPEN_WELCOME_ON_RESUME_KEY, value)
}
fun saveTwinsOnboardingShown() {
preferences.edit { putBoolean(TWINS_ONBOARDING_SHOWN_KEY, true) }
}
fun wasTwinsOnboardingShown(): Boolean {
return preferences.getBoolean(TWINS_ONBOARDING_SHOWN_KEY, false)
}
private fun incrementLaunchCounter() {
var count = preferences.getInt(APP_LAUNCH_COUNT_KEY, 0)
preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) }
@ -71,7 +48,6 @@ class PreferencesDataSource @Inject internal constructor(applicationContext: Con
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
private const val SAVE_WALLET_DIALOG_SHOWN_KEY = "saveUserWalletShown"
private const val SAVE_ACCESS_CODES_KEY = "saveAccessCodes"

View file

@ -1,8 +0,0 @@
package com.tangem.data.source.preferences.model
internal data class DataSourceUsedCardInfo(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
val isActivationFinished: Boolean = false,
)

View file

@ -1,7 +0,0 @@
package com.tangem.data.source.preferences.model
internal data class DataSourceUsedCardInfoOld(
val cardId: String,
val isScanned: Boolean = false,
val isActivationStarted: Boolean = false,
)

View file

@ -1,21 +0,0 @@
package com.tangem.data.source.preferences.storage
import android.content.SharedPreferences
import androidx.core.content.edit
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Create repository instead")
class DisclaimerPrefStorage internal constructor(
private val preferences: SharedPreferences,
) {
fun accept(disclaimerKey: String) {
preferences.edit { putBoolean(disclaimerKey, true) }
}
fun isAccepted(disclaimerKey: String): Boolean {
return preferences.getBoolean(disclaimerKey, false)
}
}

View file

@ -1,150 +0,0 @@
package com.tangem.data.source.preferences.storage
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.data.source.preferences.model.DataSourceUsedCardInfo
import com.tangem.data.source.preferences.model.DataSourceUsedCardInfoOld
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Create repository instead")
class UsedCardsPrefStorage internal constructor(
private val preferences: SharedPreferences,
private val jsonConverter: MoshiJsonConverter,
) {
private val migrationList = mutableListOf(
UserCardInfoToV2(this),
)
internal fun migrate() {
migrationList.forEach { it.migrate() }
migrationList.clear()
}
fun scanned(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isScanned = true)
?: DataSourceUsedCardInfo(cardId, true)
save(foundItem, restoredList)
}
fun wasScanned(cardId: String): Boolean {
return findCardInfo(cardId)?.isScanned ?: false
}
fun activationStarted(cardId: String) {
val restoredList = restore()
val foundItem = findCardInfo(cardId, restoredList)?.copy(isActivationStarted = true)
?: DataSourceUsedCardInfo(cardId, isActivationStarted = true)
save(foundItem, restoredList)
}
fun activationFinished(cardId: String) {
val restoredList = restore()
var foundItem = findCardInfo(cardId, restoredList) ?: DataSourceUsedCardInfo(cardId)
foundItem = foundItem.copy(
isActivationStarted = true,
isActivationFinished = true,
)
save(foundItem, restoredList)
}
fun isActivationStarted(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivationStarted ?: false
}
fun isActivationFinished(cardId: String): Boolean {
return findCardInfo(cardId)?.isActivationFinished ?: false
}
fun isActivationInProgress(cardId: String): Boolean {
val cardInfo = findCardInfo(cardId) ?: return false
return cardInfo.isActivationStarted && !cardInfo.isActivationFinished
}
fun hadFinishedActivation(): Boolean {
return restore().any { it.isActivationFinished }
}
private fun findCardInfo(
cardId: String,
list: MutableList<DataSourceUsedCardInfo>? = null,
): DataSourceUsedCardInfo? {
val findInList = list ?: restore()
return findInList.firstOrNull { it.cardId == cardId }
}
private fun save(usedCardInfo: DataSourceUsedCardInfo?, usedCardsInfo: MutableList<DataSourceUsedCardInfo>) {
val info = usedCardInfo ?: return
with(usedCardsInfo) {
val index = indexOfFirst { it.cardId == info.cardId }
if (index == -1) {
add(info)
} else {
set(index, info)
}
}
save(usedCardsInfo)
}
private fun save(list: MutableList<DataSourceUsedCardInfo>) {
val json = jsonConverter.toJson(list)
preferences.edit { putString(USED_CARDS_INFO_V2, json) }
}
private fun restore(): MutableList<DataSourceUsedCardInfo> {
val json = preferences.getString(USED_CARDS_INFO_V2, null) ?: return mutableListOf()
return try {
jsonConverter.fromJson(json, jsonConverter.typedList(DataSourceUsedCardInfo::class.java))!!
} catch (ex: Exception) {
preferences.edit(true) { remove(USED_CARDS_INFO_V2) }
mutableListOf()
}
}
companion object {
private const val USED_CARDS_INFO_V2 = "usedCardsInfo_v2"
private const val USED_CARDS_INFO = "usedCardsInfo"
}
private class UserCardInfoToV2(
private val storage: UsedCardsPrefStorage,
) : Migration {
override fun migrate() {
val restoredCardsInfo = restore()
if (restoredCardsInfo.isEmpty()) return
val newCardsInfo = restoredCardsInfo.map { cardInfo ->
DataSourceUsedCardInfo(
cardId = cardInfo.cardId,
isScanned = cardInfo.isScanned,
isActivationStarted = true,
isActivationFinished = !cardInfo.isActivationStarted,
)
}.toMutableList()
storage.save(newCardsInfo)
}
private fun restore(): MutableList<DataSourceUsedCardInfoOld> {
val json = storage.preferences.getString(USED_CARDS_INFO, null) ?: return mutableListOf()
return try {
storage.jsonConverter.fromJson(
json,
storage.jsonConverter.typedList(DataSourceUsedCardInfoOld::class.java),
)!!
} catch (ex: Exception) {
mutableListOf()
} finally {
storage.preferences.edit(true) { remove(USED_CARDS_INFO) }
}
}
}
}

View file

@ -74,23 +74,40 @@ class ResponseCryptoCurrenciesFactory {
): CryptoCurrency.Coin? {
val network = getNetwork(blockchain, responseToken.derivationPath, derivationStyleProvider) ?: return null
// workaround: Dischain was renamed but backend still returns the old name,
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
val name = if (blockchain == Blockchain.Dischain) blockchain.fullName else responseToken.name
val symbol = if (blockchain == Blockchain.Dischain) blockchain.currency else responseToken.symbol
return CryptoCurrency.Coin(
id = getCoinId(network, blockchain.toCoinId()),
network = network,
name = name,
symbol = symbol,
name = blockchain.getNameForCoin(responseToken),
symbol = blockchain.getSymbolForCoin(responseToken),
decimals = responseToken.decimals,
iconUrl = getCoinIconUrl(blockchain),
isCustom = isCustomCoin(network),
)
}
private fun Blockchain.getNameForCoin(responseToken: UserTokensResponse.Token): String {
return when (this) {
// workaround: Dischain was renamed but backend still returns the old name,
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
Blockchain.Dischain,
Blockchain.Arbitrum,
-> this.fullName
else -> responseToken.name
}
}
private fun Blockchain.getSymbolForCoin(responseToken: UserTokensResponse.Token): String {
return when (this) {
// workaround: Dischain was renamed but backend still returns the old name,
// get name and symbol from enum Blockchain until backend renamed
// [REDACTED_JIRA]
Blockchain.Dischain,
-> this.currency
else -> responseToken.symbol
}
}
private fun createToken(
blockchain: Blockchain,
sdkToken: Token,

View file

@ -4,17 +4,20 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.txhistory.models.Page
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryState
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.domain.txhistory.repository.TxHistoryRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.walletmanager.utils.SdkPageConverter
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -25,6 +28,7 @@ class DefaultTxHistoryRepository(
private val userWalletsStore: UserWalletsStore,
private val txHistoryItemsStore: TxHistoryItemsStore,
) : TxHistoryRepository {
private val sdkPageConverter by lazy { SdkPageConverter() }
override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int {
val userWallet = getUserWallet(userWalletId)
@ -66,14 +70,49 @@ class DefaultTxHistoryRepository(
override fun getTxExploreUrl(txHash: String, networkId: Network.ID): String {
val blockchain = Blockchain.fromId(networkId.value)
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
return if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
""
} else {
blockchain.getExploreTxUrl(txHash)
return when (val txExploreState = blockchain.getExploreTxUrl(txHash)) {
is TxExploreState.Url -> txExploreState.url
is TxExploreState.Unsupported -> ""
}
}
override suspend fun getFixedSizeTxHistoryItems(
userWalletId: UserWalletId,
currency: CryptoCurrency,
pageSize: Int,
refresh: Boolean,
): List<TxHistoryItem> {
cacheRegistry.invokeOnExpire(
key = getTxHistoryPageKey(currency, userWalletId, Page.Initial),
skipCache = refresh,
block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) },
)
val txs = txHistoryItemsStore.getSyncOrNull(
key = TxHistoryItemsStore.Key(userWalletId, currency),
page = Page.Initial,
)?.items
return txs ?: emptyList()
}
private fun getTxHistoryPageKey(currency: CryptoCurrency, userWalletId: UserWalletId, page: Page): String {
return "tx_history_page_${currency}_${userWalletId}_$page"
}
private suspend fun fetchFixedSizeTxHistoryItems(
userWalletId: UserWalletId,
currency: CryptoCurrency,
pageSize: Int,
) {
val wrappedItems = walletManagersFacade.getTxHistoryItems(
userWalletId = userWalletId,
currency = currency,
page = sdkPageConverter.convertBack(Page.Initial),
pageSize = pageSize,
)
txHistoryItemsStore.store(TxHistoryItemsStore.Key(userWalletId, currency), wrappedItems)
}
private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet {
return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) {
"Unable to find user wallet with provided ID: $userWalletId"

View file

@ -1,6 +1,7 @@
package com.tangem.data.visa.utils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.externallinkprovider.TxExploreState
import com.tangem.domain.visa.model.VisaTxDetails
import com.tangem.lib.visa.model.VisaTxHistoryResponse
@ -43,7 +44,12 @@ internal class VisaTxDetailsFactory {
txHash = request.txHash,
txStatus = request.txStatus,
fiatCurrency = findCurrencyByNumericCode(request.transactionCurrencyCode),
exploreUrl = request.txHash?.let(walletBlockchain::getExploreTxUrl),
exploreUrl = request.txHash?.let {
when (val txUrl = walletBlockchain.getExploreTxUrl(it)) {
is TxExploreState.Url -> txUrl.url
is TxExploreState.Unsupported -> ""
}
},
)
}
}