Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-30 11:03:53 +02:00
parent 1bf78a1f1a
commit f25889d1a0
30 changed files with 149 additions and 275 deletions

View file

@ -27,20 +27,12 @@ configurations.all {
resolutionStrategy {
dependencySubstitution {
substitute(module("com.facebook.react:react-native"))
.using(module("com.facebook.react:react-android:0.72.4"))
substitute(module("com.facebook.react:hermes-engine"))
.using(module("com.facebook.react:hermes-android:0.72.4"))
substitute(module("org.bouncycastle:bcprov-jdk15on"))
.using(module("org.bouncycastle:bcprov-jdk18on:1.73"))
}
force(
"org.bouncycastle:bcpkix-jdk15on:1.70",
"com.facebook.react:react-android:0.72.4",
"com.facebook.react:hermes-android:0.72.4",
)
}
}
@ -224,10 +216,6 @@ dependencies {
implementation(deps.walletConnectCore)
implementation(deps.walletConnectWeb3)
implementation(deps.prettyLogger)
implementation("com.facebook.react:react-android:0.72.4")
implementation(deps.sprClient) {
exclude(group = "com.github.stephenc.jcip")
}
/** Testing libraries */
testImplementation(deps.test.coroutine)

View file

@ -4,7 +4,6 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
@ -49,7 +48,7 @@ internal class LockUserWalletsTimer(
start()
if (shouldOpenWelcomeScreenOnResume) {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
}
}
@ -127,7 +126,7 @@ internal class LockUserWalletsTimer(
if (wasApplicationStopped) {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
} else {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) }
}
}
}

View file

@ -52,7 +52,6 @@ import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.LegacyFeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
@ -316,7 +315,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private fun initWithConfigDependency(config: Config) {
initAnalytics(this, config)
initFeedbackManager(this, foregroundActivityObserver, store)
initFeedbackManager(this, store)
}
private fun initAnalytics(application: Application, config: Config) {
@ -337,11 +336,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
// ExceptionHandler.append(blockchainExceptionHandler) TODO: [REDACTED_JIRA]
}
private fun initFeedbackManager(
context: Context,
foregroundActivityObserver: ForegroundActivityObserver,
store: Store<AppState>,
) {
private fun initFeedbackManager(context: Context, store: Store<AppState>) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo {
return AdditionalFeedbackInfo().apply {
appVersion = try {
@ -382,7 +377,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
val feedbackManager = LegacyFeedbackManager(
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
chatManager = ChatManager(foregroundActivityObserver),
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
)

View file

@ -1,23 +0,0 @@
package com.tangem.tap.common.chat
import android.content.Context
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
import java.io.File
class ChatManager(private val foregroundActivityObserver: ForegroundActivityObserver) {
private val openers = mutableMapOf<ChatConfig, ChatOpener>()
fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) {
val opener = openers.getOrPut(config) {
when (config) {
is SprinklrConfig -> SprinklrChatOpener(config, foregroundActivityObserver)
}
}
opener.open(createFeedbackFile, createLogsFile)
}
}

View file

@ -1,8 +0,0 @@
package com.tangem.tap.common.chat.opener
import android.content.Context
import java.io.File
internal interface ChatOpener {
fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?)
}

View file

@ -1,57 +0,0 @@
package com.tangem.tap.common.chat.opener.implementation
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.provider.Settings
import com.spr.messengerclient.config.SPRMessenger
import com.spr.messengerclient.config.bean.SPRMessengerConfig
import com.tangem.common.extensions.guard
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import timber.log.Timber
import java.io.File
import java.util.Locale
internal class SprinklrChatOpener(
private val config: SprinklrConfig,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
val messenger = SPRMessenger.shared()
if (messenger.config == null) {
initSprConfig(messenger)
}
messenger.startApplication()
}
private fun initSprConfig(messenger: SPRMessenger) {
val application = foregroundActivityObserver.foregroundActivity?.application.guard {
Timber.e("The SPR chat cannot be opened because there are no activities in foreground")
return
}
messenger.takeOff(application, createSprConfig(application, config))
}
@SuppressLint("HardwareIds")
private fun createSprConfig(application: Application, config: SprinklrConfig): SPRMessengerConfig {
return SPRMessengerConfig().apply {
appId = config.appId
appKey = CHAT_APP_KEY
deviceId = Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID)
environment = config.environment
skin = CHAT_SKIN
locale = Locale.getDefault().language
}
}
private companion object {
const val CHAT_APP_KEY = "com.sprinklr.messenger.release"
const val CHAT_SKIN = "MODERN"
}
}

