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

@ -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>() }
}
}