Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-26 16:04:26 +05:00
parent 1485eddbc7
commit 8f9b356307
27 changed files with 326 additions and 115 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.tap.di.domain
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase
import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase
@ -245,4 +246,26 @@ internal object WalletsDomainModule {
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesSetNotificationsEnabledUseCase(
walletsRepository: WalletsRepository,
currenciesRepository: CurrenciesRepository,
): SetNotificationsEnabledUseCase {
return SetNotificationsEnabledUseCase(
walletsRepository = walletsRepository,
currenciesRepository = currenciesRepository,
)
}
@Provides
@Singleton
fun providesGetWalletNotificationsEnabledUseCase(
walletsRepository: WalletsRepository,
): GetWalletNotificationsEnabledUseCase {
return GetWalletNotificationsEnabledUseCase(
walletsRepository = walletsRepository,
)
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.datasource.utils.RequestHeader
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/**
@ -18,6 +19,7 @@ internal class Express(
private val environmentConfigStorage: EnvironmentConfigStorage,
private val expressAuthProvider: ExpressAuthProvider,
private val appVersionProvider: AppVersionProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
@ -49,7 +51,7 @@ internal class Express(
private fun createHeaders(isProd: Boolean) = buildMap {
put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) })
put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId))
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
}
private fun getApiKey(isProd: Boolean): String {

View file

@ -2,12 +2,14 @@ package com.tangem.datasource.api.common.config
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
/** TangemTech [ApiConfig] */
internal class TangemTech(
private val appVersionProvider: AppVersionProvider,
private val authProvider: AuthProvider,
private val appInfoProvider: AppInfoProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
@ -30,7 +32,7 @@ internal class TangemTech(
)
private fun createHeaders() = buildMap {
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values)
putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider, appInfoProvider).values)
putAll(from = RequestHeader.AuthenticationHeader(authProvider).values)
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.datasource.api.common.config.*
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
@ -22,8 +23,14 @@ internal object ApiConfigsModule {
environmentConfigStorage: EnvironmentConfigStorage,
expressAuthProvider: ExpressAuthProvider,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig {
return Express(environmentConfigStorage, expressAuthProvider, appVersionProvider)
return Express(
environmentConfigStorage = environmentConfigStorage,
expressAuthProvider = expressAuthProvider,
appVersionProvider = appVersionProvider,
appInfoProvider = appInfoProvider,
)
}
@Provides
@ -34,8 +41,15 @@ internal object ApiConfigsModule {
@Provides
@IntoSet
fun provideTangemTechConfig(appVersionProvider: AppVersionProvider, authProvider: AuthProvider): ApiConfig =
TangemTech(appVersionProvider, authProvider)
fun provideTangemTechConfig(
appVersionProvider: AppVersionProvider,
authProvider: AuthProvider,
appInfoProvider: AppInfoProvider,
): ApiConfig = TangemTech(
appVersionProvider = appVersionProvider,
authProvider = authProvider,
appInfoProvider = appInfoProvider,
)
@Provides
@IntoSet

View file

@ -23,6 +23,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.utils.*
import com.tangem.datasource.utils.RequestHeader.AppVersionPlatformHeaders
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
@ -162,6 +163,7 @@ internal object NetworkModule {
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
): TangemTechApiV2 {
return provideTangemTechApiInternal(
moshi = moshi,
@ -169,6 +171,7 @@ internal object NetworkModule {
appVersionProvider = appVersionProvider,
baseUrl = PROD_V2_TANGEM_TECH_BASE_URL,
analyticsErrorHandler = analyticsErrorHandler,
appInfoProvider = appInfoProvider,
)
}
@ -218,15 +221,17 @@ internal object NetworkModule {
)
}
@Suppress("LongParameterList")
@Deprecated("use createApi instead")
private inline fun <reified T> provideTangemTechApiInternal(
moshi: Moshi,
context: Context,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
baseUrl: String,
analyticsErrorHandler: AnalyticsErrorHandler,
timeouts: Timeouts = Timeouts(),
requestHeaders: List<RequestHeader> = listOf(AppVersionPlatformHeaders(appVersionProvider)),
requestHeaders: List<RequestHeader> = listOf(AppVersionPlatformHeaders(appVersionProvider, appInfoProvider)),
): T {
val client = OkHttpClient.Builder()
.applyTimeoutAnnotations()

View file

@ -4,6 +4,7 @@ import android.os.Build
import com.tangem.datasource.api.common.AuthProvider
import com.tangem.datasource.utils.RequestHeader.CacheControlHeader.checkHeaderValueOrEmpty
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import java.util.Locale
import java.util.TimeZone
@ -25,7 +26,11 @@ sealed class RequestHeader(vararg pairs: Pair<String, ProviderSuspend<String>>)
"card_public_key" to ProviderSuspend(authProvider::getCardPublicKey),
)
class AppVersionPlatformHeaders(appVersionProvider: AppVersionProvider) : RequestHeader(
class AppVersionPlatformHeaders(
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
) : RequestHeader(
"system_version" to ProviderSuspend { appInfoProvider.osVersion },
"version" to ProviderSuspend { appVersionProvider.versionName },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },

View file

@ -13,6 +13,7 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import io.mockk.every
import io.mockk.mockk
@ -29,11 +30,12 @@ private val appVersionProvider = mockk<AppVersionProvider>()
private val expressAuthProvider = mockk<ExpressAuthProvider>()
private val stakeKitAuthProvider = mockk<StakeKitAuthProvider>()
private val appAuthProvider = mockk<AuthProvider>()
private val appInfoProvider = mockk<AppInfoProvider>()
// Don't forget to add new config !!!
private val API_CONFIGS = setOf(
Express(configManager, expressAuthProvider, appVersionProvider),
TangemTech(appVersionProvider, appAuthProvider),
Express(configManager, expressAuthProvider, appVersionProvider, appInfoProvider),
TangemTech(appVersionProvider, appAuthProvider, appInfoProvider),
StakeKit(stakeKitAuthProvider),
TangemVisa(appVersionProvider),
)
@ -53,6 +55,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { appAuthProvider.getCardId() } returns APP_CARD_ID
every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
every { appInfoProvider.osVersion } returns "Android 16"
}
@Test
@ -129,6 +132,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
},
"session-id" to ProviderSuspend { EXPRESS_SESSION_ID },
"version" to ProviderSuspend { VERSION_NAME },
"system_version" to ProviderSuspend { "Android 16" },
"platform" to ProviderSuspend { "android" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
@ -151,6 +155,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) {
"card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY },
"version" to ProviderSuspend { VERSION_NAME },
"platform" to ProviderSuspend { "android" },
"system_version" to ProviderSuspend { "Android 16" },
"language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() },
"timezone" to ProviderSuspend {
TimeZone.getDefault().getDisplayName(false, TimeZone.SHORT).checkHeaderValueOrEmpty()

View file

@ -669,7 +669,7 @@
<string name="onboarding_wallet_info_title_second">Identische Karten oder Ring</string>
<string name="onboarding_wallet_info_title_third">Zugangscode</string>
<string name="onramp_avaiable_with_payment_methods">Verfügbar mit %s</string>
<string name="onramp_choose_provider_title_hint">Der Kauf von Kryptowährungen bei Tangem wird von Drittanbietern zu deren Bedingungen durchgeführt.</string>
<string name="onramp_choose_provider_title_hint">Anbieter erleichtern Transaktionen</string>
<string name="onramp_country_search">Suche nach Land</string>
<string name="onramp_country_unavailable">Nicht verfügbar</string>
<string name="onramp_currency_other">Andere Währungen</string>
@ -696,6 +696,10 @@
<string name="organize_tokens_sort_by_balance">Nach Guthaben</string>
<string name="organize_tokens_title">Token organisieren</string>
<string name="organize_tokens_ungroup">Gruppe löschen</string>
<string name="push_notifications_permission_alert_description">Du kannst Benachrichtigungen für Tangem in den Einstellungen aktivieren.</string>
<string name="push_notifications_permission_alert_negative_button">Später aktivieren</string>
<string name="push_notifications_permission_alert_positive_button">Einstellungen</string>
<string name="push_notifications_permission_alert_title">Benachrichtigungen aktivieren</string>
<string name="qr_scanner_camera_denied_gallery_button">Wählen aus der Galerie aus</string>
<string name="qr_scanner_camera_denied_settings_button">Einstellungen</string>
<string name="qr_scanner_camera_denied_text">Du hast keinen Zugriff auf deine Kamera gewährt</string>
@ -867,7 +871,7 @@
<string name="staking_details_auto_claiming_rewards_daily_text">Belohnungen sammeln sich täglich automatisch in deinem Staking-Konto an.</string>
<string name="staking_details_available">Verfügbar</string>
<string name="staking_details_average_reward_rate">Durchschnittliche Belohnungsquote</string>
<string name="staking_details_banner_text">Was ist Staking?</string>
<string name="staking_details_banner_text">Wie funktioniert Staking?</string>
<string name="staking_details_estimated_profit">%s geschätzter Profit</string>
<string name="staking_details_market_rating">Marktposition</string>
<string name="staking_details_metrics_block_header">Metriken</string>
@ -899,10 +903,10 @@
<string name="staking_notification_earn_rewards_text_daily">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_text_hourly">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_text_monthly">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_text_period_day">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden tag.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Stunde.</string>
<string name="staking_notification_earn_rewards_text_period_month">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jeden Monat.</string>
<string name="staking_notification_earn_rewards_text_period_week">Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche.</string>
<string name="staking_notification_earn_rewards_text_period_day">Durch Staking kannst Du %1$s erhalten. Deine Staking-Belohnungen kommen täglich an.</string>
<string name="staking_notification_earn_rewards_text_period_hour">Durch Staking kannst Du %1$s erhalten. Deine Staking-Belohnungen werden stündlich gutgeschrieben.</string>
<string name="staking_notification_earn_rewards_text_period_month">Durch Staking kannst Du %1$s erhalten. Deine Staking-Belohnungen werden Dir jeden Monat gutgeschrieben.</string>
<string name="staking_notification_earn_rewards_text_period_week">Durch Staking kannst Du %1$s erhalten. Deine Staking-Belohnungen kommen jede Woche.</string>
<string name="staking_notification_earn_rewards_text_weekly">Tangem ermöglicht es Benutzern, ihre Kryptowährungen zu Staken</string>
<string name="staking_notification_earn_rewards_title">Verdiene Staking-Belohnungen</string>
<string name="staking_notification_low_staked_balance_text">Dein verbleibender Stakingbetrag ist zu niedrig, um den Einsatz aufzuheben. Du musst mehr Staken, um den Mindestbetrag zum Aufheben des Einsatzes zu erreichen.</string>
@ -1258,6 +1262,8 @@
<string name="wc_alert_session_disconnected_title">WalletConnect-Sitzung wurde getrennt</string>
<string name="wc_alert_unknown_error_description">Fehlercode: %s. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support.</string>
<string name="wc_alert_unknown_error_title">Wir haben einen unbekannten Fehler festgestellt.</string>
<string name="wc_alert_unsupported_method_description">Fehlercode: 8 005. Wenn das Problem weiterhin besteht, wende Dich bitte an unseren Support.</string>
<string name="wc_alert_unsupported_method_title">Wir haben einen unbekannten Fehler festgestellt.</string>
<string name="wc_alert_unsupported_networks_description">Tangem unterstützt derzeit das erforderliches Netzwerk von %s nicht.</string>
<string name="wc_alert_unsupported_networks_title">Nicht unterstützte Netzwerke</string>
<string name="wc_alert_verified_domain_description">Tangem unterstützt ein erforderliches Netzwerk um %s.</string>

View file

@ -518,7 +518,7 @@
<string name="nft_about_title">NFTについて</string>
<string name="nft_asset">NFTアセット</string>
<plurals name="nft_collections_count">
<item quantity="other">アイテム</item>
<item quantity="other">%d アイテム</item>
</plurals>
<string name="nft_collections_empty_description">ウォレットに送信されたNFTがここに表示されます。</string>
<string name="nft_collections_empty_title">コレクションはまだありません</string>
@ -687,6 +687,10 @@
<string name="organize_tokens_sort_by_balance">残高順</string>
<string name="organize_tokens_title">トークンを整理する</string>
<string name="organize_tokens_ungroup">グループ解除</string>
<string name="push_notifications_permission_alert_description">Tangemの通知は設定で有効にできます。</string>
<string name="push_notifications_permission_alert_negative_button">後で有効にする</string>
<string name="push_notifications_permission_alert_positive_button">設定</string>
<string name="push_notifications_permission_alert_title">通知を有効にする</string>
<string name="qr_scanner_camera_denied_gallery_button">ギャラリーから選択</string>
<string name="qr_scanner_camera_denied_settings_button">設定</string>
<string name="qr_scanner_camera_denied_text">カメラへのアクセスを許可していません。</string>
@ -1242,6 +1246,8 @@
<string name="wc_alert_session_disconnected_title">WalletConnectセッションが接続解除されました</string>
<string name="wc_alert_unknown_error_description">エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。</string>
<string name="wc_alert_unknown_error_title">不明なエラーが発生しました</string>
<string name="wc_alert_unsupported_method_description">エラーコード: 8 005。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。</string>
<string name="wc_alert_unsupported_method_title">不明なエラーが発生しました</string>
<string name="wc_alert_unsupported_networks_description">Tangemは現在%sで必要なネットワークをサポートしていません。</string>
<string name="wc_alert_unsupported_networks_title">サポートされていないネットワーク</string>
<string name="wc_alert_verified_domain_description">Tangemは%sで必要なネットワークをサポートします。</string>

View file

@ -701,6 +701,11 @@
<string name="organize_tokens_sort_by_balance">By balance</string>
<string name="organize_tokens_title">Organize tokens</string>
<string name="organize_tokens_ungroup">Ungroup</string>
<string name="push_notifications_more_info">More info</string>
<string name="push_notifications_permission_alert_description">You can enable Notifications for Tangem in Settings.</string>
<string name="push_notifications_permission_alert_negative_button">Enable Later</string>
<string name="push_notifications_permission_alert_positive_button">Settings</string>
<string name="push_notifications_permission_alert_title">Enable Notifications</string>
<string name="qr_scanner_camera_denied_gallery_button">Select from the gallery</string>
<string name="qr_scanner_camera_denied_settings_button">Settings</string>
<string name="qr_scanner_camera_denied_text">You have not given access to your camera</string>

View file

@ -1,4 +1,4 @@
package com.tangem.features.markets.details.impl.ui.components
package com.tangem.core.ui.components.items
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -7,9 +7,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
@ -18,15 +20,15 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.PreviewShimmerContainer
import com.tangem.features.markets.impl.R
import com.tangem.utils.StringsSigns
@Composable
internal fun Description(
fun DescriptionItem(
description: TextReference,
hasFullDescription: Boolean,
onReadMoreClick: () -> Unit,
modifier: Modifier = Modifier,
textStyle: TextStyle = TangemTheme.typography.body2,
) {
if (hasFullDescription) {
val text = buildAnnotatedString {
@ -51,20 +53,20 @@ internal fun Description(
onClick = onReadMoreClick,
),
text = text,
style = TangemTheme.typography.body2,
style = textStyle,
)
} else {
Text(
modifier = modifier,
text = description.resolveReference(),
style = TangemTheme.typography.body2,
style = textStyle,
color = TangemTheme.colors.text.secondary,
)
}
}
@Composable
internal fun DescriptionPlaceholder(modifier: Modifier = Modifier) {
public fun DescriptionPlaceholder(modifier: Modifier = Modifier) {
Column(
modifier = modifier,
) {
@ -90,7 +92,7 @@ internal fun DescriptionPlaceholder(modifier: Modifier = Modifier) {
@Composable
private fun ContentPreview() {
TangemThemePreview {
Description(
DescriptionItem(
description = stringReference(
"XRP (XRP) is a cryptocurrency launched in January 2009, where the first " +
"genesis block was mined on 9th January 2009",

View file

@ -46,18 +46,23 @@ class UserTokensSaver constructor(
push(userWalletId, enrichedUserTokensResponse, false)
}
suspend fun push(userWalletId: UserWalletId, response: UserTokensResponse, useEnricher: Boolean = true) =
withContext(dispatchers.io) {
val enrichedUserTokensResponse = if (useEnricher) {
userTokensResponseAddressesEnricher(
userWalletId = userWalletId,
response = response,
)
} else {
response
}
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedUserTokensResponse).bind() }) {
Timber.e(it, "Unable to push user tokens for: ${userWalletId.stringValue}")
}
suspend fun push(
userWalletId: UserWalletId,
response: UserTokensResponse,
useEnricher: Boolean = true,
onFailSend: () -> Unit = {},
) = withContext(dispatchers.io) {
val enrichedUserTokensResponse = if (useEnricher) {
userTokensResponseAddressesEnricher(
userWalletId = userWalletId,
response = response,
)
} else {
response
}
safeApiCall({ tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedUserTokensResponse).bind() }) {
Timber.e(it, "Unable to push user tokens for: ${userWalletId.stringValue}")
onFailSend()
}
}
}

View file

@ -110,66 +110,42 @@ class UserTokensSaverTest {
}
@Test
fun `GIVEN user wallet id and response WHEN push THEN should push enriched response`() = runTest {
// GIVEN
val userWalletId = UserWalletId("1234567890abcdef")
val response = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse
coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns ApiResponse.Success(Unit)
fun `GIVEN user wallet id and response WHEN push AND api call fails THEN should log error and call onFailSend`() =
runTest {
// GIVEN
val userWalletId = UserWalletId("1234567890abcdef")
val response = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val error = ApiResponseError.UnknownException(Exception("API Error"))
var onFailSendCalled = false
coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse
coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns
ApiResponse.Error(error) as ApiResponse<Unit>
// WHEN
userTokensSaver.push(userWalletId, response)
// WHEN
userTokensSaver.push(
userWalletId = userWalletId,
response = response,
onFailSend = { onFailSendCalled = true },
)
// THEN
coVerify {
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
// THEN
coVerify {
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
}
coVerify(exactly = 0) {
dataStore.updateData(any())
}
assert(onFailSendCalled) { "onFailSend callback should be called when API call fails" }
}
coVerify(exactly = 0) {
dataStore.updateData(any())
}
}
@Test
fun `GIVEN user wallet id and response WHEN push AND api call fails THEN should log error`() = runTest {
// GIVEN
val userWalletId = UserWalletId("1234567890abcdef")
val response = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val enrichedResponse = UserTokensResponse(
version = 0,
group = UserTokensResponse.GroupType.NETWORK,
sort = UserTokensResponse.SortType.BALANCE,
tokens = emptyList(),
)
val error = ApiResponseError.UnknownException(Exception("API Error"))
coEvery { userTokensResponseAddressesEnricher(userWalletId, response) } returns enrichedResponse
coEvery { tangemTechApi.saveUserTokens(any(), any()) } returns
ApiResponse.Error(error) as ApiResponse<Unit>
// WHEN
userTokensSaver.push(userWalletId, response)
// THEN
coVerify {
tangemTechApi.saveUserTokens(userWalletId.stringValue, enrichedResponse)
}
coVerify(exactly = 0) {
dataStore.updateData(any())
}
}
}

View file

@ -548,6 +548,16 @@ internal class DefaultCurrenciesRepository(
return blockchain?.isNetworkFeeZero() ?: false
}
override suspend fun syncTokens(userWalletId: UserWalletId) {
val savedCurrencies = requireNotNull(
value = getSavedUserTokensResponseSync(key = userWalletId),
lazyMessage = { "Saved tokens empty. Can not perform add currencies action" },
)
userTokensSaver.push(userWalletId, savedCurrencies, onFailSend = {
throw IllegalStateException("Unable to push tokens")
},)
}
private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow<List<CryptoCurrency>> {
return getSavedUserTokensResponse(userWallet.walletId).map { storedTokens ->
responseCurrenciesFactory.createCurrencies(

View file

@ -238,6 +238,10 @@ internal class DefaultWalletsRepository(
}
}
override fun notificationsEnabledStatus(userWalletId: UserWalletId): Flow<Boolean> = appPreferencesStore
.getObjectMap<Boolean>(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY)
.map { it[userWalletId.stringValue] == true }
override suspend fun isNotificationsEnabled(userWalletId: UserWalletId): Boolean =
appPreferencesStore.getObjectMap<Boolean>(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY)
.map { it[userWalletId.stringValue] }

View file

@ -8,6 +8,7 @@ import com.tangem.domain.tokens.model.FeePaidCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlin.jvm.Throws
/**
* Repository for everything related to the tokens of user wallet
@ -254,4 +255,15 @@ interface CurrenciesRepository {
): Flow<Map<UserWallet, List<CryptoCurrency>>>
fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean
/**
* Synchronizes local tokens with remote data for a specific user wallet.
* This method ensures that the local token list matches the remote state by fetching
* the token data from the local cache and push it to backend.
*
* @param userWalletId The unique identifier of the user wallet to sync tokens for.
* @throws Exception if the sync request to the backend fails
*/
@Throws
suspend fun syncTokens(userWalletId: UserWalletId)
}

View file

@ -163,4 +163,8 @@ internal class MockCurrenciesRepository(
override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean {
return false
}
override suspend fun syncTokens(userWalletId: UserWalletId) {
return Unit
}
}

View file

@ -41,6 +41,8 @@ interface WalletsRepository {
suspend fun disableNFT(userWalletId: UserWalletId)
fun notificationsEnabledStatus(userWalletId: UserWalletId): Flow<Boolean>
suspend fun isNotificationsEnabled(userWalletId: UserWalletId): Boolean
suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean)

View file

@ -0,0 +1,13 @@
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 GetWalletNotificationsEnabledUseCase(
private val walletsRepository: WalletsRepository,
) {
operator fun invoke(userWalletId: UserWalletId): Flow<Boolean> = walletsRepository
.notificationsEnabledStatus(userWalletId)
}

View file

@ -1,11 +1,13 @@
package com.tangem.domain.wallets.usecase
import arrow.core.Either
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
class SetNotificationsEnabledUseCase(
private val walletsRepository: WalletsRepository,
private val currenciesRepository: CurrenciesRepository,
) {
suspend operator fun invoke(userWalletId: UserWalletId, isEnabled: Boolean): Either<Throwable, Unit> =
@ -14,5 +16,11 @@ class SetNotificationsEnabledUseCase(
userWalletId = userWalletId,
isEnabled = isEnabled,
)
currenciesRepository.syncTokens(userWalletId)
}.onLeft {
walletsRepository.setNotificationsEnabled(
userWalletId = userWalletId,
isEnabled = !isEnabled,
)
}
}

View file

@ -10,6 +10,8 @@ import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.core.ui.components.UnableToLoadData
import com.tangem.core.ui.components.items.DescriptionItem
import com.tangem.core.ui.components.items.DescriptionPlaceholder
internal fun LazyListScope.tokenMarketDetailsBody(
state: MarketsTokenDetailsUM.Body,
@ -69,7 +71,7 @@ private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) {
private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) {
item("description") {
Description(
DescriptionItem(
modifier = Modifier.blockPaddings(),
description = description.shortDescription,
hasFullDescription = description.fullDescription != null,

View file

@ -35,6 +35,7 @@ dependencies {
implementation(projects.domain.wallets.models)
implementation(projects.domain.demo)
implementation(projects.domain.nft)
implementation(projects.domain.notifications.toggles)
/* AndroidX */
implementation(deps.androidx.fragment.ktx)

View file

@ -31,6 +31,10 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
onLinkMoreCardsClick = {},
onReferralClick = {},
isManageTokensAvailable = true,
isNotificationsEnabled = true,
isNotificationsFeatureEnabled = true,
onCheckedNotificationsChanged = {},
onNotificationsDescriptionClick = {},
),
)

View file

@ -30,4 +30,11 @@ internal sealed class WalletSettingsItemUM {
val isEnabled: Boolean,
val onClick: () -> Unit,
) : WalletSettingsItemUM()
data class DescriptionWithMore(
override val id: String,
val text: TextReference,
val more: TextReference,
val onClick: () -> Unit,
) : WalletSettingsItemUM()
}

View file

@ -22,10 +22,9 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.nft.DisableWalletNFTUseCase
import com.tangem.domain.nft.EnableWalletNFTUseCase
import com.tangem.domain.nft.GetWalletNFTEnabledUseCase
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.walletsettings.analytics.Settings
import com.tangem.feature.walletsettings.component.WalletSettingsComponent
import com.tangem.feature.walletsettings.entity.DialogConfig
@ -60,6 +59,9 @@ internal class WalletSettingsModel @Inject constructor(
private val getWalletNFTEnabledUseCase: GetWalletNFTEnabledUseCase,
private val enableWalletNFTUseCase: EnableWalletNFTUseCase,
private val disableWalletNFTUseCase: DisableWalletNFTUseCase,
private val notificationsToggles: NotificationsFeatureToggles,
private val getWalletNotificationsEnabledUseCase: GetWalletNotificationsEnabledUseCase,
private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@ -73,23 +75,27 @@ internal class WalletSettingsModel @Inject constructor(
)
init {
getWalletUseCase.invokeFlow(params.userWalletId)
.distinctUntilChanged()
.combine(getWalletNFTEnabledUseCase.invoke(params.userWalletId)) { maybeWallet, nftEnabled ->
val wallet = maybeWallet.getOrNull() ?: return@combine
val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase()
state.update { value ->
value.copy(
items = buildItems(
userWallet = wallet,
dialogNavigation = dialogNavigation,
isRenameWalletAvailable = isRenameWalletAvailable,
isNFTFeatureEnabled = nftFeatureToggles.isNFTEnabled,
isNFTEnabled = nftEnabled,
),
)
}
combine(
getWalletUseCase.invokeFlow(params.userWalletId).distinctUntilChanged(),
getWalletNFTEnabledUseCase.invoke(params.userWalletId),
getWalletNotificationsEnabledUseCase(params.userWalletId),
) { maybeWallet, nftEnabled, notificationsEnabled ->
val wallet = maybeWallet.getOrNull() ?: return@combine
val isRenameWalletAvailable = getShouldSaveUserWalletsSyncUseCase()
state.update { value ->
value.copy(
items = buildItems(
userWallet = wallet,
dialogNavigation = dialogNavigation,
isRenameWalletAvailable = isRenameWalletAvailable,
isNFTFeatureEnabled = nftFeatureToggles.isNFTEnabled,
isNFTEnabled = nftEnabled,
isNotificationsEnabled = notificationsEnabled,
isNotificationsFeatureEnabled = notificationsToggles.isNotificationsEnabled,
),
)
}
}
.launchIn(modelScope)
}
@ -99,6 +105,8 @@ internal class WalletSettingsModel @Inject constructor(
isRenameWalletAvailable: Boolean,
isNFTFeatureEnabled: Boolean,
isNFTEnabled: Boolean,
isNotificationsFeatureEnabled: Boolean,
isNotificationsEnabled: Boolean,
): PersistentList<WalletSettingsItemUM> = itemsBuilder.buildItems(
userWalletId = userWallet.walletId,
userWalletName = userWallet.name,
@ -129,6 +137,10 @@ internal class WalletSettingsModel @Inject constructor(
onLinkMoreCardsClick(scanResponse = userWallet.scanResponse)
},
onReferralClick = { onReferralClick(userWallet) },
isNotificationsEnabled = isNotificationsEnabled,
isNotificationsFeatureEnabled = isNotificationsFeatureEnabled,
onCheckedNotificationsChanged = ::onCheckedNotificationsChange,
onNotificationsDescriptionClick = ::onNotificationsDescriptionClick,
)
private fun openRenameWalletDialog(userWallet: UserWallet, dialogNavigation: SlotNavigation<DialogConfig>) {
@ -180,6 +192,17 @@ internal class WalletSettingsModel @Inject constructor(
}
}
private fun onCheckedNotificationsChange(isChecked: Boolean) {
// TODO [REDACTED_JIRA]
modelScope.launch {
setNotificationsEnabledUseCase(params.userWalletId, isChecked)
}
}
private fun onNotificationsDescriptionClick() {
// TODO [REDACTED_JIRA]
}
private fun onReferralClick(userWallet: UserWallet) {
if (isDemoCardUseCase(userWallet.cardId)) {
messageSender.send(

View file

@ -13,11 +13,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.TangemSwitch
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.BlockItem
import com.tangem.core.ui.components.items.DescriptionItem
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -98,6 +101,12 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) {
modifier = itemModifier,
model = item,
)
is WalletSettingsItemUM.DescriptionWithMore -> DescriptionWithMoreBlock(
modifier = itemModifier,
model = item,
yOffset = DESCRIPTION_OFFSET,
onDescriptionClick = item.onClick,
)
}
}
}
@ -195,6 +204,26 @@ private fun SwitchBlock(model: WalletSettingsItemUM.WithSwitch, modifier: Modifi
}
}
@Composable
private fun DescriptionWithMoreBlock(
model: WalletSettingsItemUM.DescriptionWithMore,
onDescriptionClick: () -> Unit,
modifier: Modifier = Modifier,
yOffset: Dp = 0.dp,
) {
DescriptionItem(
modifier = modifier
.offset(y = yOffset)
.padding(horizontal = 12.dp),
description = model.text,
hasFullDescription = true,
textStyle = TangemTheme.typography.caption2,
onReadMoreClick = onDescriptionClick,
)
}
private val DESCRIPTION_OFFSET = -8.dp
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)

View file

@ -34,6 +34,10 @@ internal class ItemsBuilder @Inject constructor(
isNFTFeatureEnabled: Boolean,
isNFTEnabled: Boolean,
onCheckedNFTChange: (Boolean) -> Unit,
isNotificationsFeatureEnabled: Boolean,
isNotificationsEnabled: Boolean,
onCheckedNotificationsChanged: (Boolean) -> Unit,
onNotificationsDescriptionClick: () -> Unit,
forgetWallet: () -> Unit,
renameWallet: () -> Unit,
onLinkMoreCardsClick: () -> Unit,
@ -57,6 +61,22 @@ internal class ItemsBuilder @Inject constructor(
onReferralClick = onReferralClick,
),
)
.run {
if (isNotificationsFeatureEnabled) {
add(
buildNotificationsSwitchItem(
isNotificationsEnabled,
onCheckedNotificationsChanged,
),
).add(
buildNotificationsDescriptionItem(
onDescriptionClick = onNotificationsDescriptionClick,
),
)
} else {
this
}
}
.add(buildForgetItem(forgetWallet))
private fun buildNameItem(walletName: String, isRenameWalletAvailable: Boolean, renameWallet: () -> Unit) =
@ -76,6 +96,22 @@ internal class ItemsBuilder @Inject constructor(
onCheckedChange = onCheckedNFTChange,
)
private fun buildNotificationsSwitchItem(isNFTEnabled: Boolean, onCheckedNFTChange: (Boolean) -> Unit) =
WalletSettingsItemUM.WithSwitch(
id = "notifications",
title = resourceReference(id = R.string.wallet_settings_push_notifications_title),
isChecked = isNFTEnabled,
onCheckedChange = onCheckedNFTChange,
)
private fun buildNotificationsDescriptionItem(onDescriptionClick: () -> Unit) =
WalletSettingsItemUM.DescriptionWithMore(
id = "notifications_description",
text = resourceReference(id = R.string.wallet_settings_push_notifications_description),
more = resourceReference(id = R.string.push_notifications_more_info),
onClick = onDescriptionClick,
)
@Suppress("LongParameterList")
private fun buildCardItem(
userWalletId: UserWalletId,