Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-31 14:44:40 +03:00
parent 8e1763dee2
commit 504a49949a
33 changed files with 408 additions and 25 deletions

View file

@ -28,9 +28,6 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys.WAS_LOG_FILE_CLEARED
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
@ -69,11 +66,8 @@ import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import kotlin.collections.MutableMap
import kotlin.collections.listOf
import kotlin.collections.set
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
@ -220,12 +214,15 @@ abstract class TangemApplication : Application(), ImageLoaderFactory {
private fun updateLogFiles() {
appLogsStore.deleteOldLogsFile()
scope.launch {
if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
appLogsStore.deleteLastLogFile()
appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
}
}
appLogsStore.deleteLastLogFile()
// Temporally logs are not saved
// scope.launch {
// if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) {
// appLogsStore.deleteLastLogFile()
// appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true)
// }
// }
}
fun init() {

View file

@ -159,4 +159,10 @@ internal object WalletsDomainModule {
dispatchers = dispatchers,
)
}
@Provides
@Singleton
fun providesSeedPhraseNotificationUseCase(walletsRepository: WalletsRepository): SeedPhraseNotificationUseCase {
return SeedPhraseNotificationUseCase(walletsRepository = walletsRepository)
}
}

View file

@ -692,6 +692,14 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
)
}
scope.launch {
userWallet?.let {
if (it.scanResponse.cardTypesResolver.isWallet2() && it.isImported) {
store.inject(DaggerGraphState::walletsRepository).markWallet2WasCreated(it.walletId)
}
}
}
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) {
null

View file

@ -1,4 +1,7 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}
dependencies {
implementation(projects.core.utils)
}

View file

@ -0,0 +1,28 @@
package com.tangem.common
import com.tangem.utils.SupportedLanguages
/**
[REDACTED_AUTHOR]
*/
object TangemBlogUrlBuilder {
fun build(post: Post): String {
val code = SupportedLanguages.getCurrentSupportedLanguageCode()
.takeIf { code ->
code == SupportedLanguages.RUSSIAN || code == SupportedLanguages.ENGLISH
}
?: SupportedLanguages.ENGLISH
return "https://tangem.com/$code/blog/post/${post.path}/"
}
sealed interface Post {
val path: String
data object SeedNotify : Post {
override val path: String = "seed-notify"
}
}
}

View file