View file

@ -2,13 +2,11 @@ package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.core.navigation.email.EmailSender
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.common.TapWorkarounds
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
@ -29,12 +27,10 @@ import java.io.StringWriter
class LegacyFeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
private val chatManager: ChatManager,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase,
) {
private var sessionFeedbackFile: File? = null
private var sessionLogsFile: File? = null
fun sendEmail(feedbackData: FeedbackData, scanResponse: ScanResponse?) {
@ -97,43 +93,6 @@ class LegacyFeedbackManager(
}
}
fun openChat(config: ChatConfig, feedbackData: FeedbackData) {
chatManager.open(
config = config,
createLogsFile = ::getLogFile,
createFeedbackFile = { context -> getFeedbackFile(context, feedbackData) },
)
}
private fun getFeedbackFile(context: Context, feedbackData: FeedbackData): File? {
return try {
if (sessionFeedbackFile != null) {
return sessionFeedbackFile
}
val file = File(context.filesDir, FEEDBACK_FILE)
file.delete()
file.createNewFile()
val feedback = feedbackData.run {
prepare(infoHolder)
joinTogether(context, infoHolder)
}
val fileWriter = FileWriter(file)
fileWriter.write(feedback)
fileWriter.close()
if (file.exists()) {
sessionFeedbackFile = file
sessionFeedbackFile
} else {
null
}
} catch (ex: Exception) {
Timber.e(ex, "Can't create the logs file")
null
}
}
private fun getLogFile(context: Context): File? {
return try {
if (sessionLogsFile != null) {
@ -172,7 +131,6 @@ class LegacyFeedbackManager(
private companion object {
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
const val FEEDBACK_FILE = "feedback.txt"
const val LOGS_FILE = "logs.txt"
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.StateDialog
@ -79,7 +78,6 @@ sealed class GlobalAction : Action {
data class SetFeedbackManager(val feedbackManager: LegacyFeedbackManager) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData, val scanResponse: ScanResponse?) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()
object ExchangeManager : GlobalAction() {
object Init : GlobalAction() {

View file

@ -74,26 +74,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
scanResponse = action.scanResponse,
)
}
is GlobalAction.OpenChat -> {
val globalState = store.state.globalState
val feedbackManager = globalState.feedbackManager.guard {
store.dispatchDebugErrorNotification("FeedbackManager not initialized")
return
}
val config = globalState.configManager?.config.guard {
store.dispatchDebugErrorNotification("Config not initialized")
return
}
// if config not set -> try to get it based on a scanResponse.productType
val unsafeChatConfig = action.chatConfig ?: config.sprinklr
val chatConfig = unsafeChatConfig.guard {
store.dispatchDebugErrorNotification("The chat config is not initialized")
return
}
feedbackManager.openChat(chatConfig, action.feedbackData)
}
is GlobalAction.ExchangeManager.Init -> {
val appStateSafe = appState() ?: return
val config = appStateSafe.globalState.configManager?.config ?: return

View file

@ -1,5 +1,6 @@
package com.tangem.tap.data
import com.tangem.common.CompletionResult
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
@ -30,7 +31,10 @@ internal class RuntimeUserWalletsStore(
return userWalletsListManager.userWallets.firstOrNull()
}
override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) {
userWalletsListManager.update(userWalletId, update)
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return userWalletsListManager.update(userWalletId, update)
}
}

View file

@ -19,7 +19,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.withContext
import timber.log.Timber
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
@ -48,13 +48,12 @@ internal class DefaultDerivationsRepository(
tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)
.doOnSuccess { response ->
updatePublicKeys(userWalletId = userWalletId, keys = response.entries).fold(
onSuccess = {
validateDerivations(userWallet.scanResponse, derivations)
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
.doOnSuccess {
validateDerivations(scanResponse = it.scanResponse, derivations = derivations)
return
},
onFailure = { throw it },
)
}
.doOnFailure { throw it }
}
.doOnFailure { throw it }
@ -99,8 +98,8 @@ internal class DefaultDerivationsRepository(
}
}
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): Result<Unit> {
return runCatching(dispatchers.io) {
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult<UserWallet> {
return withContext(dispatchers.io) {
userWalletsStore.update(
userWalletId = userWalletId,
update = { userWallet -> userWallet.updateDerivedKeys(keys) },

View file

@ -245,7 +245,7 @@ class DetailsMiddleware {
deleteSavedAccessCodes()
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
store.dispatchNavigationAction { popTo<AppRoute.Home>() }
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
return CompletionResult.Success(Unit)
}

View file

@ -109,7 +109,10 @@ internal class CardSettingsViewModel @Inject constructor(
if (isResetCardAllowed) {
CardInfo.ResetToFactorySettings(
description = getResetToFactoryDescription(card.backupStatus, cardTypesResolver),
description = getResetToFactoryDescription(
isActiveBackupStatus = card.backupStatus?.isActive == true,
typesResolver = cardTypesResolver,
),
).let(::add)
}
}
@ -135,10 +138,15 @@ internal class CardSettingsViewModel @Inject constructor(
push(
route = AppRoute.ResetToFactory(
userWalletId = userWalletId,
cardSpecificInfo = AppRoute.ResetToFactory.CardSpecificInfo(
cardId = card.cardId,
backupStatus = card.backupStatus,
),
cardId = card.cardId,
isActiveBackupStatus = card.backupStatus?.isActive == true,
backupCardsCount = when (val status = card.backupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked,
CardDTO.BackupStatus.NoBackup,
null,
-> 0
},
),
)
}

View file

@ -1,15 +1,14 @@
package com.tangem.tap.features.details.ui.common.utils
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
internal fun getResetToFactoryDescription(
backupStatus: CardDTO.BackupStatus?,
isActiveBackupStatus: Boolean,
typesResolver: CardTypesResolver,
): TextReference {
return if (backupStatus?.isActive != true || typesResolver.isTangemTwins()) {
return if (!isActiveBackupStatus || typesResolver.isTangemTwins()) {
TextReference.Res(R.string.reset_card_without_backup_to_factory_message)
} else {
TextReference.Res(R.string.reset_card_with_backup_to_factory_message)

View file

@ -13,7 +13,6 @@ import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.domain.card.ResetCardUseCase
import com.tangem.domain.card.ResetCardUserCodeParams
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.models.UserWalletId
@ -65,8 +64,15 @@ internal class ResetCardViewModel @Inject constructor(
// endregion
// region Data of card that was scanned on CardSettings
private val primaryCardId: String
private val primaryBackupStatus: CardDTO.BackupStatus?
private val primaryCardId: String = savedStateHandle.get<String>(AppRoute.ResetToFactory.CARD_ID)
?: error("CardId must be provided for ResetCardViewModel")
private val isActiveBackupPrimaryCard =
savedStateHandle.get<Boolean>(AppRoute.ResetToFactory.IS_ACTIVE_BACKUP_STATUS)
?: error("IsActiveBackupCard must be provided for ResetCardViewModel")
private val primaryBackupCardsCount = savedStateHandle.get<Int>(AppRoute.ResetToFactory.BACKUP_CARDS_COUNT)
?: error("CardCount must be provided for ResetCardViewModel")
// endregion
// TODO: move logic to separate domain entity
@ -76,15 +82,6 @@ internal class ResetCardViewModel @Inject constructor(
value = getInitialState(),
)
init {
val cardSpecificInfo = savedStateHandle.get<Bundle>(AppRoute.ResetToFactory.CARD_SPECIFIC_DATA)
?.unbundle(AppRoute.ResetToFactory.CardSpecificInfo.serializer())
?: error("CardSpecificData must be provided for ResetCardViewModel")
primaryCardId = cardSpecificInfo.cardId
primaryBackupStatus = cardSpecificInfo.backupStatus
}
private fun getInitialState(): ResetCardScreenState {
val shouldShowResetPasswordButton = shouldShowResetPasswordButton()
val warningsToShow = buildList {
@ -98,7 +95,7 @@ internal class ResetCardViewModel @Inject constructor(
return ResetCardScreenState(
resetButtonEnabled = false,
descriptionText = getResetToFactoryDescription(
backupStatus = primaryBackupStatus,
isActiveBackupStatus = isActiveBackupPrimaryCard,
typesResolver = currentCardTypesResolver,
),
warningsToShow = warningsToShow,
@ -115,7 +112,7 @@ internal class ResetCardViewModel @Inject constructor(
private fun shouldShowResetPasswordButton(): Boolean {
val isTangemWallet = currentCardTypesResolver.isTangemWallet() || currentCardTypesResolver.isWallet2()
return isTangemWallet && primaryBackupStatus is CardDTO.BackupStatus.Active
return isTangemWallet && isActiveBackupPrimaryCard
}
private fun toggleFirstCondition(isAccepted: Boolean) {
@ -283,12 +280,6 @@ internal class ResetCardViewModel @Inject constructor(
private fun getBackupCardsCount(): Int {
if (!currentCardTypesResolver.isMultiwalletAllowed()) return 0
return when (val status = primaryBackupStatus) {
is CardDTO.BackupStatus.Active -> status.cardCount
is CardDTO.BackupStatus.CardLinked,
is CardDTO.BackupStatus.NoBackup,
null,
-> 0
}
return primaryBackupCardsCount
}
}

View file

@ -13,7 +13,7 @@ internal object AttestationFailedDialog {
return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply {
setTitle(R.string.common_error)
setMessage(R.string.issuer_signature_loading_failed)
setPositiveButton(R.string.ok) { dialog, _ ->
setPositiveButton(R.string.common_ok) { dialog, _ ->
dialog.dismiss()
}
setOnDismissListener {

View file

@ -135,6 +135,7 @@ internal class TokensListMigration(
derivePublicKeysUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
.onRight {
addCryptoCurrenciesUseCase(userWalletId = currentUserWallet.walletId, currencies = currencyList)
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
.onLeft { Timber.e(it, "Failed to derive public keys") }
}

View file

@ -11,8 +11,6 @@ import androidx.lifecycle.viewModelScope
import androidx.paging.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.utils.popTo
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.getGreyedOutIconRes
@ -26,7 +24,6 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.fullNameWithoutTestnet
import com.tangem.tap.common.extensions.getNetworkName
import com.tangem.tap.features.customtoken.impl.presentation.models.SupportBlockchainType
@ -326,7 +323,6 @@ internal class TokensListViewModel @Inject constructor(
)
uiState = state.copy(isSavingInProgress = false)
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
}
}

View file

@ -12,7 +12,9 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
import io.mockk.*
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -144,7 +146,7 @@ internal class DefaultDerivationsRepositoryTest {
coEvery { tangemSdkManager.derivePublicKeys(null, any()) } returns CompletionResult.Success(
DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys),
)
coEvery { userWalletsStore.update(defaultUserWalletId, any()) } just Runs
coEvery { userWalletsStore.update(defaultUserWalletId, any()) } returns CompletionResult.Success(userWallet)
runCatching {
repository.derivePublicKeys(

View file

@ -5,7 +5,6 @@ import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
@ -145,25 +144,35 @@ sealed class AppRoute(val path: String) : Route {
data object AppSettings : AppRoute(path = "/app_settings")
/**
* Reset to factory route
* Reset to factory
*
* @property userWalletId user wallet id
* @property cardSpecificInfo info about card that was scanned on CardSettings
* @property userWalletId user wallet id
* @property cardId reset card id
* @property isActiveBackupStatus reset backup card status
* @property backupCardsCount backup cards count
*/
@Serializable
data class ResetToFactory(
val userWalletId: UserWalletId,
val cardSpecificInfo: CardSpecificInfo,
) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}/$cardSpecificInfo"), RouteBundleParams {
val cardId: String,
val isActiveBackupStatus: Boolean,
val backupCardsCount: Int,
) : AppRoute(
path = "/reset_to_factory" +
"/${userWalletId.stringValue}" +
"/$cardId" +
"/$isActiveBackupStatus" +
"/$backupCardsCount",
),
RouteBundleParams {
override fun getBundle(): Bundle = bundle(serializer())
@Serializable
data class CardSpecificInfo(val cardId: String, val backupStatus: CardDTO.BackupStatus?)
companion object {
const val USER_WALLET_ID = "userWalletId"
const val CARD_SPECIFIC_DATA = "cardSpecificInfo"
const val CARD_ID = "cardId"
const val IS_ACTIVE_BACKUP_STATUS = "isActiveBackupStatus"
const val BACKUP_CARDS_COUNT = "backupCardsCount"
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.local.userwallet
import com.tangem.common.CompletionResult
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -14,6 +15,8 @@ interface UserWalletsStore {
suspend fun getAllSyncOrNull(): List<UserWallet>?
@Throws
suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet)
suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet>
}

View file

@ -8,8 +8,6 @@ import com.tangem.common.card.EncryptionMode
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.attestation.Attestation
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Date
import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion
@ -300,19 +298,12 @@ data class CardDTO(
}
}
@Serializable
sealed class BackupStatus {
@Serializable
@SerialName("card_linked")
data class CardLinked(val cardCount: Int) : BackupStatus()
@Serializable
@SerialName("active")
data class Active(val cardCount: Int) : BackupStatus()
@Serializable
@SerialName("no_backup")
data object NoBackup : BackupStatus()
val isActive: Boolean

View file

@ -24,7 +24,7 @@ internal class DefaultDetailsComponent @AssistedInject constructor(
private val model: DetailsModel = getOrCreateModel(params)
private val userWalletListComponent = userWalletListComponentFactory.create(
context = child(key = "user_wallet_list"),
context = child(key = "user_wallet_list_component"),
)
@Composable

View file

@ -1,17 +1,22 @@
package com.tangem.features.details.utils
import arrow.core.raise.*
import arrow.core.recover
import androidx.compose.ui.res.stringResource
import arrow.core.raise.Raise
import arrow.core.raise.ensureNotNull
import arrow.core.raise.fold
import arrow.core.raise.recover
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.navigation.popTo
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.SimpleOkDialog
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.isNullOrEmpty
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.ContentMessage
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.models.scan.ScanResponse
@ -21,7 +26,7 @@ import com.tangem.domain.wallets.models.SaveWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
import com.tangem.features.details.impl.R
import javax.inject.Inject
@ -31,7 +36,7 @@ internal class UserWalletSaver @Inject constructor(
private val scanCardProcessor: ScanCardProcessor,
private val saveWalletUseCase: SaveWalletUseCase,
private val generateWalletNameUseCase: GenerateWalletNameUseCase,
private val selectWalletUseCase: SelectWalletUseCase,
private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase,
private val reduxStateHolder: ReduxStateHolder,
private val messageSender: UiMessageSender,
private val router: Router,
@ -43,8 +48,6 @@ internal class UserWalletSaver @Inject constructor(
val userWallet = createUserWallet(response)
saveWallet(userWallet)
router.popTo<AppRoute.Wallet>()
},
recover = { error ->
val message = error.message
@ -56,28 +59,44 @@ internal class UserWalletSaver @Inject constructor(
)
private suspend fun Raise<Error>.saveWallet(userWallet: UserWallet) {
saveWalletUseCase(userWallet).recover { error ->
when (error) {
is SaveWalletError.WalletAlreadySaved -> selectUserWallet(userWallet)
is SaveWalletError.DataError -> {
val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) {
Error.Unknown
fold(
block = { saveWalletUseCase(userWallet).bind() },
recover = { error ->
when (error) {
is SaveWalletError.WalletAlreadySaved -> {
if (shouldSaveUserWalletsSyncUseCase()) {
selectUserWallet()
} else {
router.popTo<AppRoute.Wallet>()
}
}
is SaveWalletError.DataError -> {
val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) {
Error.Unknown
}
raise(Error.Message(messageRef))
raise(Error.Message(messageRef))
}
}
}
}.bind()
},
transform = {
// call only if wallet is successfully saved
reduxStateHolder.onUserWalletSelected(userWallet)
reduxStateHolder.onUserWalletSelected(userWallet)
router.popTo<AppRoute.Wallet>()
},
)
}
private suspend fun Raise<Error>.selectUserWallet(userWallet: UserWallet) {
withError({ Error.Unknown }) {
selectWalletUseCase(userWallet.walletId).bind()
}
router.popTo<AppRoute.Wallet>()
private fun selectUserWallet() {
messageSender.send(
message = ContentMessage { onDismiss ->
SimpleOkDialog(
message = stringResource(id = R.string.user_wallet_list_error_wallet_already_saved),
onDismissDialog = onDismiss,
)
},
)
}
private suspend fun Raise<Error>.createUserWallet(response: ScanResponse): UserWallet {

View file

@ -59,7 +59,11 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
.background(backgroundColor)
.statusBarsPadding(),
) {
Column(modifier = Modifier.padding(bottom = bottomPadding)) {
Column(
modifier = Modifier
.padding(bottom = bottomPadding)
.fillMaxSize(),
) {
TangemTopAppBar(
title = resourceReference(R.string.disclaimer_title),
startButton = TopAppBarButtonUM(
@ -110,6 +114,7 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) {
it.setBackgroundColor(backgroundColor.toArgb())
},
client = remember { DisclaimerWebViewClient() },
modifier = Modifier.fillMaxSize(),
)
AnimatedVisibility(
@ -117,6 +122,9 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) {
label = "Loading state change animation",
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.fillMaxSize()
.background(backgroundColor),
) {
Box(
modifier = Modifier

View file

@ -12,7 +12,6 @@ import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -32,6 +31,7 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.OkxPromoNotification
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.components.transactions.txHistoryItems
import com.tangem.core.ui.event.EventEffect
@ -64,7 +64,16 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
snackbarHost = {
TangemSnackbarHost(
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = bottomBarHeight + TangemTheme.dimens.spacing16,
),
hostState = snackbarHostState,
)
},
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->

View file

@ -368,7 +368,9 @@ private fun BaseScaffoldWithMarkets(
WalletSnackbarHost(
snackbarHostState = it,
event = state.event,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16),
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing4)
.navigationBarsPadding(),
)
},
containerColor = TangemTheme.colors.background.secondary,

View file

@ -341,6 +341,8 @@ internal class WalletViewModel @Inject constructor(
}
private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) {
walletScreenContentLoader.cancel(action.deletedWalletId)
walletScreenContentLoader.load(
userWallet = action.selectedWallet,
clickIntents = clickIntents,

View file

@ -254,7 +254,6 @@ reactive-network = { module = "com.github.pwittchen:reactivenetwork-rx2", versio
walletConnectCore = { module = "com.walletconnect:android-core", version.ref = "walletConnectCore" }
walletConnectWeb3 = { module = "com.walletconnect:web3wallet", version.ref = "walletConnectWeb3" }
prettyLogger = { module = "com.orhanobut:logger", version.ref = "prettyLogger" }
sprClient = { module = "com.spr:messengerclient", version.ref = "spr-client" }
chucker = { module = "com.github.chuckerteam.chucker:library", version.ref = "chucker" }
chuckerStub = { module = "com.github.chuckerteam.chucker:library-no-op", version.ref = "chucker" }
mlKit-barcodeScanning = { module = "com.google.mlkit:barcode-scanning", version.ref = "mlKit-barcodeScanning" }

View file

@ -65,9 +65,7 @@ private fun AppExtension.configureBuildTypes() {
private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buildType: BuildType) {
when (buildType) {
BuildType.Release,
BuildType.External,
-> {
BuildType.Release -> {
isDebuggable = false
isMinifyEnabled = false
proguardFiles(extension.getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
@ -76,6 +74,11 @@ private fun AndroidBuildType.configureBuildVariant(extension: AppExtension, buil
isDebuggable = true
isMinifyEnabled = false
}
BuildType.External -> {
initWith(extension.buildTypes.getByName(BuildType.Release.id))
matchingFallbacks.add(BuildType.Release.id)
signingConfig = extension.signingConfigs.getByName(BuildType.Debug.id)
}
BuildType.Internal,
BuildType.Mocked,
-> {