@ -49,6 +49,9 @@ interface TangemTechApi {
@Body userTokens: UserTokensResponse,
): ApiResponse<Unit>
@POST("user-tokens")
suspend fun markUserWallerWasCreated(@Body body: MarkUserWalletWasCreatedBody): ApiResponse<Unit>
/** Returns referral status by [walletId] */
@GET("referral/{walletId}")
suspend fun getReferralStatus(@Path("walletId") walletId: String): ReferralResponse
@ -108,6 +111,17 @@ interface TangemTechApi {
@GET("networks/providers")
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
@GET("seedphrase-notification/{wallet_id}")
suspend fun getSeedPhraseNotificationStatus(
@Path("wallet_id") walletId: String,
): ApiResponse<SeedPhraseNotificationDTO>
@PUT("seedphrase-notification/{wallet_id}")
suspend fun updateSeedPhraseNotificationStatus(
@Path("wallet_id") walletId: String,
@Body body: SeedPhraseNotificationDTO,
): ApiResponse<Unit>
companion object {
val marketsQuoteFields = listOf(
"price",

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class MarkUserWalletWasCreatedBody(
@Json(name = "user_wallet_id") val userWalletId: String,
)

View file

@ -0,0 +1,24 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SeedPhraseNotificationDTO(val status: Status) {
enum class Status {
@Json(name = "notneeded")
NOT_NEEDED,
@Json(name = "notified")
NOTIFIED,
@Json(name = "declined")
DECLINED,
@Json(name = "confirmed")
CONFIRMED,
;
}
}

View file

@ -59,6 +59,9 @@ class AppLogsStore @Inject constructor(
/** Save log [message] */
fun saveLogMessage(message: String) {
// Temporally logs are not saved
return
launchWithLock {
createFileIfNotExist()
@ -68,6 +71,9 @@ class AppLogsStore @Inject constructor(
/** Save log that consists from [messages] */
fun saveLogMessage(vararg messages: String) {
// Temporally logs are not saved
return
launchWithLock {
createFileIfNotExist()

View file

@ -51,6 +51,8 @@
<string name="balance_hidden_do_not_show_button">Nicht mehr anzeigen</string>
<string name="balance_hidden_got_it_button">Verstanden</string>
<string name="balance_hidden_title">Guthaben sind ausgeblendet</string>
<string name="beta_mode_warning_message">Laut den Blockchain-Entwicklern befinden sich der Kaspa-Token derzeit in der Betaphase. Bleibe dran für Updates!</string>
<string name="beta_mode_warning_title">Beta-Phase</string>
<string name="biometric_lockout_permanent_warning_description">Bitte Karte oder Ring scannen</string>
<string name="biometric_lockout_warning_description">Bitte versuche es in 30 Sekunden erneut oder scanne die Karte oder Ring</string>
<string name="biometric_lockout_warning_title">Zu viele Versuche</string>
@ -1065,6 +1067,8 @@
<string name="warning_rate_app_title">Gefällt dir Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Du musst deinen Token zuordnen, bevor du Token erhalten kannst</string>
<string name="warning_rent_fee_title">Netzmietgebühr erforderlich</string>
<string name="warning_seedphrase_issue_message">Haben Sie schon einmal unser Support-Team über die App kontaktiert?</string>
<string name="warning_seedphrase_issue_title">Wir haben ein Problem im Support-System von Tangem festgestellt</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s ist ein Vermögenswert im %2$s Netzwerk. Um eine %3$s Transaktion durchzuführen, musst du etwas %4$s (%5$s) einzahlen, um die Netzwerkgebühr zu decken.</string>
<string name="warning_send_blocked_funds_for_fee_title">Unzureichende %1$s zur Deckung der Netzgebühr</string>
<string name="warning_solana_fee_message">Das Solana-Netz ist überlastet. Wenn deine Transaktion nicht innerhalb von 2 Minuten bearbeitet wird, wiederhole bitte die Transaktion.</string>

View file

@ -1065,6 +1065,8 @@
<string name="warning_rate_app_title">¿Disfrutando de Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Deba asociar su token antes de recibir tokens</string>
<string name="warning_rent_fee_title">Se requiere tarifa de alquiler de red</string>
<string name="warning_seedphrase_issue_message">¿Alguna vez te has puesto en contacto con nuestro equipo de soporte a través de la aplicación?</string>
<string name="warning_seedphrase_issue_title">Hemos encontrado un problema en el sistema de soporte de Tangem</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s un activo en la red %2$s. Para realizar una transacción %3$s, deposite %4$s (%5$s) para cubrir la tarifa de red.</string>
<string name="warning_send_blocked_funds_for_fee_title">%1$s insuficiente para cubrir la tarifa de red</string>
<string name="warning_solana_fee_message">La red Solana está congestionada. Si su transacción no se procesa dentro de 2 minutos, repita la transacción.</string>

View file

@ -51,6 +51,8 @@
<string name="balance_hidden_do_not_show_button">Ne plus afficher</string>
<string name="balance_hidden_got_it_button">Compris</string>
<string name="balance_hidden_title">Les soldes sont masqués</string>
<string name="beta_mode_warning_message">Selon les développeurs de la blockchain, les jetons Kaspa sont actuellement en version bêta. Restez à l\'écoute des mises à jour !</string>
<string name="beta_mode_warning_title">Mode bêta</string>
<string name="biometric_lockout_permanent_warning_description">Veuillez scanner la carte/bague</string>
<string name="biometric_lockout_warning_description">Veuillez réessayer dans 30 secondes ou scannez la carte/bague</string>
<string name="biometric_lockout_warning_title">Trop de tentatives</string>
@ -1065,6 +1067,8 @@
<string name="warning_rate_app_title">Vous appréciez Tangem ?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Vous devez associer votre jeton avant de recevoir des jetons</string>
<string name="warning_rent_fee_title">Frais de location de réseau requis</string>
<string name="warning_seedphrase_issue_message">Avez-vous déjà contacté notre équipe d\'assistance via l\'application ?</string>
<string name="warning_seedphrase_issue_title">Nous avons trouvé un problème dans le système de support de Tangem</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s s\'agit d\'un actif du réseau %2$s. Pour effectuer une transaction %3$s, vous devez déposer une certaine quantité de %4$s (%5$s) pour couvrir les frais de réseau.</string>
<string name="warning_send_blocked_funds_for_fee_title">%1$s insuffisant pour couvrir les frais de réseau</string>
<string name="warning_solana_fee_message">Le réseau Solana est encombré. Si votre transaction n\'est pas traitée dans les 2 minutes, veuillez répéter la transaction.</string>

View file

@ -51,6 +51,8 @@
<string name="balance_hidden_do_not_show_button">今後表示しない</string>
<string name="balance_hidden_got_it_button">わかりました</string>
<string name="balance_hidden_title">残高は非表示</string>
<string name="beta_mode_warning_message">ブロックチェーン開発者によると、Kaspaトークンは現在ベータ版です。アップデートをお楽しみに</string>
<string name="beta_mode_warning_title">ベータモード</string>
<string name="biometric_lockout_permanent_warning_description">カードまたはリングをスキャンしてください</string>
<string name="biometric_lockout_warning_description">30秒後に再試行するか、カードまたはリングをスキャンしてください</string>
<string name="biometric_lockout_warning_title">試行回数が多すぎます</string>

View file

@ -1086,6 +1086,8 @@
<string name="warning_rate_app_title">Нравится Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Вам необходимо провести ассоциацию токена для того, чтобы иметь возможность принимать его</string>
<string name="warning_rent_fee_title">Необходима плата за аренду сети</string>
<string name="warning_seedphrase_issue_message">Вы когда-либо связывались с нашей службой поддержки через приложение?</string>
<string name="warning_seedphrase_issue_title">Мы обнаружили проблему в системе поддержки Tangem</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s - это монета в сети %2$s. Для совершения транзакции %3$s, вам необходимо внести немного %4$s (%5$s), чтобы покрыть комиссию сети.</string>
<string name="warning_send_blocked_funds_for_fee_title">Недостаточно %1$s для оплаты комиссии сети</string>
<string name="warning_solana_fee_message">Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку.</string>

View file

@ -1087,6 +1087,8 @@
<string name="warning_rate_app_title">Подобається Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">Вам необхідно провести асоціацію токена, щоб мати можливість приймати його</string>
<string name="warning_rent_fee_title">Необхідна плата за оренду мережі</string>
<string name="warning_seedphrase_issue_message">Ви коли-небудь зверталися до нашої служби підтримки через застосунок?</string>
<string name="warning_seedphrase_issue_title">Ми виявили проблему в системі підтримки Tangem</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s є активом у мережі %2$s. Щоб здійснити транзакцію %3$s, ви повинні внести певну суму %4$s (%5$s), щоб покрити комісію мережі.</string>
<string name="warning_send_blocked_funds_for_fee_title">Недостатньо %1$s для покриття комісії мережі</string>
<string name="warning_solana_fee_message">Мережа Солана зазнає високого навантаження. Якщо транзакція не пройшла протягом 2 хвилин, повторіть транзакцію.</string>

View file

@ -1067,6 +1067,8 @@
<string name="warning_rate_app_title">Enjoying Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">You must associate your token before receiving tokens</string>
<string name="warning_rent_fee_title">Network rent fee required</string>
<string name="warning_seedphrase_issue_message">Have you ever contacted our support team through the app?</string>
<string name="warning_seedphrase_issue_title">We found an issue in Tangem\'s support system</string>
<string name="warning_send_blocked_funds_for_fee_message">%1$s is an asset in the %2$s network. To make a %3$s transaction, you must deposit some %4$s (%5$s) to cover the network fee.</string>
<string name="warning_send_blocked_funds_for_fee_title">Insufficient %1$s to cover network fee</string>
<string name="warning_solana_fee_message">The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction.</string>

View file

@ -1,16 +1,33 @@
package com.tangem.data.wallets
import com.tangem.datasource.api.common.response.ApiResponseError.HttpException
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.MarkUserWalletWasCreatedBody
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO
import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status
import com.tangem.datasource.local.datastore.RuntimeStateStore
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.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
internal class DefaultWalletsRepository(
private val appPreferencesStore: AppPreferencesStore,
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<Boolean>,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletsRepository {
override suspend fun shouldSaveUserWalletsSync(): Boolean {
@ -38,4 +55,71 @@ internal class DefaultWalletsRepository(
it[PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY] = added + userWalletId.stringValue
}
}
override fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow<Boolean> {
return channelFlow {
launch {
seedPhraseNotificationVisibilityStore.get().collectLatest(::send)
}
fetchSeedPhraseNotificationStatus(userWalletId)
}
}
private suspend fun fetchSeedPhraseNotificationStatus(userWalletId: UserWalletId) {
val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId)
val status = if (userWallet?.isImported == false) {
false
} else {
runCatching(dispatchers.io) {
tangemTechApi.getSeedPhraseNotificationStatus(walletId = userWalletId.stringValue).getOrThrow()
}
.fold(
onSuccess = { it.status == Status.NOTIFIED },
onFailure = { it is HttpException && it.code == HttpException.Code.NOT_FOUND },
)
}
seedPhraseNotificationVisibilityStore.store(value = status)
}
override suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId) {
runCatching(dispatchers.io) {
tangemTechApi.updateSeedPhraseNotificationStatus(
walletId = userWalletId.stringValue,
body = SeedPhraseNotificationDTO(status = Status.NOTIFIED),
).getOrThrow()
}
}
override suspend fun confirmSeedPhraseNotification(userWalletId: UserWalletId) {
runCatching(dispatchers.io) {
tangemTechApi.updateSeedPhraseNotificationStatus(
walletId = userWalletId.stringValue,
body = SeedPhraseNotificationDTO(status = Status.CONFIRMED),
).getOrThrow()
}
seedPhraseNotificationVisibilityStore.store(value = false)
}
override suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId) {
runCatching(dispatchers.io) {
tangemTechApi.updateSeedPhraseNotificationStatus(
walletId = userWalletId.stringValue,
body = SeedPhraseNotificationDTO(status = Status.DECLINED),
).getOrThrow()
}
seedPhraseNotificationVisibilityStore.store(value = false)
}
override suspend fun markWallet2WasCreated(userWalletId: UserWalletId) {
runCatching(dispatchers.io) {
tangemTechApi.markUserWallerWasCreated(
body = MarkUserWalletWasCreatedBody(userWalletId = userWalletId.stringValue),
)
}
}
}

View file

@ -2,9 +2,13 @@ package com.tangem.data.wallets.di
import com.tangem.data.wallets.DefaultWalletNamesMigrationRepository
import com.tangem.data.wallets.DefaultWalletsRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -17,8 +21,19 @@ internal object WalletsDataModule {
@Provides
@Singleton
fun providesWalletsRepository(appPreferencesStore: AppPreferencesStore): WalletsRepository {
return DefaultWalletsRepository(appPreferencesStore = appPreferencesStore)
fun providesWalletsRepository(
appPreferencesStore: AppPreferencesStore,
tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
): WalletsRepository {
return DefaultWalletsRepository(
appPreferencesStore = appPreferencesStore,
tangemTechApi = tangemTechApi,
userWalletsStore = userWalletsStore,
seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = false),
dispatchers = dispatchers,
)
}
@Provides

View file

@ -29,7 +29,8 @@ class SendFeedbackEmailUseCase(
address = getAddress(type.cardInfo),
subject = emailSubjectResolver.resolve(type),
message = createMessage(type),
file = feedbackRepository.getLogFile(),
// Temporally user data is not sent
file = null, // feedbackRepository.getLogFile(),
)
feedbackRepository.sendEmail(email)

View file

@ -14,4 +14,14 @@ interface WalletsRepository {
suspend fun isWalletWithRing(userWalletId: UserWalletId): Boolean
suspend fun setHasWalletsWithRing(userWalletId: UserWalletId)
fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow<Boolean>
suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId)
suspend fun confirmSeedPhraseNotification(userWalletId: UserWalletId)
suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId)
suspend fun markWallet2WasCreated(userWalletId: UserWalletId)
}

View file

@ -0,0 +1,26 @@
package com.tangem.domain.wallets.usecase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import kotlinx.coroutines.flow.Flow
class SeedPhraseNotificationUseCase(
private val walletsRepository: WalletsRepository,
) {
operator fun invoke(userWalletId: UserWalletId): Flow<Boolean> {
return walletsRepository.seedPhraseNotificationStatus(userWalletId)
}
suspend fun notified(userWalletId: UserWalletId) {
walletsRepository.notifiedSeedPhraseNotification(userWalletId)
}
suspend fun confirm(userWalletId: UserWalletId) {
walletsRepository.confirmSeedPhraseNotification(userWalletId)
}
suspend fun decline(userWalletId: UserWalletId) {
walletsRepository.declineSeedPhraseNotification(userWalletId)
}
}

View file

@ -101,8 +101,9 @@ dependencies {
implementation(projects.features.onboardingV2.api)
/** Common modules */
implementation(projects.common.ui)
implementation(projects.common)
implementation(projects.common.routing)
implementation(projects.common.ui)
/** Test libraries */
implementation(deps.test.junit)

View file

@ -126,5 +126,11 @@ sealed class WalletScreenAnalyticsEvent {
data object EditWalletTapped : MainScreen(event = "Button - Edit Wallet Tapped")
data object DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped")
data object NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support")
data object NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No")
data object NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes")
}
}

View file

@ -62,6 +62,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
source = AnalyticsParam.ScreensSources.Main,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Ring,
)
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.feature.wallet.presentation.wallet.analytics.utils
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import dagger.hilt.android.scopes.ViewModelScoped
import javax.inject.Inject
@ViewModelScoped
internal class WalletWarningsSingleEventSender @Inject constructor(
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
private val screenLifecycleProvider: ScreenLifecycleProvider,
) {
suspend fun send(
userWalletId: UserWalletId,
displayedUiState: WalletState?,
newWarnings: List<WalletNotification>,
) {
if (screenLifecycleProvider.isBackgroundState.value) return
if (newWarnings.isEmpty()) return
if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return
val events = newWarnings.filter { it !in displayedUiState.warnings }
events.forEach { event ->
if (event is WalletNotification.Critical.SeedPhraseNotification) {
seedPhraseNotificationUseCase.notified(userWalletId = userWalletId)
}
}
}
}

View file

@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.tokens.repository.PromoRepository
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import dagger.hilt.android.scopes.ViewModelScoped
@ -35,23 +36,33 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val promoRepository: PromoRepository,
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
private val backupValidator: BackupValidator,
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
) {
@Suppress("MagicNumber", "MaximumLineLength")
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
val promoFlow = flow { emit(promoRepository.getRingPromoBanner()) }
return combine(
flow = tokenListStore.getOrThrow(userWallet.walletId),
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = shouldShowRingPromoUseCase(userWalletId = userWallet.walletId),
flow5 = promoFlow,
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, shouldShowPromo, promoBanner ->
tokenListStore.getOrThrow(userWallet.walletId),
isReadyToShowRateAppUseCase(),
isNeedToBackupUseCase(userWallet.walletId),
shouldShowRingPromoUseCase(userWalletId = userWallet.walletId),
promoFlow,
seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
) {
val maybeTokenList = it[0] as Lce<TokenListError, TokenList>
val isReadyToShowRating = it[1] as Boolean
val isNeedToBackup = it[2] as Boolean
val shouldShowPromo = it[3] as Boolean
val promoBanner = it[4] as? PromoBanner
val seedPhraseIssueStatus = it[5] as Boolean
buildList {
addRingPromoNotification(shouldShowPromo, promoBanner, clickIntents)
addCriticalNotifications(userWallet, clickIntents)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
@ -83,8 +94,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private fun MutableList<WalletNotification>.addCriticalNotifications(
userWallet: UserWallet,
seedPhraseIssueStatus: Boolean,
clickIntents: WalletClickIntents,
) {
addIf(
element = WalletNotification.Critical.SeedPhraseNotification(
onDeclineClick = clickIntents::onSeedPhraseNotificationDecline,
onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm,
),
condition = with(userWallet) {
val isDemo = isDemoCardUseCase(cardId = userWallet.cardId)
val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported
!isDemo && isWalletWithSeedPhrase && seedPhraseIssueStatus
},
)
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
addIf(
element = WalletNotification.Critical.BackupError { clickIntents.onSupportClick() },

View file

@ -6,6 +6,7 @@ import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
@ -22,6 +23,7 @@ internal class MultiWalletContentLoader(
private val clickIntents: WalletClickIntents,
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val tokenListStore: MultiWalletTokenListStore,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
@ -49,6 +51,7 @@ internal class MultiWalletContentLoader(
clickIntents = clickIntents,
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
),
)
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
@ -25,6 +26,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) {
@ -39,6 +41,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor(
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
applyTokenListSortingUseCase = applyTokenListSortingUseCase,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
)

View file

@ -5,6 +5,7 @@ import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
@ -21,6 +22,7 @@ internal class SingleWalletWithTokenContentLoader(
private val stateHolder: WalletStateController,
private val tokenListAnalyticsSender: TokenListAnalyticsSender,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val walletWithFundsChecker: WalletWithFundsChecker,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val tokenListStore: MultiWalletTokenListStore,
@ -46,6 +48,7 @@ internal class SingleWalletWithTokenContentLoader(
clickIntents = clickIntents,
getMultiWalletWarningsFactory = getMultiWalletWarningsFactory,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
),
)
}

View file

@ -5,6 +5,7 @@ import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore
import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker
@ -22,6 +23,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
private val tokenListStore: MultiWalletTokenListStore,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase,
) {
@ -36,6 +38,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor(
tokenListStore = tokenListStore,
getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase,
walletWarningsAnalyticsSender = walletWarningsAnalyticsSender,
walletWarningsSingleEventSender = walletWarningsSingleEventSender,
runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase,
)
}

View file

@ -2,7 +2,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.model
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.feature.wallet.impl.R
import org.joda.time.DateTime
@ -47,6 +50,17 @@ sealed class WalletNotification(val config: NotificationConfig) {
onClick = onSupportClick,
),
)
data class SeedPhraseNotification(val onDeclineClick: () -> Unit, val onConfirmClick: () -> Unit) : Critical(
title = resourceReference(R.string.warning_seedphrase_issue_title),
subtitle = resourceReference(R.string.warning_seedphrase_issue_message),
buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
primaryText = resourceReference(R.string.common_yes),
onPrimaryClick = onConfirmClick,
secondaryText = resourceReference(R.string.common_no),
onSecondaryClick = onDeclineClick,
),
)
}
sealed class Warning(

View file

@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender
import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender
import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
@ -20,6 +21,7 @@ internal class MultiWalletWarningsSubscriber(
private val clickIntents: WalletClickIntents,
private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory,
private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender,
private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender,
) : WalletSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<ImmutableList<WalletNotification>> {
@ -31,6 +33,11 @@ internal class MultiWalletWarningsSubscriber(
stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings))
walletWarningsAnalyticsSender.send(displayedState, warnings)
walletWarningsSingleEventSender.send(
userWalletId = userWallet.walletId,
displayedUiState = displayedState,
newWarnings = warnings,
)
}
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import arrow.core.getOrElse
import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.card.SetCardWasScannedUseCase
@ -11,7 +13,10 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.*
import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
import com.tangem.domain.settings.RemindToRateAppLaterUseCase
import com.tangem.domain.settings.ShouldShowRingPromoUseCase
import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase
import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode
import com.tangem.domain.tokens.model.CryptoCurrency
@ -20,6 +25,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockTy
import com.tangem.domain.wallets.models.UnlockWalletsError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.domain.wallets.usecase.UnlockWalletsUseCase
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
@ -65,6 +71,10 @@ internal interface WalletWarningsClickIntents {
fun onSupportClick()
fun onNoteMigrationButtonClick(url: String)
fun onSeedPhraseNotificationConfirm()
fun onSeedPhraseNotificationDecline()
}
@Suppress("LongParameterList")
@ -87,6 +97,8 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
private val urlOpener: UrlOpener,
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
override fun onAddBackupCardClick() {
@ -276,6 +288,28 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onSeedPhraseNotificationConfirm() {
val userWallet = getSelectedUserWallet() ?: return
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes)
viewModelScope.launch {
seedPhraseNotificationUseCase.confirm(userWalletId = userWallet.walletId)
urlOpener.openUrl(url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotify))
}
}
override fun onSeedPhraseNotificationDecline() {
val userWallet = getSelectedUserWallet() ?: return
analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo)
viewModelScope.launch {
seedPhraseNotificationUseCase.decline(userWalletId = userWallet.walletId)
}
}
private fun getSelectedUserWallet(): UserWallet? {
val userWalletId = stateHolder.getSelectedWalletId()
return getUserWalletUseCase(userWalletId).getOrElse {