Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-22 12:48:29 +03:00
commit a1b910cc7e
149 changed files with 4442 additions and 1330 deletions

View file

@ -50,6 +50,7 @@ dependencies {
implementation(projects.domain.appTheme.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.transaction)
implementation(projects.common)
implementation(projects.core.analytics)

View file

@ -30,7 +30,6 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.common.LogConfig
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
@ -209,9 +208,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var userTokensStore: UserTokensStore
@Inject
lateinit var appRatingRepository: AppRatingRepository
@Inject
lateinit var getAppThemeModeUseCase: GetAppThemeModeUseCase
@ -248,10 +244,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
runBlocking {
walletsRepository.initialize()
initUserWalletsListManager()
featureTogglesManager.init()
appRatingRepository.initialize()
// learn2earnInteractor.init()
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.di.domain
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
internal object TransactionDomainModule {
@Provides
@ViewModelScoped
fun provideGetUseCase(
walletManagersFacade: WalletManagersFacade,
dispatchers: CoroutineDispatcherProvider,
): GetFeeUseCase {
return GetFeeUseCase(walletManagersFacade, dispatchers)
}
}

View file

@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
import com.tangem.tap.features.details.ui.common.TangemSwitch
import com.tangem.core.ui.components.TangemSwitch
@Composable
internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
@ -25,7 +26,6 @@ import coil.request.ImageRequest
import com.tangem.core.ui.components.CurrencyPlaceholderIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
import com.tangem.tap.common.compose.extensions.toPx
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
import com.tangem.wallet.R
import kotlinx.coroutines.launch
@ -126,29 +126,37 @@ internal fun TokenItem(model: TokenItemState) {
@Composable
private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> Unit, modifier: Modifier = Modifier) {
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size46.roundToPx() }
val iconModifier = modifier
.size(size = TangemTheme.dimens.size46)
.clip(TangemTheme.shapes.roundedCorners8)
val screenBackgroundColor = TangemTheme.colors.background.primary.toArgb()
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
SubcomposeAsyncImage(
modifier = iconModifier,
model = ImageRequest.Builder(context = LocalContext.current)
.size(size = TangemTheme.dimens.size46.toPx().toInt())
.data(data = iconUrl)
.size(size = pixelsSize)
.memoryCacheKey(key = iconUrl + pixelsSize)
.crossfade(enable = true)
.allowHardware(false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
if (!isBackgroundColorDefined && isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = screenBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
backgroundColor = itemBackgroundColor,
size = pixelsSize,
).getContrastColor(isDarkTheme = true)
onContrastCalculate(color)
iconBackgroundColor = color
isBackgroundColorDefined = true
}
}
},
@ -189,13 +197,12 @@ private fun Subtitle(isExpanded: Boolean, modifier: Modifier = Modifier) {
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ChangeNetworksViewButton(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
IconButton(onClick = onClick, modifier = modifier.size(size = TangemTheme.dimens.size46)) {
AnimatedContent(
targetState = isExpanded,
transitionSpec = { fadeIn() + scaleIn() with scaleOut() + fadeOut() },
transitionSpec = { (fadeIn() + scaleIn()).togetherWith(scaleOut() + fadeOut()) },
) { isExpanded ->
Icon(
painter = painterResource(

View file

@ -4,7 +4,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.lib.auth.AuthProvider
import com.tangem.tap.proxy.AppStateHolder
class AuthProviderImpl(private val appStateHolder: AppStateHolder) : AuthProvider {
internal class DefaultAuthProvider(private val appStateHolder: AppStateHolder) : AuthProvider {
override fun getCardPublicKey(): String {
return appStateHolder.scanResponse?.card?.cardPublicKey?.toHexString() ?: ""

View file

@ -0,0 +1,32 @@
package com.tangem.tap.network.auth
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator
import java.util.UUID
import java.util.concurrent.atomic.AtomicReference
internal class DefaultExpressAuthProvider(
private val userWalletsStore: UserWalletsStore,
private val configManager: ConfigManager,
) : ExpressAuthProvider, ExpressSessionIdGenerator {
private var uuid = AtomicReference(UUID.randomUUID())
override fun getApiKey(): String {
return configManager.config.tangemExpressApiKey
}
override fun getUserId(): String {
return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: ""
}
override fun getSessionId(): String {
return uuid.get().toString()
}
override fun generateNewSessionId() {
uuid = AtomicReference(UUID.randomUUID())
}
}

View file

@ -1,7 +1,11 @@
package com.tangem.tap.network.auth.di
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.lib.auth.AuthProvider
import com.tangem.tap.network.auth.AuthProviderImpl
import com.tangem.lib.auth.ExpressAuthProvider
import com.tangem.tap.network.auth.DefaultAuthProvider
import com.tangem.tap.network.auth.DefaultExpressAuthProvider
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
import dagger.Provides
@ -16,6 +20,18 @@ class AuthModule {
@Provides
@Singleton
fun provideAuthProvider(appStateHolder: AppStateHolder): AuthProvider {
return AuthProviderImpl(appStateHolder)
return DefaultAuthProvider(appStateHolder)
}
@Provides
@Singleton
fun provideExpressAuthProvider(
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
): ExpressAuthProvider {
return DefaultExpressAuthProvider(
userWalletsStore = userWalletsStore,
configManager = configManager,
)
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.datasource.api.express.models.request.PairsRequestBody
import com.tangem.datasource.api.express.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Query
import java.math.BigDecimal
@ -17,14 +16,8 @@ import java.math.BigDecimal
@Suppress("LongParameterList")
interface ExpressApi {
// TODO move first three params to retrofit interceptor
@POST("assets")
suspend fun getAssets(
@Header("api-key") apiKey: String,
@Header("user-id") userId: String,
@Header("session-id") sessionId: String,
@Body body: AssetsRequestBody,
): ApiResponse<List<Asset>>
suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse<List<Asset>>
@POST("pairs")
suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse<List<SwapPair>>
@ -55,6 +48,6 @@ interface ExpressApi {
@Query("toAddress") toAddress: String,
): ApiResponse<ExchangeDataResponse>
@GET("exchange-results")
@GET("exchange-result")
suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse<ExchangeResultsResponse>
}

View file

@ -106,6 +106,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
swapReferrerAccount = configValues.swapReferrerAccount,
walletConnectProjectId = configValues.walletConnectProjectId,
tangemComAuthorization = configValues.tangemComAuthorization,
tangemExpressApiKey = configValues.tangemExpressApiKey,
)
}
}

View file

@ -19,4 +19,5 @@ data class Config(
val swapReferrerAccount: SwapReferrerAccount? = null,
val walletConnectProjectId: String = "",
val tangemComAuthorization: String? = null,
val tangemExpressApiKey: String = "",
)

View file

@ -40,6 +40,7 @@ class ConfigValueModel(
val tangemComAuthorization: String?,
val chiaFireAcademyApiKey: String?,
val chiaTangemApiKey: String?,
val tangemExpressApiKey: String,
)
data class AppsFlyer(

View file

@ -1,18 +1,11 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.appcurrency.implementation.DefaultAvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.implementation.DefaultSelectedAppCurrencyStore
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@ -23,23 +16,6 @@ internal object AppCurrencyDataModule {
@Provides
@Singleton
fun provideAvailableAppCurrenciesStore(): AvailableAppCurrenciesStore {
return DefaultAvailableAppCurrenciesStore(
dataStore = RuntimeDataStore(),
)
}
@Provides
@Singleton
fun provideSelectedAppCurrencyStore(
@ApplicationContext context: Context,
@NetworkMoshi moshi: Moshi,
): SelectedAppCurrencyStore {
return DefaultSelectedAppCurrencyStore(
dataStore = JsonSharedPreferencesDataStore(
preferencesName = "selected_app_currency",
context = context,
adapter = moshi.adapter(CurrenciesResponse.Currency::class.java),
),
)
return DefaultAvailableAppCurrenciesStore(dataStore = RuntimeDataStore())
}
}

View file

@ -1,29 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.apptheme.AppThemeModeStore
import com.tangem.datasource.local.apptheme.DefaultAppThemeModeStore
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
import com.tangem.domain.apptheme.model.AppThemeMode
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object AppThemeModeDataModule {
@Provides
fun provideAppThemeModeStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): AppThemeModeStore {
return DefaultAppThemeModeStore(
dataStore = JsonSharedPreferencesDataStore(
preferencesName = "app_theme",
context = context,
adapter = moshi.adapter(AppThemeMode::class.java),
),
)
}
}

View file

@ -1,34 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.tangem.datasource.local.card.DefaultUsedCardsStore
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.card.UsedCardsStore
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object CardDataModule {
@Provides
@Singleton
fun provideUsedCardsStore(@ApplicationContext context: Context, @NetworkMoshi moshi: Moshi): UsedCardsStore {
return DefaultUsedCardsStore(
store = JsonSharedPreferencesDataStore(
preferencesName = "tapPrefs",
context = context,
adapter = moshi.adapter(
Types.newParameterizedType(List::class.java, UsedCardInfo::class.java),
),
),
)
}
}

View file

@ -1,32 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
import com.tangem.datasource.local.appcurrency.implementation.BalanceStateHidingSettingsStore
import com.tangem.datasource.local.datastore.JsonSharedPreferencesDataStore
import com.tangem.domain.balancehiding.BalanceHidingSettings
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal object HiddenBalanceDataModule {
@Provides
fun provideHiddenBalanceStateStore(
@ApplicationContext context: Context,
@NetworkMoshi moshi: Moshi,
): BalanceHidingSettingsStore {
return BalanceStateHidingSettingsStore(
dataStore = JsonSharedPreferencesDataStore(
preferencesName = "balance_hiding_settings",
context = context,
adapter = moshi.adapter(BalanceHidingSettings::class.java),
),
)
}
}

View file

@ -3,12 +3,14 @@ package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.ExpressApi
import com.tangem.datasource.api.promotion.PromotionApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.utils.RequestHeader.*
import com.tangem.datasource.utils.addHeaders
import com.tangem.datasource.utils.addLoggers
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -24,6 +26,27 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Provides
@Singleton
fun provideExpressApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
expressAuthProvider: ExpressAuthProvider,
): ExpressApi {
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
.baseUrl(DEV_EXPRESS_BASE_URL)
.client(
OkHttpClient.Builder()
.addHeaders(Express(expressAuthProvider))
.addLoggers(context)
.build(),
)
.build()
.create(ExpressApi::class.java)
}
@Provides
@Singleton
fun provideTangemTechApi(@NetworkMoshi moshi: Moshi, @ApplicationContext context: Context): TangemTechApi {
@ -74,6 +97,9 @@ class NetworkModule {
}
private companion object {
const val PROD_EXPRESS_BASE_URL = "https://express.tangem.com/v1/"
const val DEV_EXPRESS_BASE_URL = "[REDACTED_ENV_URL]"
const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/"
const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/"

View file

@ -1,50 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.LongSharedPreferencesDataStore
import com.tangem.datasource.local.settings.*
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object SettingsDataModule {
@Provides
@Singleton
fun provideAppLaunchCountStore(@ApplicationContext context: Context): AppLaunchCountStore {
return DefaultAppLaunchCountStore(
store = IntSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
@Provides
@Singleton
fun provideAppRatingShowingCountStore(@ApplicationContext context: Context): AppRatingShowingCountStore {
return DefaultAppRatingShowingCountStore(
store = IntSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
@Provides
@Singleton
fun provideFundsFoundDateInMillisStore(@ApplicationContext context: Context): FundsFoundDateInMillisStore {
return DefaultFundsFoundDateInMillisStore(
store = LongSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
@Provides
@Singleton
fun provideUserInteractingStatusStore(@ApplicationContext context: Context): UserInteractingStatusStore {
return DefaultUserInteractingStatusStore(
store = BooleanSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
}

View file

@ -1,26 +0,0 @@
package com.tangem.datasource.di
import android.content.Context
import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore
import com.tangem.datasource.local.settings.*
import com.tangem.datasource.local.userwallet.DefaultShouldSaveUserWalletStore
import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object WalletsDataModule {
@Provides
@Singleton
fun provideShouldSaveUserWalletsStore(@ApplicationContext context: Context): ShouldSaveUserWalletStore {
return DefaultShouldSaveUserWalletStore(
store = BooleanSharedPreferencesDataStore(preferencesName = "tapPrefs", context = context),
)
}
}

View file

@ -5,6 +5,7 @@ package com.tangem.datasource.local
*
[REDACTED_AUTHOR]
*/
@Deprecated(message = "Use AppPreferencesStore", level = DeprecationLevel.WARNING)
interface AppPreferenceStorage {
/** Json config with feature toggles 'ToggleName: String - Availability: Boolean' */

View file

@ -1,13 +0,0 @@
package com.tangem.datasource.local.appcurrency
import com.tangem.domain.balancehiding.BalanceHidingSettings
import kotlinx.coroutines.flow.Flow
interface BalanceHidingSettingsStore {
fun get(): Flow<BalanceHidingSettings>
suspend fun getSyncOrDefault(): BalanceHidingSettings
suspend fun store(settings: BalanceHidingSettings)
}

View file

@ -1,15 +0,0 @@
package com.tangem.datasource.local.appcurrency
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import kotlinx.coroutines.flow.Flow
interface SelectedAppCurrencyStore {
fun get(): Flow<CurrenciesResponse.Currency>
suspend fun getSyncOrNull(): CurrenciesResponse.Currency?
suspend fun store(item: CurrenciesResponse.Currency)
suspend fun isEmpty(): Boolean
}

View file

@ -1,19 +0,0 @@
package com.tangem.datasource.local.appcurrency.implementation
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.balancehiding.BalanceHidingSettings
internal class BalanceStateHidingSettingsStore(
dataStore: StringKeyDataStore<BalanceHidingSettings>,
) : BalanceHidingSettingsStore, KeylessDataStoreDecorator<BalanceHidingSettings>(dataStore) {
override suspend fun getSyncOrDefault(): BalanceHidingSettings {
return getSyncOrNull() ?: BalanceHidingSettings(
isHidingEnabledInSettings = false,
isBalanceHidden = false,
isBalanceHidingNotificationEnabled = true,
)
}
}

View file

@ -1,10 +0,0 @@
package com.tangem.datasource.local.appcurrency.implementation
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
internal class DefaultSelectedAppCurrencyStore(
dataStore: StringKeyDataStore<CurrenciesResponse.Currency>,
) : SelectedAppCurrencyStore, KeylessDataStoreDecorator<CurrenciesResponse.Currency>(dataStore)

View file

@ -1,13 +0,0 @@
package com.tangem.datasource.local.apptheme
import com.tangem.domain.apptheme.model.AppThemeMode
import kotlinx.coroutines.flow.Flow
interface AppThemeModeStore {
fun get(): Flow<AppThemeMode>
suspend fun store(item: AppThemeMode)
suspend fun isEmpty(): Boolean
}

View file

@ -1,9 +0,0 @@
package com.tangem.datasource.local.apptheme
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.domain.apptheme.model.AppThemeMode
internal class DefaultAppThemeModeStore(
dataStore: StringKeyDataStore<AppThemeMode>,
) : AppThemeModeStore, KeylessDataStoreDecorator<AppThemeMode>(dataStore)

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.local.card
import com.tangem.datasource.local.datastore.SharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface UsedCardsStore {
fun get(): Flow<List<UsedCardInfo>>
suspend fun getSyncOrNull(): List<UsedCardInfo>?
suspend fun store(item: List<UsedCardInfo>)
}
internal class DefaultUsedCardsStore(
store: SharedPreferencesDataStore<List<UsedCardInfo>>,
) : UsedCardsStore, KeylessDataStoreDecorator<List<UsedCardInfo>>(
wrappedDataStore = store,
key = "usedCardsInfo_v2",
)

View file

@ -1,16 +0,0 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import androidx.core.content.edit
internal class BooleanSharedPreferencesDataStore(
preferencesName: String,
context: Context,
) : SharedPreferencesDataStore<Boolean>(preferencesName, context) {
override fun getByKey(key: String): Boolean = sharedPreferences.getBoolean(key, false)
override fun storeByKey(key: String, value: Boolean) {
sharedPreferences.edit { putBoolean(key, value) }
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import androidx.core.content.edit
internal class IntSharedPreferencesDataStore(
preferencesName: String,
context: Context,
) : SharedPreferencesDataStore<Int>(preferencesName, context) {
override fun getByKey(key: String): Int = sharedPreferences.getInt(key, 0)
override fun storeByKey(key: String, value: Int) {
sharedPreferences.edit { putInt(key, value) }
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import androidx.core.content.edit
import com.squareup.moshi.JsonAdapter
internal class JsonSharedPreferencesDataStore<Value : Any>(
preferencesName: String,
context: Context,
private val adapter: JsonAdapter<Value>,
) : SharedPreferencesDataStore<Value>(preferencesName, context) {
override fun getByKey(key: String): Value? {
val json = sharedPreferences.getString(key, null) ?: return null
return adapter.fromJson(json)
}
override fun storeByKey(key: String, value: Value) {
val json = adapter.toJson(value)
sharedPreferences.edit { putString(key, json) }
}
}

View file

@ -1,16 +0,0 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import androidx.core.content.edit
internal class LongSharedPreferencesDataStore(
preferencesName: String,
context: Context,
) : SharedPreferencesDataStore<Long>(preferencesName, context) {
override fun getByKey(key: String): Long = sharedPreferences.getLong(key, 0)
override fun storeByKey(key: String, value: Long) {
sharedPreferences.edit { putLong(key, value) }
}
}

View file

@ -1,98 +0,0 @@
package com.tangem.datasource.local.datastore
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
import com.tangem.datasource.local.datastore.utils.Trigger
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapNotNull
import timber.log.Timber
internal abstract class SharedPreferencesDataStore<Value : Any>(
preferencesName: String,
context: Context,
) : StringKeyDataStore<Value> {
protected val sharedPreferences: SharedPreferences by lazy {
context.getSharedPreferences(preferencesName, MODE_PRIVATE)
}
private val writeTrigger = Trigger()
abstract fun getByKey(key: String): Value?
abstract fun storeByKey(key: String, value: Value)
override suspend fun isEmpty(): Boolean {
return sharedPreferences.all.isEmpty()
}
override suspend fun contains(key: String): Boolean {
return sharedPreferences.contains(key)
}
override fun get(key: String): Flow<Value> {
return writeTrigger
.mapNotNull { getInternal(key) }
.distinctUntilChanged()
}
override fun getAll(): Flow<List<Value>> {
throw UnsupportedOperationException("Unknown key")
}
override suspend fun getSyncOrNull(key: String): Value? = getInternal(key)
override suspend fun getAllSyncOrNull(): List<Value> {
throw UnsupportedOperationException("Unknown key")
}
override suspend fun store(key: String, value: Value) {
try {
storeByKey(key, value)
writeTrigger.trigger()
} catch (e: Throwable) {
Timber.e(e, "Unable to edit preferences: $key")
}
}
override suspend fun store(values: Map<String, Value>) {
values.forEach { (key, item) ->
store(key, item)
}
}
override suspend fun remove(key: String) {
sharedPreferences.edit { remove(key) }
writeTrigger.trigger()
}
override suspend fun remove(keys: Collection<String>) {
sharedPreferences.edit {
keys.forEach { key ->
remove(key)
}
}
writeTrigger.trigger()
}
override suspend fun clear() {
sharedPreferences.edit { clear() }
writeTrigger.trigger()
}
private fun getInternal(key: String): Value? {
if (!sharedPreferences.contains(key)) return null
return try {
getByKey(key)
} catch (e: Throwable) {
Timber.e(e, "Unable to get value from preferences: $key")
null
}
}
}

View file

@ -4,11 +4,13 @@ import android.content.Context
import androidx.datastore.core.DataMigration
import androidx.datastore.core.DataStore
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStoreFile
import com.tangem.datasource.local.preferences.PreferencesDataStore.INSTANCE
import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import timber.log.Timber
@ -23,6 +25,8 @@ import kotlin.coroutines.CoroutineContext
internal object PreferencesDataStore {
private const val PREFERENCES_FILE_NAME = "TAP_PREFS"
private const val LEGACY_TAP_PREFS_FILE_NAME = "tapPrefs"
private const val LEGACY_DEFAULT_KEY_NAME = "key"
private var INSTANCE: DataStore<Preferences>? = null
@ -33,7 +37,7 @@ internal object PreferencesDataStore {
private fun create(context: Context, dispatcher: CoroutineContext): DataStore<Preferences> {
return PreferenceDataStoreFactory.create(
corruptionHandler = createCorruptionHandler(),
migrations = createMigrations(),
migrations = createMigrations(context = context),
scope = CoroutineScope(context = dispatcher + SupervisorJob()),
produceFile = { context.preferencesDataStoreFile(name = PREFERENCES_FILE_NAME) },
)
@ -48,7 +52,31 @@ internal object PreferencesDataStore {
)
}
private fun createMigrations(): List<DataMigration<Preferences>> {
return listOf()
private fun createMigrations(context: Context): List<DataMigration<Preferences>> {
return listOf(
SharedPreferencesMigration(
context = context,
sharedPreferencesName = LEGACY_TAP_PREFS_FILE_NAME,
keysToMigrate = getTapPrefKeysToMigrate(),
),
SharedPreferencesKeyMigration(
context = context,
legacyPrefsName = "app_theme",
legacyKeyName = LEGACY_DEFAULT_KEY_NAME,
keyName = PreferencesKeys.APP_THEME_MODE_KEY.name,
),
SharedPreferencesKeyMigration(
context = context,
legacyPrefsName = "selected_app_currency",
legacyKeyName = LEGACY_DEFAULT_KEY_NAME,
keyName = PreferencesKeys.SELECTED_APP_CURRENCY_KEY.name,
),
SharedPreferencesKeyMigration(
context = context,
legacyPrefsName = "balance_hiding_settings",
legacyKeyName = LEGACY_DEFAULT_KEY_NAME,
keyName = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY.name,
),
)
}
}

View file

@ -1,13 +1,49 @@
package com.tangem.datasource.local.preferences
import androidx.datastore.preferences.core.*
import com.tangem.datasource.local.preferences.PreferencesKeys.APP_LAUNCH_COUNT_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.FUNDS_FOUND_DATE_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SAVE_USER_WALLETS_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USED_CARDS_INFO_KEY
import com.tangem.datasource.local.preferences.PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY
/**
* All preferences keys that DataStore<Preferences> is stored.
*
[REDACTED_AUTHOR]
*/
object PreferencesKeys
object PreferencesKeys {
val SAVE_USER_WALLETS_KEY by lazy { booleanPreferencesKey(name = "saveUserWallets") }
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") }
val FUNDS_FOUND_DATE_KEY by lazy { longPreferencesKey(name = "fundsFoundDate") }
val USER_WAS_INTERACT_WITH_RATING_KEY by lazy { booleanPreferencesKey(name = "userWasInteractWithRating") }
val USED_CARDS_INFO_KEY by lazy { stringPreferencesKey(name = "usedCardsInfo_v2") }
val APP_THEME_MODE_KEY by lazy { stringPreferencesKey(name = "appThemeMode") }
val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") }
val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") }
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
internal fun getTapPrefKeysToMigrate(): Set<String> {
return setOf()
return setOf(
SAVE_USER_WALLETS_KEY,
APP_LAUNCH_COUNT_KEY,
SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY,
FUNDS_FOUND_DATE_KEY,
USER_WAS_INTERACT_WITH_RATING_KEY,
USED_CARDS_INFO_KEY,
)
.map(Preferences.Key<*>::name)
.toSet()
}

View file

@ -30,6 +30,14 @@ internal class DefaultQuotesStore(
}
}
override suspend fun getSync(currenciesIds: Set<CryptoCurrency.ID>): Set<StoredQuote> {
return currenciesIds.mapNotNull { currencyId ->
currencyId.rawCurrencyId?.let {
dataStore.getSyncOrNull(it)
}
}.toSet()
}
override suspend fun store(response: QuotesResponse) {
val quotes = response.quotes.mapValues { (id, quote) ->
StoredQuote(id, quote)

View file

@ -9,5 +9,7 @@ interface QuotesStore {
fun get(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<StoredQuote>>
suspend fun getSync(currenciesIds: Set<CryptoCurrency.ID>): Set<StoredQuote>
suspend fun store(response: QuotesResponse)
}

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.local.settings
import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface AppLaunchCountStore {
fun get(): Flow<Int>
suspend fun getSyncOrNull(): Int?
suspend fun store(item: Int)
}
internal class DefaultAppLaunchCountStore(
store: IntSharedPreferencesDataStore,
) : AppLaunchCountStore, KeylessDataStoreDecorator<Int>(
wrappedDataStore = store,
key = "launchCount",
)

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.local.settings
import com.tangem.datasource.local.datastore.IntSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface AppRatingShowingCountStore {
fun get(): Flow<Int>
suspend fun getSyncOrNull(): Int?
suspend fun store(item: Int)
}
internal class DefaultAppRatingShowingCountStore(
store: IntSharedPreferencesDataStore,
) : AppRatingShowingCountStore, KeylessDataStoreDecorator<Int>(
wrappedDataStore = store,
key = "showRatingDialogAtLaunchCount",
)

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.local.settings
import com.tangem.datasource.local.datastore.LongSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface FundsFoundDateInMillisStore {
fun get(): Flow<Long>
suspend fun getSyncOrNull(): Long?
suspend fun store(item: Long)
}
internal class DefaultFundsFoundDateInMillisStore(
store: LongSharedPreferencesDataStore,
) : FundsFoundDateInMillisStore, KeylessDataStoreDecorator<Long>(
wrappedDataStore = store,
key = "fundsFoundDate",
)

View file

@ -1,21 +0,0 @@
package com.tangem.datasource.local.settings
import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
interface UserInteractingStatusStore {
fun get(): Flow<Boolean>
suspend fun getSyncOrNull(): Boolean?
suspend fun store(item: Boolean)
}
internal class DefaultUserInteractingStatusStore(
store: BooleanSharedPreferencesDataStore,
) : UserInteractingStatusStore, KeylessDataStoreDecorator<Boolean>(
wrappedDataStore = store,
key = "userWasInteractWithRating",
)

View file

@ -1,24 +0,0 @@
package com.tangem.datasource.local.userwallet
import com.tangem.datasource.local.datastore.BooleanSharedPreferencesDataStore
import com.tangem.datasource.local.datastore.core.KeylessDataStoreDecorator
import kotlinx.coroutines.flow.Flow
/**
[REDACTED_AUTHOR]
*/
interface ShouldSaveUserWalletStore {
fun get(): Flow<Boolean>
suspend fun getSyncOrNull(): Boolean?
suspend fun store(item: Boolean)
}
internal class DefaultShouldSaveUserWalletStore(
store: BooleanSharedPreferencesDataStore,
) : ShouldSaveUserWalletStore, KeylessDataStoreDecorator<Boolean>(
wrappedDataStore = store,
key = "saveUserWallets",
)

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.utils
import com.tangem.lib.auth.AuthProvider
import com.tangem.lib.auth.ExpressAuthProvider
/**
* Presentation of request header
@ -18,4 +19,10 @@ sealed class RequestHeader(vararg pairs: Pair<String, () -> String>) {
"card_id" to { authProvider.getCardId() },
"card_public_key" to { authProvider.getCardPublicKey() },
)
class Express(expressAuthProvider: ExpressAuthProvider) : RequestHeader(
"api-key" to { expressAuthProvider.getApiKey() },
"user-id" to { expressAuthProvider.getUserId() },
"session-id" to { expressAuthProvider.getSessionId() },
)
}

View file

@ -11,8 +11,9 @@
<string name="alert_failed_to_send_email_title">Не удалось отправить письмо</string>
<string name="alert_failed_to_send_transaction_message">Причина: %s</string>
<string name="alert_failed_to_send_transaction_title">Не могу отправить транзакцию</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">Выбранный кошелёк не поддерживает сеть %1$s</string>
<string name="alert_manage_tokens_unsupported_curve_message">Для активации криптографии сети %1$s необходимо сбросить кошелек до заводских настроек. Пожалуйста, выведите свои средства, чтобы не потерять их, после сброса доступ к текущему кошельку будет невозможен.</string>
<string name="alert_manage_tokens_unsupported_message">Токены в сети Solana не поддерживаются этой картой из-за ограничений прошивки.</string>
<string name="alert_manage_tokens_unsupported_message">Токены в сети %1$s не поддерживаются этой картой из-за ограничений прошивки.</string>
<string name="alert_troubleshooting_scan_card_title">У вас возникли трудности со сканированием карты?</string>
<string name="alert_unsupported_card">Эта карта не предназначена для работы с этим приложением</string>
<string name="app_settings_enable_biometrics_description">Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem</string>
@ -57,10 +58,7 @@
<string name="card_settings_title">Настройки карты</string>
<string name="chat_bot_name">Tangem Bot</string>
<string name="chat_button_title">Чат</string>
<string name="chat_user_action_rate_user">Оценить агента</string>
<string name="chat_user_action_send_log">Отправить логи</string>
<string name="chat_user_actions_title">Пожалуйста, выберите действие</string>
<string name="chat_user_rate_agent_title">Пожалуйста, оцените работу агента</string>
<string name="common_accept">Принять</string>
<string name="common_access_denied">Доступ запрещен</string>
<string name="common_add">Добавить</string>
@ -90,12 +88,12 @@
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
<string name="common_explore">Обозреватель</string>
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_selector_title">Скорость и комиссия</string>
<string name="common_fee_selector_footer">Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции.</string>
<string name="common_fee_selector_option_slow">Медленно</string>
<string name="common_fee_selector_option_market">По рынку</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_custom">Свое</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_market">По рынку</string>
<string name="common_fee_selector_option_slow">Медленно</string>
<string name="common_fee_selector_title">Скорость и комиссия</string>
<string name="common_generate_addresses">Сгенерировать адреса</string>
<string name="common_import">Импортировать</string>
<string name="common_like">Нравится</string>
@ -145,7 +143,10 @@
<string name="custom_token_creation_error_required_field">Обязательное поле</string>
<string name="custom_token_creation_error_wrong_decimals">Количество знаков после запятой должно быть корректным числом не больше %d</string>
<string name="custom_token_custom_derivation">Своя деривация</string>
<string name="custom_token_custom_derivation_placeholder">Например m/00\'/0000\'/0\'/0/0</string>
<string name="custom_token_custom_derivation_title">Введите свою деривацию</string>
<string name="custom_token_decimals_input_title">Знаков после запятой</string>
<string name="custom_token_derivation_path">Путь деривации</string>
<string name="custom_token_derivation_path_default">По умолчанию</string>
<string name="custom_token_derivation_path_input_title">Деривация по BIP44</string>
<string name="custom_token_invalid_derivation_path">Введенный путь деривации некорректен</string>
@ -153,11 +154,15 @@
<string name="custom_token_name_input_title">Название токена</string>
<string name="custom_token_network_input_not_selected">Не выбрано</string>
<string name="custom_token_network_input_title">Сеть</string>
<string name="custom_token_network_selector_title">Сеть</string>
<string name="custom_token_subtitle">Вы можете добавить токен в ручную, если он не поддерживается Tangem</string>
<string name="custom_token_token_symbol_input_placeholder">Например, USDC</string>
<string name="custom_token_token_symbol_input_title">Символ</string>
<string name="custom_token_token_symbol_input_title_old">Символ токена</string>
<string name="custom_token_validation_error_already_added">Этот токен/сеть уже находится в вашем списке</string>
<string name="custom_token_validation_error_not_found">Токены могут быть созданы кем угодно. Остерегайтесь мошеннических токенов, они могут ничего не стоить</string>
<string name="custom_token_validation_error_not_found_description">Остерегайтесь мошеннических токенов, они могут ничего не стоить</string>
<string name="custom_token_validation_error_not_found_title">Токены могут быть созданы кем угодно</string>
<string name="details_chat">Чат</string>
<string name="details_manage_security_access_code">Код доступа</string>
<string name="details_manage_security_access_code_description">Перед сканированием карты вам нужно будет ввести правильный код доступа.</string>
@ -183,6 +188,13 @@
<string name="error_wrong_wallet_tapped">Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком.</string>
<string name="exchange_receive_view_header">Вы получаете</string>
<string name="exchange_send_view_header">Вы отправляете</string>
<string name="exchange_tokens_available_tokens_header">Мои токены</string>
<string name="exchange_tokens_empty_tokens">У вас нет добавленных токенов. Добавьте токены для обмена</string>
<string name="exchange_tokens_unavailable_tokens_header">Недоступен для обмена с %s</string>
<string name="express_choose_providers_subtitle">Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами</string>
<string name="express_choose_providers_title">Выберите провайдера</string>
<string name="express_provider_best_rate">Лучший курс</string>
<string name="express_provider_permission_needed">Требуется разрешение</string>
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
<string name="feedback_preface_scan_failed">Скажите, пожалуйста, какая у вас карта?</string>
@ -239,7 +251,9 @@
<string name="manage_tokens_title">Рыночная капитализация</string>
<string name="manage_tokens_unavailable_description">Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.</string>
<string name="manage_tokens_unavailable_vote">Голосовать</string>
<string name="manage_tokens_wallet_does_not_supported_blockchain">Кошелек не поддерживает выбранную монету</string>
<string name="manage_tokens_wallet_selector_title">Выберите кошелек</string>
<string name="manage_tokens_wallet_support_only_one_network_title">Кошелёк не поддерживает более одной сети</string>
<string name="onboarding_access_code_feature_1_description">Вам необходимо установить единый код доступа для защиты всех ваших карт</string>
<string name="onboarding_access_code_feature_1_title">Защита</string>
<string name="onboarding_access_code_feature_2_description">Позже вы сможете установить индивидуальный код доступа для каждой карты</string>
@ -393,50 +407,50 @@
<string name="scan_card_settings_title">Приготовьте свою карту</string>
<string name="send_amount_label">Сумма</string>
<string name="send_amount_substract">Вычесть из суммы отправки</string>
<string name="send_amount_substract_footer">Сумма к получению %1$s</string>
<string name="send_amount_substract_footer">Сумма к получению %s</string>
<string name="send_date_format">%1$s в %2$s</string>
<string name="send_destination_hint_address">Адрес</string>
<string name="send_destination_tag_field">Код назначения</string>
<string name="send_datetime_formatter">%1$s в %2$s</string>
<string name="send_enter_address_field">Введите адрес</string>
<string name="send_error_address_same_as_wallet">Адрес совпадает с адресом кошелька</string>
<string name="send_extras_error_invalid_destination_tag">Недопустимый Tag. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_error_invalid_memo">Недопустимый Memo. Он не будет добавлен в транзакцию.</string>
<string name="send_extras_hint_destination_tag">Tag</string>
<string name="send_extras_hint_memo">Memo</string>
<string name="send_recent_transactions">Последние</string>
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %1$s. Ошибки могут привести к потере ваших токенов.</string>
<string name="send_recipient_memo_footer">Мемо/Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
<string name="send_fee_include_description">Включая комиссию</string>
<string name="send_fee_label">Комиссия</string>
<string name="send_fee_picker_low">Низкая</string>
<string name="send_fee_picker_normal">Нормальная</string>
<string name="send_fee_picker_priority">Приоритетная</string>
<string name="send_gas_limit">Лимит газа</string>
<string name="send_gas_price">Цена газа</string>
<string name="send_gas_price_footer">Цена газа влияет на скорость транзакции. При сильно низкой, транзакция может быть не обработана.</string>
<string name="send_gas_limit">Лимит газа</string>
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_max_amount">Всё</string>
<string name="send_max_amount_label">Максимальная сумма</string>
<string name="send_max_fee">Комиссия не превысит </string>
<string name="send_max_fee_footer">Максимальная cумма комиссии</string>
<string name="send_max_fee">Комиссия не превысит</string>
<string name="send_network_fee_title">Сетевая комиссия</string>
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_network_fee_warning_content">Сумма отправки будет уменьшена для покрытия выбранного уровня комиссии</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_network_fee_warning_title">Покрытие сетевой комиссии</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса</string>
<string name="send_notification_exceed_balance_title">Недостаточно средств</string>
<string name="send_notification_exceed_balance_text">Недостаточно средств для перевода, так как сумма комиссии и сумма перевода в совокупности больше имеющегося баланса.</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению.</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма резерва не может быть менее %1$s.</string>
<string name="send_notification_top_up_text">Пожалуйста, пополните свой баланс, чтобы продолжить.</string>
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
<string name="send_notification_exceed_fee_text">Размер комиссии превышает баланс сети. Для продолжения необходимо пополнить баланс сети.</string>
<string name="send_notification_exceed_fee_title">Комиссия превышает баланс</string>
<string name="send_notification_high_fee_text">Комиссия при переводе всего баланса выше. Для того, чтобы снизить комиссию Вы можете оставить 0.01.</string>
<string name="send_notification_high_fee_title">Увеличение комиссии</string>
<string name="send_notification_invalid_amount_text">Включенная комиссия превышает сумму перевода, что приводит к отрицательному значению</string>
<string name="send_notification_invalid_amount_title">Недопустимая сумма</string>
<string name="send_notification_invalid_minimum_amount_text">Минимальная сумма отправки - %1$s. Пожалуйста, убедитесь, что остаток после отправки также не будет меньше %1$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Пожалуйста, пополните свой баланс, чтобы продолжить</string>
<string name="send_notification_invalid_reserve_amount_title">Сумма резерва не может быть менее %1$s</string>
<string name="send_notification_transaction_delay_text">Обратите внимание, что при определенных параметрах комиссии возможны задержки по вашей транзакции</string>
<string name="send_notification_transaction_delay_title">Возможны задержки по транзакции</string>
<string name="send_optional_field">Необязательное</string>
<string name="send_qrcode_scan_amount_alert_title">QR код содержит информацию о сумме отправки равной %s</string>
<string name="send_recent_transactions">Последние</string>
<string name="send_recipient">Получатель</string>
<string name="send_recipient_address_footer">Убедитесь, что вы отправляете средства на адрес кошелька %s. Ошибки могут привести к потере ваших токенов</string>
<string name="send_recipient_memo_footer">Мемо/ Код назначения - это код, разделяющий транзакции к общему получателю в сети криптовалют. Внимание: отсутствие мемо может привести к потере средств.</string>
<string name="send_recipient_wallets_title">Мои кошельки</string>
<string name="send_title_currency_format">Отправка %s</string>
<string name="send_total_label">Всего</string>
@ -651,7 +665,7 @@
<string name="warning_solana_rent_fee_message">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
<string name="warning_some_networks_unreachable_message">Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже.</string>
<string name="warning_some_networks_unreachable_title">Некоторые сети недоступны</string>
<string name="warning_testnet_card_message">Это Testnet карта. Она не может обрабатывать транзакции и используется только в целях тестирования и разработки.</string>
<string name="warning_testnet_card_message">Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки.</string>
<string name="warning_testnet_card_title">Только для целей тестирования</string>
<string name="welcome_interrupted_backup_alert_discard">Отказаться</string>
<string name="welcome_interrupted_backup_alert_message">Вы не закончили резервное копирование. Хотите продолжить?</string>

View file

@ -11,8 +11,9 @@
<string name="alert_failed_to_send_email_title">Failed to send the email</string>
<string name="alert_failed_to_send_transaction_message">Reason: %s</string>
<string name="alert_failed_to_send_transaction_title">Can\'t send a transaction</string>
<string name="alert_manage_tokens_unsupported_blockchain_by_card_message">The selected does not support the %1$s network</string>
<string name="alert_manage_tokens_unsupported_curve_message">To activate the %1$s blockchain\'s cryptographic encryption, you\'ll need to reset the wallet to factory settings. Please withdraw your funds before doing so to ensure that you don\'t lose them, and then complete the reset process. Access to the current wallet will not be possible after the reset.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in Solana network are not supported by this card due to firmware limitation.</string>
<string name="alert_manage_tokens_unsupported_message">Tokens in %1$s network are not supported by this card due to firmware limitation.</string>
<string name="alert_troubleshooting_scan_card_title">Are you having difficulty scanning your card?</string>
<string name="alert_unsupported_card">This card is not designed to work with this app</string>
<string name="app_settings_enable_biometrics_description">Go to settings to enable biometric authentication in the Tangem App</string>
@ -55,10 +56,7 @@
<string name="card_settings_title">Card Settings</string>
<string name="chat_bot_name">Tangem Bot</string>
<string name="chat_button_title">Support</string>
<string name="chat_user_action_rate_user">Rate agent</string>
<string name="chat_user_action_send_log">Send logs</string>
<string name="chat_user_actions_title">Please select an action</string>
<string name="chat_user_rate_agent_title">Please, rate the work of the agent</string>
<string name="common_accept">Accept</string>
<string name="common_access_denied">Access denied</string>
<string name="common_add">Add</string>
@ -85,15 +83,15 @@
<string name="common_enabled">Enabled</string>
<string name="common_error">Error</string>
<string name="common_exchange">Exchange</string>
<string name="common_explore_transaction_history">Explore transaction history</string>
<string name="common_explore">Explore</string>
<string name="common_explore_transaction_history">Explore transaction history</string>
<string name="common_explorer">Explorer</string>
<string name="common_fee_selector_title">Speed and fee</string>
<string name="common_fee_selector_footer">Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority</string>
<string name="common_fee_selector_option_slow">Slow</string>
<string name="common_fee_selector_option_market">Market</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_footer">Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority.</string>
<string name="common_fee_selector_option_custom">Custom</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_option_market">Market</string>
<string name="common_fee_selector_option_slow">Slow</string>
<string name="common_fee_selector_title">Speed and fee</string>
<string name="common_generate_addresses">Generate addresses</string>
<string name="common_import">Import</string>
<string name="common_learn_and_earn">Learn &amp; Earn</string>
@ -162,6 +160,8 @@
<string name="custom_token_token_symbol_input_title_old">Token symbol</string>
<string name="custom_token_validation_error_already_added">This token/network has already been added to your list</string>
<string name="custom_token_validation_error_not_found">Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing.</string>
<string name="custom_token_validation_error_not_found_description">Be aware of adding scam tokens, they can cost nothing</string>
<string name="custom_token_validation_error_not_found_title">Note that tokens can be created by anyone</string>
<string name="details_chat">Chat</string>
<string name="details_manage_security_access_code">Access code</string>
<string name="details_manage_security_access_code_description">You will have to submit the correct access code before scanning the card</string>
@ -187,6 +187,13 @@
<string name="error_wrong_wallet_tapped">You have used a card from another wallet. Tap the card associated with this wallet</string>
<string name="exchange_receive_view_header">You Receive</string>
<string name="exchange_send_view_header">You Send</string>
<string name="exchange_tokens_available_tokens_header">My tokens</string>
<string name="exchange_tokens_empty_tokens">You don\'t have any added tokens yet. Add tokens via Market to swap</string>
<string name="exchange_tokens_unavailable_tokens_header">Unavailable for swap from %s</string>
<string name="express_choose_providers_subtitle">Providers facilitate transactions, ensuring smooth and efficient token exchanges</string>
<string name="express_choose_providers_title">Choose Provider</string>
<string name="express_provider_best_rate">Best Rate</string>
<string name="express_provider_permission_needed">Permission Needed</string>
<string name="feedback_data_collection_message">The following information is optional. You can erase it if you don\'t want to share it.</string>
<string name="feedback_preface_rate_negative">Tell us what functions you are missing, and we will try to help you.</string>
<string name="feedback_preface_scan_failed">Please tell us what card do you have</string>
@ -224,6 +231,8 @@
<string name="main_tokens">Tokens</string>
<string name="manage_tokens_add">Add</string>
<string name="manage_tokens_edit">Edit</string>
<string name="manage_tokens_list_header_subtitle">Couldnt find this token, you can add it manually</string>
<string name="manage_tokens_list_header_title">Coin market cap</string>
<string name="manage_tokens_network_selector_native_subtitle">Blockchain the cryptocurrency was initially created</string>
<string name="manage_tokens_network_selector_native_title">Native network</string>
<string name="manage_tokens_network_selector_non_native_info">Using non-native networks for tokens enables cross-blockchain interoperability, allowing assets to be utilized in diverse decentralized applications and smart contracts across platforms. However, this often involves a custodian or smart contract to hold the original asset securely, introducing centralization and counterparty risk.</string>
@ -242,7 +251,9 @@
<string name="manage_tokens_title">Coin market cap</string>
<string name="manage_tokens_unavailable_description">The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it.</string>
<string name="manage_tokens_unavailable_vote">Upvote</string>
<string name="manage_tokens_wallet_does_not_supported_blockchain">Wallet Incompatible with selected Coin</string>
<string name="manage_tokens_wallet_selector_title">Choose wallet</string>
<string name="manage_tokens_wallet_support_only_one_network_title">The wallet doesn\'t support more than one network</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your wallets</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">You can set up an individual access code on each card later</string>
@ -392,10 +403,10 @@
<string name="scan_card_settings_title">Get your card ready!</string>
<string name="send_amount_label">Amount</string>
<string name="send_amount_substract">Subtract from send amount</string>
<string name="send_amount_substract_footer">The recipient will receive %1$s</string>
<string name="send_amount_substract_footer">The recipient will receive %s</string>
<string name="send_date_format">%1$s at %2$s</string>
<string name="send_destination_hint_address">Address</string>
<string name="send_destination_tag_field">Destination Tag</string>
<string name="send_datetime_formatter">%1$s at %2$s</string>
<string name="send_enter_address_field">Enter address</string>
<string name="send_error_address_same_as_wallet">Address is the same as wallet address</string>
<string name="send_extras_error_invalid_destination_tag">Invalid Tag. It won\'t be added to the transaction.</string>
@ -408,47 +419,47 @@
<string name="send_fee_picker_low">Low</string>
<string name="send_fee_picker_normal">Normal</string>
<string name="send_fee_picker_priority">Priority</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_fee_unreachable_error_text">Check your network connection</string>
<string name="send_fee_unreachable_error_title">Network fee info unreachable</string>
<string name="send_from_wallet_android">From</string>
<string name="send_gas_limit">Gas limit</string>
<string name="send_gas_limit_footer">Gas Limit is auto-calculated; raise it during network congestion</string>
<string name="send_gas_price">Gas price</string>
<string name="send_gas_price_footer">Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed.</string>
<string name="send_max_amount_label">Maximum amount</string>
<string name="send_gas_price_footer">Gas Price affects transaction speed. If it\'s too low, the transaction might not be processed</string>
<string name="send_max_amount">Max</string>
<string name="send_max_fee_footer">Maximum fee amount</string>
<string name="send_max_amount_label">Maximum amount</string>
<string name="send_max_fee">Max fee</string>
<string name="send_max_fee_footer">Maximum fee amount</string>
<string name="send_memo_destination_tag_error">Numbers only for Destination Tag</string>
<string name="send_network_fee_title">Network fee</string>
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_network_fee_warning_content">Sending amount will be reduced to cover the selected fee level</string>
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
<string name="send_network_fee_warning_title">Network fee coverage</string>
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance</string>
<string name="send_notification_exceed_balance_title">Total exceeds balance</string>
<string name="send_notification_exceed_balance_text">Insufficient funds for the transfer, as the total of the fee and transfer amount exceeds the existing balance.</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value.</string>
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s.</string>
<string name="send_notification_invalid_reserve_amount_title">The balance amount must be at least %1$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Please top up your balance to continue.</string>
<string name="send_notification_high_fee_title">Fee is increased</string>
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
<string name="send_notification_exceed_fee_title">Fee exceeds balance</string>
<string name="send_notification_exceed_fee_text">The commission fee exceeds the network balance. To continue, it is necessary to replenish the network balance.</string>
<string name="send_notification_exceed_fee_title">Fee exceeds balance</string>
<string name="send_notification_high_fee_text">The fee for transferring the entire balance is higher. To reduce the commission, you can leave 0.01.</string>
<string name="send_notification_high_fee_title">Fee is increased</string>
<string name="send_notification_invalid_amount_text">The included commission exceeds the transfer amount, leading to a negative value</string>
<string name="send_notification_invalid_amount_title">Invalid amount</string>
<string name="send_notification_invalid_minimum_amount_text">The minimum sending amount is %1$s. Please ensure that the remaining balance after sending will not be less than %1$s.</string>
<string name="send_notification_invalid_reserve_amount_text">Please top up your balance to continue</string>
<string name="send_notification_invalid_reserve_amount_title">The balance amount must be at least %1$s</string>
<string name="send_notification_transaction_delay_text">Kindly be aware that your transaction may experience delays under specific fee settings</string>
<string name="send_notification_transaction_delay_title">Transaction delays are possible</string>
<string name="send_optional_field">Optional</string>
<string name="send_qrcode_scan_info">Please align your QR code with the square to scan it. Ensure you scan %s network address.</string>
<string name="send_qrcode_scan_address_success">Recipients address scanned</string>
<string name="send_qrcode_scan_memo_success">Sending amount was changed</string>
<string name="send_qrcode_scan_amount_alert_title">QR code contains information about the sending amount equal to %s</string>
<string name="send_qrcode_scan_amount_alert_text">Change the entered amount?</string>
<string name="send_qrcode_alert_change">Change</string>
<string name="send_qrcode_alert_decline">Decline</string>
<string name="send_qrcode_scan_address_success">Recipients address scanned</string>
<string name="send_qrcode_scan_amount_alert_text">Change the entered amount?</string>
<string name="send_qrcode_scan_amount_alert_title">QR code contains information about the sending amount equal to %s</string>
<string name="send_qrcode_scan_info">Please align your QR code with the square to scan it. Ensure you scan %s network address.</string>
<string name="send_recent_transactions">Recent</string>
<string name="send_recipient">Recipient</string>
<string name="send_recipient_address_error">Not a valid address</string>
<string name="send_recipient_address_footer">Ensure that you are sending funds to an %s wallet address. Errors may result in the loss of your tokens</string>
<string name="send_recipient_memo_footer">A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds</string>
<string name="send_recipient_wallets_title">My wallets</string>
<string name="send_recipient_address_footer">Ensure that you are sending funds to an %1$s wallet address. Errors may result in the loss of your tokens.</string>
<string name="send_recipient_memo_footer">A Memo/Destination Tag is a unique ID for differentiating transactions sent to the same recipient on the same network. Caution: Omitting a memo may lead to misplaced funds.</string>
<string name="send_title_currency_format">Sending %s</string>
<string name="send_total_label">Total</string>
<string name="send_total_subtitle_asset_format">%1$s and %2$s will be sent</string>
@ -456,6 +467,7 @@
<string name="send_total_subtitle_format">%s will be sent</string>
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
<string name="send_validation_invalid_address">Invalid address</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="shop_buy_now">Buy now</string>
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
<string name="shop_one_wallet">Tangem Wallet</string>

View file

@ -14,6 +14,7 @@ dependencies {
/** Project - Core */
implementation(projects.core.res)
implementation(projects.core.utils)
/** AndroidX libraries */
implementation(deps.androidx.fragment.ktx)

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.details.ui.common
package com.tangem.core.ui.components
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.*

View file

@ -0,0 +1,103 @@
package com.tangem.core.ui.components.appbar
import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
@Composable
fun AppBarWithBackButtonAndIcon(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
text: String? = null,
@DrawableRes backIconRes: Int? = null,
@DrawableRes iconRes: Int? = null,
onIconClick: (() -> Unit)? = null,
backgroundColor: Color = TangemTheme.colors.background.secondary,
) {
Row(
modifier = modifier
.background(color = backgroundColor)
.fillMaxWidth()
.padding(all = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(backIconRes ?: R.drawable.ic_back_24),
contentDescription = null,
modifier = Modifier
.size(size = TangemTheme.dimens.size24)
.clickable { onBackClick() },
tint = TangemTheme.colors.icon.primary1,
)
AnimatedContent(
targetState = text,
modifier = Modifier.weight(1f),
transitionSpec = { fadeIn().togetherWith(fadeOut()) },
label = "Toolbar title change",
) {
if (!it.isNullOrBlank()) {
Text(
text = it,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
style = TangemTheme.typography.subtitle1,
)
}
}
AnimatedContent(
targetState = iconRes,
transitionSpec = { (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut()) },
label = "Toolbar icon change",
) {
if (onIconClick != null && it != null) {
Icon(
painter = painterResource(it),
contentDescription = null,
modifier = Modifier
.size(size = TangemTheme.dimens.size24)
.clickable { onIconClick() },
tint = TangemTheme.colors.icon.primary1,
)
}
}
}
}
@Preview(widthDp = 360, heightDp = 56, showBackground = true)
@Composable
private fun PreviewAppBarWithBackButtonAndIconInLightTheme() {
TangemTheme(isDark = false) {
AppBarWithBackButtonAndIcon(
text = "Title",
iconRes = R.drawable.ic_qrcode_scan_24,
onBackClick = {},
onIconClick = {},
)
}
}
@Preview(widthDp = 360, heightDp = 56, showBackground = true)
@Composable
private fun PreviewAppBarWithBackButtonAndIconInDarkTheme() {
TangemTheme(isDark = true) {
AppBarWithBackButtonAndIcon(
text = "Title",
iconRes = R.drawable.ic_qrcode_scan_24,
onBackClick = {},
onIconClick = {},
)
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Shape
@ -108,7 +109,7 @@ private inline fun RowScope.ButtonContentContainer(
if (showProgress) {
progressIndicator()
} else {
Column {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row(horizontalArrangement = Arrangement.Center) {
if (buttonIcon is TangemButtonIconPosition.Start) {
icon(buttonIcon.iconResId)

View file

@ -9,6 +9,8 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
@ -19,16 +21,19 @@ import kotlinx.coroutines.launch
@Composable
internal inline fun DefaultCurrencyIcon(
iconData: Any,
size: Dp,
alpha: Float,
colorFilter: ColorFilter?,
crossinline errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
val isDarkTheme = isSystemInDarkTheme()
val coroutineScope = rememberCoroutineScope()
val pixelsSize = with(LocalDensity.current) { size.roundToPx() }
SubcomposeAsyncImage(
modifier = modifier
.background(
@ -38,17 +43,21 @@ internal inline fun DefaultCurrencyIcon(
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconData)
.size(size = pixelsSize)
.memoryCacheKey(key = iconData.toString() + pixelsSize)
.crossfade(enable = true)
.allowHardware(false)
.allowHardware(enable = false)
.listener(
onSuccess = { _, result ->
if (isDarkTheme) {
if (!isBackgroundColorDefined && isDarkTheme) {
coroutineScope.launch {
val color = ImageBackgroundContrastChecker(
drawable = result.drawable,
backgroundColor = itemBackgroundColor,
).getContrastColorIfNeeded(isDarkTheme)
size = pixelsSize,
).getContrastColor(isDarkTheme = true)
iconBackgroundColor = color
isBackgroundColorDefined = true
}
}
},

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
@ -18,21 +19,23 @@ import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
@Composable
fun FiatIcon(
url: String?,
size: Dp,
modifier: Modifier = Modifier,
@DrawableRes fallbackResId: Int = R.drawable.ic_shape_circle,
) {
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
DefaultCurrencyIcon(
modifier = modifier,
iconData = iconData,
size = size,
alpha = 1f,
colorFilter = null,
errorIcon = {
Image(
painter = painterResource(id = fallbackResId),
contentDescription = null,
)
},
alpha = 1f,
colorFilter = null,
modifier = modifier,
)
}

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun ContentIcon(
@ -67,8 +68,10 @@ private fun CoinIcon(
val iconData: Any = if (url.isNullOrBlank()) fallbackResId else url
DefaultCurrencyIcon(
modifier = modifier,
iconData = iconData,
size = TangemTheme.dimens.size36,
alpha = alpha,
colorFilter = colorFilter,
errorIcon = {
Image(
painter = painterResource(id = fallbackResId),
@ -77,8 +80,7 @@ private fun CoinIcon(
contentDescription = null,
)
},
alpha = alpha,
colorFilter = colorFilter,
modifier = modifier,
)
}
@ -94,11 +96,12 @@ private fun TokenIcon(
errorIcon()
} else {
DefaultCurrencyIcon(
modifier = modifier,
iconData = url,
errorIcon = errorIcon,
size = TangemTheme.dimens.size36,
alpha = alpha,
colorFilter = colorFilter,
errorIcon = errorIcon,
modifier = modifier,
)
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.components.fields
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
class AmountVisualTransformation(
private val symbol: String,
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
return TransformedText(
buildAnnotatedString {
append(text)
if (text.isNotBlank()) {
append(" ")
append(symbol)
}
},
object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
return text.length
}
override fun transformedToOriginal(offset: Int): Int {
return text.length
}
},
)
}
}

View file

@ -0,0 +1,60 @@
package com.tangem.core.ui.components.fields
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.input.VisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Simple text field with placeholder
*/
@Composable
fun SimpleTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: TextReference? = null,
singleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
color: Color = TangemTheme.colors.text.primary1,
readOnly: Boolean = false,
) {
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TangemTheme.typography.body2.copy(color = color),
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
decorationBox = { textValue ->
Box {
if (value.isBlank() && placeholder != null) {
Text(
text = placeholder.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.disabled,
modifier = Modifier,
)
}
textValue()
}
},
modifier = modifier
.focusRequester(focusRequester),
)
}

View file

@ -0,0 +1,152 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4)
*
* @param title title reference
* @param text primary text reference
* @param modifier modifier
* @param titleColor title color
* @param textColor text color
* @param iconRes action icon
* @param iconTint action icon tint
* @param onIconClick click on action icon
* @param showDivider show divider
* @see [InputRowEnter] for editable version
*/
@Composable
fun InputRowDefault(
title: TextReference,
text: TextReference,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Column(
modifier = Modifier
.weight(1f),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
iconRes?.let {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onIconClick?.invoke() },
)
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowDefaultPreview_Light(
@PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData,
) {
TangemTheme {
InputRowDefault(
title = TextReference.Str(data.title),
text = TextReference.Str(data.text),
iconRes = data.iconRes,
showDivider = data.showDivider,
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
@Preview
@Composable
private fun InputRowDefaultPreview_Dark(
@PreviewParameter(InputRowDefaultPreviewDataProvider::class) data: InputRowDefaultPreviewData,
) {
TangemTheme(isDark = true) {
InputRowDefault(
title = TextReference.Str(data.title),
text = TextReference.Str(data.text),
iconRes = data.iconRes,
showDivider = data.showDivider,
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
private data class InputRowDefaultPreviewData(
val title: String,
val text: String,
val iconRes: Int?,
val showDivider: Boolean,
)
private class InputRowDefaultPreviewDataProvider : PreviewParameterProvider<InputRowDefaultPreviewData> {
override val values: Sequence<InputRowDefaultPreviewData>
get() = sequenceOf(
InputRowDefaultPreviewData(
title = "title",
text = "text",
iconRes = null,
showDivider = true,
),
InputRowDefaultPreviewData(
title = "title",
text = "text",
iconRes = R.drawable.ic_chevron_right_24,
showDivider = false,
),
)
}
//endregion

View file

@ -0,0 +1,167 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [InputRowEnter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
*
* @param title title reference
* @param text primary text reference
* @param onValueChange text change callback
* @param modifier modifier
* @param titleColor title color
* @param textColor text color
* @param isSingleLine text
* @param visualTransformation applied transformation to text
* @param keyboardOptions keyboard options for field
* @param iconRes action icon
* @param iconTint action icon tint
* @param onIconClick click on action icon
* @param showDivider show divider
* @see [InputRowDefault] for read only version
*/
@Composable
fun InputRowEnter(
title: TextReference,
text: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
isSingleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
SimpleTextField(
value = text,
onValueChange = onValueChange,
color = textColor,
singleLine = isSingleLine,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
)
}
iconRes?.let {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onIconClick?.invoke() },
)
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowEnterPreview_Light(
@PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData,
) {
TangemTheme {
InputRowEnter(
title = TextReference.Str(data.title),
text = data.text,
iconRes = data.iconRes,
showDivider = data.showDivider,
onValueChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
@Preview
@Composable
private fun InputRowEnterPreview_Dark(
@PreviewParameter(InputRowEnterPreviewDataProvider::class) data: InputRowEnterPreviewData,
) {
TangemTheme(isDark = true) {
InputRowEnter(
title = TextReference.Str(data.title),
text = data.text,
iconRes = data.iconRes,
showDivider = data.showDivider,
onValueChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
private data class InputRowEnterPreviewData(
val title: String,
val text: String,
val iconRes: Int?,
val showDivider: Boolean,
)
private class InputRowEnterPreviewDataProvider :
PreviewParameterProvider<InputRowEnterPreviewData> {
override val values: Sequence<InputRowEnterPreviewData>
get() = sequenceOf(
InputRowEnterPreviewData(
title = "title",
text = "text",
iconRes = null,
showDivider = true,
),
InputRowEnterPreviewData(
title = "title",
text = "text",
iconRes = R.drawable.ic_chevron_right_24,
showDivider = false,
),
)
}
//endregion

View file

@ -0,0 +1,153 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Input Row Enter with Info variation.
* [Input Row Enter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
* [Input Row Enter Info](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7854-33577&mode=design&t=6o23sqF8fDQdn4C5-4)
*
* @param title title reference
* @param text primary text reference
* @param onValueChange text change callback
* @param modifier modifier
* @param titleColor title color
* @param textColor text color
* @param isSingleLine text
* @param visualTransformation applied transformation to text
* @param keyboardOptions keyboard options for field
* @param showDivider show divider
*/
@Composable
fun InputRowEnterInfo(
title: TextReference,
text: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
info: TextReference? = null,
titleColor: Color = TangemTheme.colors.text.secondary,
textColor: Color = TangemTheme.colors.text.primary1,
infoColor: Color = TangemTheme.colors.text.tertiary,
isSingleLine: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Row {
SimpleTextField(
value = text,
onValueChange = onValueChange,
singleLine = isSingleLine,
color = textColor,
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8)
.weight(1f),
)
info?.let {
Text(
text = it.resolveReference(),
style = TangemTheme.typography.body2,
color = infoColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.align(Alignment.Bottom),
)
}
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowEnterInfoPreview_Light(
@PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData,
) {
TangemTheme {
InputRowEnterInfo(
title = data.title,
text = data.text,
info = data.info,
showDivider = data.showDivider,
onValueChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
@Preview
@Composable
private fun InputRowEnterInfoPreview_Dark(
@PreviewParameter(InputRowEnterInfoPreviewDataProvider::class) data: InputRowEnterInfoPreviewData,
) {
TangemTheme(isDark = true) {
InputRowEnterInfo(
title = data.title,
text = data.text,
info = data.info,
showDivider = data.showDivider,
onValueChange = {},
modifier = Modifier.background(TangemTheme.colors.background.action),
)
}
}
private data class InputRowEnterInfoPreviewData(
val title: TextReference,
val text: String,
val showDivider: Boolean,
val info: TextReference?,
)
private class InputRowEnterInfoPreviewDataProvider :
PreviewParameterProvider<InputRowEnterInfoPreviewData> {
override val values: Sequence<InputRowEnterInfoPreviewData>
get() = sequenceOf(
InputRowEnterInfoPreviewData(
title = TextReference.Str("title"),
text = "text",
showDivider = true,
info = TextReference.Str("info"),
),
InputRowEnterInfoPreviewData(
title = TextReference.Str("title"),
text = "text",
showDivider = false,
info = null,
),
)
}
//endregion

View file

@ -0,0 +1,196 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [Input Row Image](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-813&mode=design&t=IQ5lBJEkFGU4WSvi-4)
*
* @param title title reference
* @param subtitle subtitle reference
* @param caption caption reference
* @param tokenIconState token icon state [TokenIconState]
* @param modifier modifier
* @param titleColor title color
* @param subtitleColor subtitle color
* @param captionColor caption color
* @param iconRes action icon
* @param iconTint action icon tint
* @param onIconClick click on action icon
* @param showNetworkIcon show token network icon
* @param showDivider show divider
*/
@Composable
fun InputRowImage(
title: TextReference,
subtitle: TextReference,
caption: TextReference,
tokenIconState: TokenIconState,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.secondary,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
iconRes: Int? = null,
iconTint: Color = TangemTheme.colors.icon.informative,
onIconClick: (() -> Unit)? = null,
showNetworkIcon: Boolean = false,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Row(
modifier = Modifier
.padding(
top = TangemTheme.dimens.spacing6,
),
) {
TokenIcon(
state = tokenIconState,
shouldDisplayNetwork = showNetworkIcon,
modifier = Modifier
.size(TangemTheme.dimens.size36),
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = TangemTheme.dimens.spacing12),
) {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
Text(
text = caption.resolveReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
iconRes?.let {
Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconTint,
modifier = Modifier
.align(CenterVertically)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onIconClick?.invoke() },
)
}
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowInputEnterInfoPreview_Light(
@PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData,
) {
TangemTheme {
InputRowImage(
title = data.title,
modifier = Modifier.background(TangemTheme.colors.background.action),
subtitle = data.subtitle,
caption = data.caption,
tokenIconState = data.iconState,
iconRes = data.actionIconRes,
onIconClick = {},
showNetworkIcon = false,
showDivider = data.showDivider,
)
}
}
@Preview
@Composable
private fun InputRowImagePreview_Dark(
@PreviewParameter(InputRowImagePreviewDataProvider::class) data: InputRowImagePreviewData,
) {
TangemTheme(isDark = true) {
InputRowImage(
title = data.title,
modifier = Modifier.background(TangemTheme.colors.background.action),
subtitle = data.subtitle,
caption = data.caption,
tokenIconState = data.iconState,
iconRes = data.actionIconRes,
onIconClick = {},
showNetworkIcon = false,
showDivider = data.showDivider,
)
}
}
private data class InputRowImagePreviewData(
val title: TextReference,
val subtitle: TextReference,
val caption: TextReference,
val iconState: TokenIconState,
val showDivider: Boolean,
val actionIconRes: Int?,
val showNetworkIcon: Boolean = false,
)
private class InputRowImagePreviewDataProvider :
PreviewParameterProvider<InputRowImagePreviewData> {
override val values: Sequence<InputRowImagePreviewData>
get() = sequenceOf(
InputRowImagePreviewData(
title = TextReference.Str("title"),
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
iconState = TokenIconState.Locked,
actionIconRes = null,
showDivider = false,
showNetworkIcon = false,
),
InputRowImagePreviewData(
title = TextReference.Str("title"),
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
iconState = TokenIconState.Locked,
actionIconRes = R.drawable.ic_chevron_right_24,
showDivider = true,
showNetworkIcon = true,
),
)
}
//endregion

View file

@ -0,0 +1,168 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.fields.SimpleTextField
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.inputrow.inner.PasteButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode=design&t=IQ5lBJEkFGU4WSvi-4)
*
* @param title title reference
* @param value recipient address
* @param placeholder placeholder
* @param onValueChange callback for value change
* @param onPasteClick callback for paste
* @param modifier composable modifier
* @param singleLine is single line text
* @param error error text
* @param isError is error flag
* @param showDivider show divider
*
* @see InputRowRecipientDefault for readonly version
*/
@Composable
fun InputRowRecipient(
title: TextReference,
value: String,
placeholder: TextReference,
onValueChange: (String) -> Unit,
onPasteClick: (String) -> Unit,
modifier: Modifier = Modifier,
singleLine: Boolean = false,
error: TextReference? = null,
isError: Boolean = false,
showDivider: Boolean = false,
) {
val (titleText, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
title to TangemTheme.colors.text.secondary
}
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = titleText.resolveReference(),
style = TangemTheme.typography.caption2,
color = color,
)
Row(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
) {
IdentIcon(
address = value,
modifier = Modifier
.align(CenterVertically)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.size(TangemTheme.dimens.size36)
.background(TangemTheme.colors.background.tertiary),
)
SimpleTextField(
value = value,
placeholder = placeholder,
onValueChange = onValueChange,
singleLine = singleLine,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.weight(1f)
.align(CenterVertically),
)
PasteButton(
isPasteButtonVisible = value.isBlank(),
onClick = onPasteClick,
modifier = Modifier
.align(CenterVertically)
.padding(start = TangemTheme.dimens.spacing8),
)
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowRecipientPreview_Light(
@PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData,
) {
TangemTheme {
InputRowRecipient(
value = value.value,
title = TextReference.Res(R.string.send_recipient),
placeholder = TextReference.Res(R.string.send_optional_field),
error = TextReference.Str("Error"),
isError = value.isError,
showDivider = true,
onValueChange = {},
onPasteClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
@Preview
@Composable
private fun InputRowRecipientPreview_Dark(
@PreviewParameter(InputRowRecipientPreviewDataProvider::class) value: InputRowRecipientPreviewData,
) {
TangemTheme(isDark = true) {
InputRowRecipient(
value = value.value,
title = TextReference.Res(R.string.send_recipient),
placeholder = TextReference.Res(R.string.send_optional_field),
error = TextReference.Str("Error"),
isError = value.isError,
showDivider = true,
onValueChange = {},
onPasteClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
private data class InputRowRecipientPreviewData(
val value: String,
val isError: Boolean,
)
private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider<InputRowRecipientPreviewData> {
override val values: Sequence<InputRowRecipientPreviewData>
get() = sequenceOf(
InputRowRecipientPreviewData(
value = "",
isError = false,
),
InputRowRecipientPreviewData(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
isError = false,
),
InputRowRecipientPreviewData(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
isError = true,
),
)
}
//endregion

View file

@ -0,0 +1,106 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.icons.identicon.IdentIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
/**
* Read only version of [InputRowRecipient].
* [Input Row Recipient](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-826&mode
* =design&t=IQ5lBJEkFGU4WSvi-4)
*
* @param title title reference
* @param value recipient address
* @param modifier composable modifier
* @param titleColor title color
* @param showDivider show divider
* @see InputRowRecipient for editable version
*/
@Composable
fun InputRowRecipientDefault(
title: TextReference,
value: String,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.secondary,
showDivider: Boolean = false,
) {
DividerContainer(
modifier = modifier,
showDivider = showDivider,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
color = titleColor,
)
Row(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
) {
IdentIcon(
address = value,
modifier = Modifier
.align(Alignment.CenterVertically)
.clip(RoundedCornerShape(TangemTheme.dimens.radius18))
.size(TangemTheme.dimens.size36)
.background(TangemTheme.colors.background.tertiary),
)
Text(
text = value,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12)
.weight(1f)
.align(Alignment.CenterVertically),
)
}
}
}
}
//region preview
@Preview
@Composable
private fun InputRowRecipientPreview_Light() {
TangemTheme {
InputRowRecipientDefault(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
title = TextReference.Res(R.string.send_recipient),
showDivider = true,
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
@Preview
@Composable
private fun InputRowRecipientPreview_Dark() {
TangemTheme(isDark = true) {
InputRowRecipientDefault(
value = "0x391316d97a07027a0702c8A002c8A0C25d8470",
title = TextReference.Res(R.string.send_recipient),
showDivider = false,
modifier = Modifier.background(TangemTheme.colors.background.primary),
)
}
}
//endregion

View file

@ -0,0 +1,31 @@
package com.tangem.core.ui.components.inputrow.inner
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.TangemTheme
@Composable
fun DividerContainer(
modifier: Modifier = Modifier,
showDivider: Boolean = false,
paddingValues: PaddingValues = PaddingValues(start = TangemTheme.dimens.spacing12),
content: @Composable () -> Unit,
) {
Box(modifier = modifier) {
content()
if (showDivider) {
Divider(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(paddingValues),
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
)
}
}
}

View file

@ -0,0 +1,80 @@
package com.tangem.core.ui.components.inputrow.inner
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
/**
* Paste button with cross icon. Retrieves text from clipboard.
* [Paste button](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=7853-33535&mode=design&t=6o23sqF8fDQdn4C5-4)
*
* @param isPasteButtonVisible is paste button visible
* @param onClick action callback
* @param modifier composable modifier
*/
@Composable
fun PasteButton(isPasteButtonVisible: Boolean, onClick: (String) -> Unit, modifier: Modifier = Modifier) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
if (isPasteButtonVisible) {
Box(modifier = modifier) {
Text(
text = "Paste",
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary2,
modifier = Modifier
.background(
color = TangemTheme.colors.button.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(
horizontal = TangemTheme.dimens.spacing10,
vertical = TangemTheme.dimens.spacing2,
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius8),
onClick = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onClick(
clipboardManager
.getText()
?.toString()
.orEmpty(),
)
},
),
)
}
} else {
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = stringResource(R.string.common_close),
modifier = modifier
.size(TangemTheme.dimens.size20)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(radius = TangemTheme.dimens.radius10),
onClick = { onClick("") },
),
)
}
}

View file

@ -55,6 +55,7 @@ data class TangemDimens internal constructor(
val size20: Dp = 20.dp,
val size24: Dp = 24.dp,
val size28: Dp = 28.dp,
val size30: Dp = 30.dp,
val size32: Dp = 32.dp,
val size34: Dp = 34.dp,
val size36: Dp = 36.dp,

View file

@ -9,12 +9,14 @@ import java.util.Locale
@Suppress("MagicNumber")
object DateTimeFormatters {
private const val DDMMYYYY = "dd.MM.yyyy"
/**
* Two SS means, SHORT style for date and time.
* If pattern contains "a", it means time is in 12 hour format.
* [Documentation](https://www.joda.org/joda-time/apidocs/org/joda/time/format/DateTimeFormat.html)
*/
val timeFormatter by lazy {
val timeFormatter: DateTimeFormatter by lazy {
val is12HourFormat = DateTimeFormat.patternForStyle("SS", Locale.getDefault()).contains("a")
if (is12HourFormat) {
DateTimeFormatterBuilder()
@ -35,7 +37,7 @@ object DateTimeFormatters {
}
}
val dateFormatter by lazy {
val dateFormatter: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendDayOfMonth(1)
.appendLiteral(' ')
@ -46,6 +48,13 @@ object DateTimeFormatters {
.withLocale(Locale.getDefault())
}
val dateDDMMYYYY: DateTimeFormatter by lazy {
DateTimeFormatterBuilder()
.appendPattern(DDMMYYYY)
.toFormatter()
.withLocale(Locale.getDefault())
}
fun formatTime(formatter: DateTimeFormatter = timeFormatter, time: DateTime): String {
return formatter.print(time)
}

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.utils
import android.text.format.DateUtils
import com.tangem.utils.extensions.isToday
import com.tangem.utils.extensions.isYesterday
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.format.DateTimeFormatter
/**
* If [this] timestamp is today or yesterday, returns relative date,
* otherwise returns formatting date.
*/
fun Long.toDateFormat(formatter: DateTimeFormatter = DateTimeFormatters.dateFormatter): String {
val localDate = DateTime(this, DateTimeZone.getDefault())
return if (localDate.isToday() || localDate.isYesterday()) {
DateUtils.getRelativeTimeSpanString(
this,
DateTime.now().millis,
DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
} else {
DateTimeFormatters.formatDate(formatter = formatter, date = localDate)
}
}
/**
* Returns formatted time according to [formatter].
*/
fun Long.toTimeFormat(formatter: DateTimeFormatter = DateTimeFormatters.timeFormatter): String {
return DateTimeFormatters.formatTime(formatter = formatter, time = DateTime(this, DateTimeZone.getDefault()))
}

View file

@ -13,12 +13,12 @@ class ImageBackgroundContrastChecker(
private val image: Bitmap,
private val backgroundColor: Int,
) {
constructor(drawable: Drawable, backgroundColor: Int) : this(
image = drawable.toBitmap(),
constructor(drawable: Drawable, backgroundColor: Int, size: Int) : this(
image = drawable.toBitmap(width = size, height = size),
backgroundColor = backgroundColor,
)
suspend fun getContrastColorIfNeeded(isDarkTheme: Boolean): Color {
suspend fun getContrastColor(isDarkTheme: Boolean): Color {
return if (isLowContrast()) {
if (isDarkTheme) Color.White else Color.Black
} else {
@ -26,7 +26,7 @@ class ImageBackgroundContrastChecker(
}
}
suspend fun isLowContrast(): Boolean {
private suspend fun isLowContrast(): Boolean {
val palette = generatePaletteAsync(bitmap = image)
val color = palette.getDominantColor(backgroundColor)
val contrast = ColorUtils.calculateContrast(color, backgroundColor)

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.316,6.992C14.311,6.992 13.538,7.568 12.995,8.794C10.963,6.753 7.995,5.045 5.043,4.806C4.152,4.746 4.031,5.519 4.011,5.98C3.816,9.464 6.319,12.606 9.521,13.127C8.61,15.057 7.359,16.684 5.776,18.093C5.387,18.421 5.545,18.934 5.727,19.241C6.61,20.688 9.408,21.692 11.137,21.056C11.667,20.863 11.74,20.513 11.74,20.163V17.849C13.534,17.653 16.32,16.201 15.927,12.273L15.899,11.979C17.227,11.573 18.158,10.6 18.158,9.549C18.158,8.298 16.858,6.992 15.316,6.992ZM15.389,8.388C16.219,8.388 16.867,9.011 16.867,9.549C16.867,10.015 16.316,10.557 15.008,10.749C14.68,10.792 14.49,11.018 14.514,11.33L14.579,12.299C14.809,15.795 12.283,16.265 10.854,16.389C10.558,16.415 10.364,16.641 10.364,16.931V19.736C10.364,19.851 10.339,19.916 10.202,19.928C9.278,20.039 7.687,19.442 7.149,18.81C7.08,18.72 7.08,18.643 7.132,18.596C8.837,17.072 10.214,15.095 11.149,12.747C11.34,12.29 11.121,11.898 10.7,11.893C7.768,11.868 5.549,9.575 5.387,6.45C5.383,6.326 5.428,6.3 5.533,6.322C8.145,6.736 10.574,8.303 12.085,9.848C12.437,10.207 12.667,10.361 13,10.361C13.538,10.361 13.805,10.096 14.081,9.498C14.368,8.862 14.656,8.388 15.389,8.388ZM13.996,8.26L15.283,7.782C14.344,5.408 13.016,3.769 11.242,2.68C10.586,2.283 9.918,2.56 9.732,3.342C9.4,4.691 9.258,5.912 9.449,7.218L10.987,7.995C10.655,6.714 10.663,5.647 10.902,4.418C10.922,4.285 11.028,4.242 11.129,4.319C12.34,5.22 13.255,6.458 13.996,8.26ZM16.749,10.77L19.417,11.185C19.988,11.27 20.256,10.408 19.677,10.143L17.263,9.058L16.749,10.77Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,7 @@
<vector android:height="24dp" android:viewportHeight="25"
android:viewportWidth="25" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<group>
<clip-path android:pathData="M0.676,0.968h24v24h-24z"/>
<path android:fillColor="#909090" android:pathData="M5.676,19.968H6.938L17.174,9.731L15.913,8.47L5.676,18.706V19.968ZM4.176,21.468V18.083L17.366,4.899C17.518,4.761 17.685,4.655 17.867,4.58C18.05,4.505 18.242,4.468 18.442,4.468C18.643,4.468 18.837,4.503 19.025,4.575C19.213,4.646 19.379,4.759 19.524,4.914L20.745,6.151C20.9,6.295 21.011,6.462 21.077,6.651C21.143,6.839 21.176,7.028 21.176,7.216C21.176,7.417 21.142,7.609 21.073,7.792C21.004,7.975 20.895,8.142 20.745,8.293L7.561,21.468H4.176ZM16.532,9.112L15.913,8.47L17.174,9.731L16.532,9.112Z"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15.564,18.109C16.941,18.109 17.59,17.357 17.59,16.486C17.59,14.706 16.348,14.153 14.183,14.153C13.347,14.153 13.085,14.205 12.874,14.253L12.432,13.087C11.6,10.885 10.06,9.95 8.352,9.95C8.097,9.95 7.799,9.95 7.493,10.018C7.071,10.109 6.896,10.288 6.896,10.555C6.896,10.937 7.186,11.2 7.58,11.196C7.815,11.196 8.066,11.156 8.336,11.156C9.682,11.156 10.601,11.916 11.389,13.819L12.078,15.51C12.619,15.403 13.184,15.307 14.135,15.307C15.262,15.307 16.121,15.67 16.121,16.314C16.121,16.561 15.934,16.776 15.564,16.776C14.919,16.776 14.438,16.633 13.821,16.633C13.16,16.633 12.424,16.697 12.189,16.697C11.922,16.697 11.715,16.681 11.512,16.493L8.667,13.711C6.593,13.827 5.268,13.341 4.452,12.211C3.887,12.255 3.465,12.056 3.465,11.646C3.465,11.228 3.918,10.981 4.519,10.981C5.148,9.003 6.322,8.107 8.054,8.107C10.044,8.107 11.767,9.648 13.574,10.543C14.115,10.802 14.712,10.921 15.071,10.921C16.034,10.921 16.702,10.356 16.917,9.305L12.858,6.539C12.555,6.336 12.209,6.05 12.209,5.902C12.209,5.584 12.814,5.401 13.295,5.401C14.207,5.401 14.896,5.879 15.588,6.671L17.705,9.102C19.504,9.106 20.667,10.372 20.667,11.988C20.667,12.935 20.249,13.206 18.983,13.206C18.259,13.206 17.343,12.931 16.476,12.462C15.226,12.776 13.916,13.465 13.092,14.281L14.235,14.691C14.971,14.217 15.779,13.891 16.316,13.684C17.212,14.197 18.195,14.42 18.947,14.416C20.901,14.408 22,13.707 22,11.98C22,9.795 20.44,8.167 18.318,8.048L17.033,6.209C16.034,4.788 14.876,4 13.28,4C12.113,4 10.601,4.633 10.601,5.767C10.601,6.48 11.174,6.933 11.994,7.479C12.754,7.98 14.398,9.003 15.278,9.556C15.21,9.596 15.126,9.628 15.027,9.628C14.792,9.628 14.362,9.465 14.056,9.258C11.648,7.992 10.104,6.675 8.066,6.675C5.829,6.675 4.197,7.845 3.417,10.149C2.537,10.308 2,10.906 2,11.753C2,12.776 2.792,13.377 3.755,13.389C4.364,14.424 6.004,15.256 8.03,15.097L10.573,17.52C10.983,17.922 11.5,18.07 12.161,18.07C12.706,18.07 13.192,17.942 13.861,17.942C14.557,17.942 14.999,18.109 15.564,18.109ZM7.389,18.838C8.826,18.838 10.247,18.352 11.063,17.385L10.107,16.593C9.69,17.234 8.639,17.624 7.429,17.624C6.649,17.624 6.088,17.369 6.088,16.927C6.088,16.72 6.243,16.553 6.529,16.553C6.907,16.553 7.282,16.661 7.755,16.661C8.499,16.661 9.132,16.549 9.63,16.115L8.758,15.252C8.452,15.467 8.078,15.526 7.759,15.526C7.158,15.522 6.808,15.403 6.326,15.403C5.339,15.403 4.675,16.004 4.675,16.907C4.675,18.113 5.761,18.838 7.389,18.838ZM18.537,11.622C18.876,11.622 19.15,11.347 19.15,11.009C19.15,10.675 18.876,10.396 18.537,10.396C18.203,10.396 17.924,10.675 17.924,11.009C17.924,11.347 18.203,11.622 18.537,11.622Z"
android:fillColor="#909090"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h24v24h-24z"/>
<path
android:pathData="M4,4H10V10H4V4ZM20,4V10H14V4H20ZM14,15H16V13H14V11H16V13H18V11H20V13H18V15H20V18H18V20H16V18H13V20H11V16H14V15ZM16,15V18H18V15H16ZM4,20V14H10V20H4ZM6,6V8H8V6H6ZM16,6V8H18V6H16ZM6,16V18H8V16H6ZM4,11H6V13H4V11ZM9,11H13V15H11V13H9V11ZM11,6H13V10H11V6ZM2,2V6H0V2C0,1.47 0.211,0.961 0.586,0.586C0.961,0.211 1.47,0 2,0L6,0V2H2ZM22,0C22.53,0 23.039,0.211 23.414,0.586C23.789,0.961 24,1.47 24,2V6H22V2H18V0H22ZM2,18V22H6V24H2C1.47,24 0.961,23.789 0.586,23.414C0.211,23.039 0,22.53 0,22V18H2ZM22,22V18H24V22C24,22.53 23.789,23.039 23.414,23.414C23.039,23.789 22.53,24 22,24H18V22H22Z"
android:fillColor="#1E1E1E"/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M9.408,17.317C11.952,17.317 12.77,15.774 14.392,15.699C16.461,15.551 17.283,14.326 18.957,14.326C20.756,14.326 22,13.667 22,12.497C22,10.076 20.634,8.665 18.879,8.665C17.231,8.665 16.213,9.635 15.865,11.172C15.739,11.701 15.506,12.038 15.177,12.267L14.943,11.842C13.348,8.917 11.848,7.732 9.416,7.725C6.906,7.732 5.469,8.876 3.703,12.179C2.985,13.53 2,13.708 2,14.618C2,15.277 2.529,15.699 3.462,15.699C3.666,15.699 3.881,15.699 4.088,15.699C6.002,15.699 7.043,17.317 9.408,17.317ZM9.408,15.87C7.513,15.87 6.332,14.267 4.54,14.267C4.344,14.267 4.155,14.27 3.925,14.281L3.899,14.189C4.281,13.844 4.632,13.489 4.943,12.863C6.391,10.09 7.454,9.176 9.416,9.168C11.493,9.176 12.548,10.394 13.755,12.612C14.166,13.367 14.484,13.878 14.895,14.252C14.736,14.244 14.54,14.252 14.336,14.27C12.352,14.415 11.293,15.87 9.408,15.87ZM16.424,13.963C16.247,13.807 16.076,13.608 15.932,13.348C16.528,13.145 16.924,12.663 17.105,11.79C17.335,10.623 17.924,10.05 18.879,10.05C19.856,10.05 20.663,10.972 20.663,12.319C20.663,12.66 19.916,12.974 19.175,12.974C17.816,12.974 17.131,13.422 16.424,13.963ZM19.012,11.982C19.323,11.982 19.582,11.723 19.582,11.412C19.582,11.097 19.323,10.842 19.012,10.842C18.697,10.842 18.442,11.097 18.442,11.412C18.442,11.723 18.697,11.982 19.012,11.982ZM9.364,12.73C11.367,12.73 12.851,11.805 13.74,10.864L13.207,9.913C12.193,11.035 10.852,11.649 9.364,11.649C7.853,11.649 6.513,11.042 5.517,9.909L4.988,10.868C5.869,11.801 7.354,12.73 9.364,12.73ZM6.935,15.614C7.587,14.94 8.201,13.434 8.268,12.438L7.328,12.149C7.287,12.941 6.639,14.552 5.976,15.189L6.935,15.614ZM11.893,15.614L12.855,15.189C12.189,14.548 11.541,12.945 11.5,12.149L10.56,12.43C10.597,13.419 11.215,14.922 11.893,15.614ZM3.907,18.024C5.195,18.024 6.484,17.476 7.476,16.451L6.476,15.884C5.491,16.673 4.71,17.014 3.977,17.014C3.551,17.014 3.325,16.869 3.325,16.658C3.325,16.507 3.455,16.399 3.755,16.318C4.04,16.233 5.151,15.907 5.676,15.573L4.74,15.229C4.366,15.392 3.87,15.466 3.455,15.611C2.792,15.811 2.159,16.062 2.159,16.832C2.159,17.58 2.889,18.024 3.907,18.024ZM14.736,18.024C15.754,18.024 16.487,17.584 16.487,16.832C16.487,15.996 15.762,15.748 15.021,15.551C14.636,15.448 14.229,15.374 13.903,15.233L12.97,15.577C13.496,15.907 14.603,16.24 14.892,16.318C15.191,16.399 15.317,16.507 15.317,16.662C15.317,16.873 15.091,17.017 14.666,17.017C13.933,17.017 13.151,16.677 12.17,15.884L11.171,16.451C12.163,17.447 13.455,18.024 14.736,18.024Z"
android:fillColor="#909090"/>
</vector>

View file

@ -23,12 +23,15 @@ dependencies {
/** Project - Utils */
implementation(projects.core.utils)
/** AndroidX */
implementation(deps.androidx.datastore)
/** DI */
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)
/** Other */
implementation(deps.jodatime)
implementation(deps.kotlin.coroutines)
implementation(deps.timber)
implementation(deps.jodatime)
}

View file

@ -6,12 +6,16 @@ import com.tangem.data.common.cache.CacheRegistry
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -19,23 +23,25 @@ import org.joda.time.Duration
internal class DefaultAppCurrencyRepository(
private val tangemTechApi: TangemTechApi,
private val appPreferencesStore: AppPreferencesStore,
private val availableAppCurrenciesStore: AvailableAppCurrenciesStore,
private val selectedAppCurrencyStore: SelectedAppCurrencyStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : AppCurrencyRepository {
private val appCurrencyConverter = AppCurrencyConverter()
override fun getSelectedAppCurrency(): Flow<AppCurrency> = channelFlow {
launch(dispatchers.io) {
selectedAppCurrencyStore.get()
.map(appCurrencyConverter::convert)
.collect(::send)
}
override fun getSelectedAppCurrency(): Flow<AppCurrency> {
return channelFlow {
launch {
appPreferencesStore
.getObject<CurrenciesResponse.Currency>(key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY)
.filterNotNull()
.map(appCurrencyConverter::convert)
.collect(::send)
}
withContext(dispatchers.io) {
if (selectedAppCurrencyStore.isEmpty()) {
withContext(dispatchers.io) {
fetchDefaultAppCurrency()
}
}
@ -61,7 +67,10 @@ internal class DefaultAppCurrencyRepository(
"Unable to find app currency with provided code: $currencyCode"
}
selectedAppCurrencyStore.store(currency)
appPreferencesStore.storeObject(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
value = currency,
)
}
}

View file

@ -4,7 +4,7 @@ import com.tangem.data.appcurrency.DefaultAppCurrencyRepository
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appcurrency.AvailableAppCurrenciesStore
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
@ -21,17 +21,17 @@ internal object AppCurrencyDataModule {
@Singleton
fun provideAppCurrencyRepository(
tangemTechApi: TangemTechApi,
appPreferencesStore: AppPreferencesStore,
availableAppCurrenciesStore: AvailableAppCurrenciesStore,
selectedAppCurrencyStore: SelectedAppCurrencyStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): AppCurrencyRepository {
return DefaultAppCurrencyRepository(
tangemTechApi,
availableAppCurrenciesStore,
selectedAppCurrencyStore,
cacheRegistry,
dispatchers,
tangemTechApi = tangemTechApi,
appPreferencesStore = appPreferencesStore,
availableAppCurrenciesStore = availableAppCurrenciesStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
)
}
}

View file

@ -27,4 +27,7 @@ dependencies {
/** Other */
implementation(deps.kotlin.coroutines)
/** Local storages */
implementation(deps.androidx.datastore)
}

View file

@ -1,35 +1,25 @@
package com.tangem.data.apptheme
import com.tangem.datasource.local.apptheme.AppThemeModeStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.Flow
internal class DefaultAppThemeModeRepository(
private val appThemeModeStore: AppThemeModeStore,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
) : AppThemeModeRepository {
override fun getAppThemeMode(): Flow<AppThemeMode> {
return channelFlow {
launch(dispatchers.io) {
if (appThemeModeStore.isEmpty()) {
appThemeModeStore.store(AppThemeMode.DEFAULT)
}
}
launch(dispatchers.io) {
appThemeModeStore.get().collect(::send)
}
}
return appPreferencesStore.getObject(
key = PreferencesKeys.APP_THEME_MODE_KEY,
default = AppThemeMode.DEFAULT,
)
}
override suspend fun changeAppThemeMode(mode: AppThemeMode) {
withContext(dispatchers.io) {
appThemeModeStore.store(mode)
}
appPreferencesStore.storeObject(key = PreferencesKeys.APP_THEME_MODE_KEY, value = mode)
}
}

View file

@ -1,9 +1,8 @@
package com.tangem.data.apptheme.di
import com.tangem.data.apptheme.DefaultAppThemeModeRepository
import com.tangem.datasource.local.apptheme.AppThemeModeStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -16,10 +15,7 @@ internal object AppThemeModeDataModule {
@Provides
@Singleton
fun provideAppThemeModeRepository(
appThemeModeStore: AppThemeModeStore,
dispatchers: CoroutineDispatcherProvider,
): AppThemeModeRepository {
return DefaultAppThemeModeRepository(appThemeModeStore, dispatchers)
fun provideAppThemeModeRepository(appPreferencesStore: AppPreferencesStore): AppThemeModeRepository {
return DefaultAppThemeModeRepository(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -11,10 +11,10 @@ android {
}
dependencies {
implementation(deps.androidx.datastore)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
implementation(deps.kotlin.coroutines)

View file

@ -1,38 +1,43 @@
package com.tangem.data.balancehiding
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrDefault
import com.tangem.datasource.local.preferences.utils.storeObject
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.withContext
internal class DefaultBalanceHidingRepository(
private val balanceHidingSettingsStore: BalanceHidingSettingsStore,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
) : BalanceHidingRepository {
override var isUpdateEnabled: Boolean = true
override fun getBalanceHidingSettingsFlow(): Flow<BalanceHidingSettings> {
return balanceHidingSettingsStore.get()
.onStart { emit(getBalanceHidingSettings()) }
.flowOn(dispatchers.io)
.distinctUntilChanged()
return appPreferencesStore.getObject(
key = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY,
default = DEFAULT_HIDING_SETTINGS,
)
}
override suspend fun storeBalanceHidingSettings(balanceHidingSettings: BalanceHidingSettings) {
withContext(dispatchers.io) {
balanceHidingSettingsStore.store(balanceHidingSettings)
}
override suspend fun storeBalanceHidingSettings(isBalanceHidden: BalanceHidingSettings) {
appPreferencesStore.storeObject(key = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY, value = isBalanceHidden)
}
override suspend fun getBalanceHidingSettings(): BalanceHidingSettings {
return withContext(dispatchers.io) {
balanceHidingSettingsStore.getSyncOrDefault()
}
return appPreferencesStore.getObjectSyncOrDefault(
key = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY,
default = DEFAULT_HIDING_SETTINGS,
)
}
private companion object {
val DEFAULT_HIDING_SETTINGS = BalanceHidingSettings(
isHidingEnabledInSettings = false,
isBalanceHidden = false,
isBalanceHidingNotificationEnabled = true,
)
}
}

View file

@ -3,16 +3,15 @@ package com.tangem.data.balancehiding.di
import android.content.Context
import com.tangem.data.balancehiding.DefaultBalanceHidingRepository
import com.tangem.data.balancehiding.DefaultDeviceFlipDetector
import com.tangem.datasource.local.appcurrency.BalanceHidingSettingsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.balancehiding.DeviceFlipDetector
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import dagger.hilt.android.qualifiers.ApplicationContext
@Module
@InstallIn(SingletonComponent::class)
@ -20,14 +19,8 @@ internal object BalanceHidingModule {
@Provides
@Singleton
fun provideBalanceHidingRepository(
balanceHidingSettingsStore: BalanceHidingSettingsStore,
dispatchers: CoroutineDispatcherProvider,
): BalanceHidingRepository {
return DefaultBalanceHidingRepository(
balanceHidingSettingsStore = balanceHidingSettingsStore,
dispatchers = dispatchers,
)
fun provideBalanceHidingRepository(appPreferencesStore: AppPreferencesStore): BalanceHidingRepository {
return DefaultBalanceHidingRepository(appPreferencesStore = appPreferencesStore)
}
@Provides

View file

@ -11,6 +11,8 @@ android {
}
dependencies {
implementation(deps.androidx.datastore)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)

View file

@ -1,43 +1,37 @@
package com.tangem.data.card
import com.tangem.datasource.local.card.UsedCardInfo
import com.tangem.datasource.local.card.UsedCardsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.domain.card.repository.CardRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.flow.map
internal class DefaultCardRepository(
private val usedCardsStore: UsedCardsStore,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
) : CardRepository {
override fun wasCardScanned(cardId: String): Flow<Boolean> {
return channelFlow {
launch(dispatchers.io) {
usedCardsStore.get()
.collect { savedCards ->
send(element = savedCards.any { it.cardId == cardId })
}
return appPreferencesStore.getObject<List<UsedCardInfo>>(key = PreferencesKeys.USED_CARDS_INFO_KEY)
.map { savedCards ->
savedCards?.any { it.cardId == cardId } ?: false
}
withContext(dispatchers.io) {
if (usedCardsStore.getSyncOrNull() == null) {
send(element = false)
}
}
}
}
override suspend fun setCardWasScanned(cardId: String) {
withContext(dispatchers.io) {
usedCardsStore.store(
item = usedCardsStore.getSyncOrNull()
?.updateCard(cardId)
?: listOf(UsedCardInfo(cardId = cardId, isScanned = true)),
appPreferencesStore.editData { mutablePreferences ->
val usedCards: List<UsedCardInfo>? = mutablePreferences.getObject(
key = PreferencesKeys.USED_CARDS_INFO_KEY,
)
val updatedUsedCards = usedCards?.updateCard(cardId)
?: listOf(UsedCardInfo(cardId = cardId, isScanned = true))
mutablePreferences.setObject(
key = PreferencesKeys.USED_CARDS_INFO_KEY,
value = updatedUsedCards,
)
}
}

View file

@ -4,10 +4,9 @@ import com.tangem.data.card.DefaultCardRepository
import com.tangem.data.card.DefaultCardSdkConfigRepository
import com.tangem.data.card.sdk.CardSdkProvider
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.local.card.UsedCardsStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -32,10 +31,7 @@ internal object CardDataModule {
@Provides
@Singleton
fun provideCardRepository(
usedCardsStore: UsedCardsStore,
dispatchers: CoroutineDispatcherProvider,
): CardRepository {
return DefaultCardRepository(usedCardsStore = usedCardsStore, dispatchers = dispatchers)
fun provideCardRepository(appPreferencesStore: AppPreferencesStore): CardRepository {
return DefaultCardRepository(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -12,18 +12,19 @@ android {
dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.data.source.preferences)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.settings)
implementation(deps.androidx.datastore)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
implementation(deps.kotlin.coroutines)
implementation(projects.core.utils)
implementation(projects.core.datasource)
implementation(projects.domain.settings)
implementation(projects.domain.balanceHiding.models)
implementation(projects.data.source.preferences)
}

View file

@ -1,68 +1,42 @@
package com.tangem.data.settings
import com.tangem.datasource.local.settings.AppLaunchCountStore
import com.tangem.datasource.local.settings.AppRatingShowingCountStore
import com.tangem.datasource.local.settings.FundsFoundDateInMillisStore
import com.tangem.datasource.local.settings.UserInteractingStatusStore
import androidx.datastore.preferences.core.MutablePreferences
import androidx.datastore.preferences.core.edit
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.domain.settings.repositories.AppRatingRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.withContext
import java.util.Calendar
internal class DefaultAppRatingRepository(
private val fundsFoundDateInMillisStore: FundsFoundDateInMillisStore,
private val appLaunchCountStore: AppLaunchCountStore,
private val appRatingShowingCountStore: AppRatingShowingCountStore,
private val userInteractingStatusStore: UserInteractingStatusStore,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
) : AppRatingRepository {
override suspend fun initialize() {
fundsFoundDateInMillisStore.getSyncOrNull()
?: fundsFoundDateInMillisStore.store(item = FUNDS_FOUND_DATE_UNDEFINED)
appLaunchCountStore.getSyncOrNull()
?: appLaunchCountStore.store(item = DEFAULT_APP_LAUNCH_COUNT)
appRatingShowingCountStore.getSyncOrNull()
?: appRatingShowingCountStore.store(item = FIRST_SHOWING_COUNT)
userInteractingStatusStore.getSyncOrNull()
?: userInteractingStatusStore.store(item = false)
}
override suspend fun setWalletWithFundsFound() {
withContext(dispatchers.io) {
val foundDate = fundsFoundDateInMillisStore.getSyncOrNull()
if (foundDate != null && foundDate != FUNDS_FOUND_DATE_UNDEFINED) return@withContext
appPreferencesStore.edit { mutablePreferences ->
val foundDate = mutablePreferences[PreferencesKeys.FUNDS_FOUND_DATE_KEY]
if (foundDate != null && foundDate != FUNDS_FOUND_DATE_UNDEFINED) return@edit
fundsFoundDateInMillisStore.store(item = Calendar.getInstance().timeInMillis)
mutablePreferences[PreferencesKeys.FUNDS_FOUND_DATE_KEY] = Calendar.getInstance().timeInMillis
val appLaunchCount = appLaunchCountStore.getSyncOrNull().let { count ->
if (count == null) {
appLaunchCountStore.store(item = DEFAULT_APP_LAUNCH_COUNT)
DEFAULT_APP_LAUNCH_COUNT
} else {
count
}
}
appRatingShowingCountStore.store(item = appLaunchCount + FIRST_SHOWING_COUNT)
userInteractingStatusStore.getSyncOrNull()
?: userInteractingStatusStore.store(item = false)
val appLaunchCount = mutablePreferences[PreferencesKeys.APP_LAUNCH_COUNT_KEY] ?: DEFAULT_APP_LAUNCH_COUNT
mutablePreferences[PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY] =
appLaunchCount + FIRST_SHOWING_COUNT
}
}
override fun isReadyToShow(): Flow<Boolean> {
// TODO: [REDACTED_JIRA]
return combine(
userInteractingStatusStore.get(),
appRatingShowingCountStore.get(),
appLaunchCountStore.get(),
fundsFoundDateInMillisStore.get(),
appPreferencesStore.get(key = PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY, default = false),
appPreferencesStore.get(
key = PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY,
default = FIRST_SHOWING_COUNT,
),
appPreferencesStore.get(key = PreferencesKeys.APP_LAUNCH_COUNT_KEY, default = DEFAULT_APP_LAUNCH_COUNT),
appPreferencesStore.get(key = PreferencesKeys.FUNDS_FOUND_DATE_KEY, default = FUNDS_FOUND_DATE_UNDEFINED),
) { isInteracting, ratingShowingCount, appLaunchCount, fundsFoundDate ->
if (!isInteracting) {
val diff = Calendar.getInstance().timeInMillis - fundsFoundDate
@ -77,20 +51,22 @@ internal class DefaultAppRatingRepository(
}
override suspend fun remindLater() {
appLaunchCountStore.getSyncOrNull()?.let {
updateNextShowing(at = it + DEFER_SHOWING_COUNT)
appPreferencesStore.edit { mutablePreferences ->
mutablePreferences[PreferencesKeys.APP_LAUNCH_COUNT_KEY]?.let {
mutablePreferences.updateNextShowing(it + DEFER_SHOWING_COUNT)
}
}
}
override suspend fun setNeverToShow() {
updateNextShowing(at = Int.MAX_VALUE)
appPreferencesStore.edit { mutablePreferences ->
mutablePreferences.updateNextShowing(at = Int.MAX_VALUE)
}
}
private suspend fun updateNextShowing(at: Int) {
withContext(dispatchers.io) {
appRatingShowingCountStore.store(item = at)
userInteractingStatusStore.store(item = true)
}
private fun MutablePreferences.updateNextShowing(at: Int) {
this[PreferencesKeys.SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY] = at
this[PreferencesKeys.USER_WAS_INTERACT_WITH_RATING_KEY] = true
}
private companion object {

View file

@ -3,10 +3,7 @@ package com.tangem.data.settings.di
import com.tangem.data.settings.DefaultAppRatingRepository
import com.tangem.data.settings.DefaultSettingsRepository
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.local.settings.AppLaunchCountStore
import com.tangem.datasource.local.settings.AppRatingShowingCountStore
import com.tangem.datasource.local.settings.FundsFoundDateInMillisStore
import com.tangem.datasource.local.settings.UserInteractingStatusStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -34,19 +31,7 @@ internal object SettingsDataModule {
@Provides
@Singleton
fun provideAppRatingRepository(
fundsFoundDateInMillisStore: FundsFoundDateInMillisStore,
appLaunchCountStore: AppLaunchCountStore,
appRatingShowingCountStore: AppRatingShowingCountStore,
userInteractingStatusStore: UserInteractingStatusStore,
dispatchers: CoroutineDispatcherProvider,
): AppRatingRepository {
return DefaultAppRatingRepository(
fundsFoundDateInMillisStore = fundsFoundDateInMillisStore,
appLaunchCountStore = appLaunchCountStore,
appRatingShowingCountStore = appRatingShowingCountStore,
userInteractingStatusStore = userInteractingStatusStore,
dispatchers = dispatchers,
)
fun provideAppRatingRepository(appPreferencesStore: AppPreferencesStore): AppRatingRepository {
return DefaultAppRatingRepository(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -32,6 +32,9 @@ dependencies {
implementation(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
/** AndroidX */
implementation(deps.androidx.datastore)
/** DI */
implementation(deps.hilt.core)
kapt(deps.hilt.kapt)

View file

@ -6,8 +6,8 @@ import com.tangem.data.tokens.repository.DefaultMarketCryptoCurrencyRepository
import com.tangem.data.tokens.repository.DefaultNetworksRepository
import com.tangem.data.tokens.repository.DefaultQuotesRepository
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.local.network.NetworksStatusesStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.datasource.local.token.UserMarketCoinsStore
import com.tangem.datasource.local.token.UserTokensStore
@ -52,15 +52,15 @@ internal object TokensDataModule {
@Singleton
fun provideQuotesRepository(
tangemTechApi: TangemTechApi,
appPreferencesStore: AppPreferencesStore,
quotesStore: QuotesStore,
selectedAppCurrencyStore: SelectedAppCurrencyStore,
cacheRegistry: CacheRegistry,
dispatchers: CoroutineDispatcherProvider,
): QuotesRepository {
return DefaultQuotesRepository(
tangemTechApi = tangemTechApi,
appPreferencesStore = appPreferencesStore,
quotesStore = quotesStore,
selectedAppCurrencyStore = selectedAppCurrencyStore,
cacheRegistry = cacheRegistry,
dispatchers = dispatchers,
)

View file

@ -5,7 +5,12 @@ import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.tokens.utils.QuotesConverter
import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.appcurrency.SelectedAppCurrencyStore
import com.tangem.datasource.api.tangemTech.models.CurrenciesResponse
import com.tangem.datasource.local.*
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.quote.QuotesStore
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Quote
@ -17,8 +22,8 @@ import kotlinx.coroutines.withContext
internal class DefaultQuotesRepository(
private val tangemTechApi: TangemTechApi,
private val appPreferencesStore: AppPreferencesStore,
private val quotesStore: QuotesStore,
private val selectedAppCurrencyStore: SelectedAppCurrencyStore,
private val cacheRegistry: CacheRegistry,
private val dispatchers: CoroutineDispatcherProvider,
) : QuotesRepository {
@ -31,8 +36,11 @@ internal class DefaultQuotesRepository(
@OptIn(ExperimentalCoroutinesApi::class)
override fun getQuotesUpdates(currenciesIds: Set<CryptoCurrency.ID>): Flow<Set<Quote>> {
return selectedAppCurrencyStore.get()
return appPreferencesStore.getObject<CurrenciesResponse.Currency>(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
)
.distinctUntilChanged()
.filterNotNull()
.flatMapLatest { appCurrency ->
fetchExpiredQuotes(currenciesIds, appCurrency.id, refresh = false)
@ -44,13 +52,16 @@ internal class DefaultQuotesRepository(
override suspend fun getQuotesSync(currenciesIds: Set<CryptoCurrency.ID>, refresh: Boolean): Set<Quote> {
return withContext(dispatchers.io) {
val selectedAppCurrency = requireNotNull(selectedAppCurrencyStore.getSyncOrNull()) {
"Unable to get selected application currency to update quotes"
}
val selectedAppCurrency = requireNotNull(
value = appPreferencesStore.getObjectSyncOrNull<CurrenciesResponse.Currency>(
key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY,
),
lazyMessage = { "Unable to get selected application currency to update quotes" },
)
fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, refresh)
val quotes = quotesStore.get(currenciesIds).first()
val quotes = quotesStore.getSync(currenciesIds)
quotesConverter.convertSet(quotes)
}

View file

@ -13,10 +13,12 @@ android {
dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.data.source.preferences)
implementation(projects.domain.wallets)
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Local storages */
implementation(deps.androidx.datastore)
}

View file

@ -1,29 +1,26 @@
package com.tangem.data.wallets
import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
internal class DefaultWalletsRepository(
private val shouldSaveUserWalletStore: ShouldSaveUserWalletStore,
private val dispatchers: CoroutineDispatcherProvider,
private val appPreferencesStore: AppPreferencesStore,
) : WalletsRepository {
override suspend fun initialize() {
withContext(dispatchers.io) {
shouldSaveUserWalletStore.getSyncOrNull() ?: shouldSaveUserWalletStore.store(item = false)
}
}
override suspend fun shouldSaveUserWalletsSync(): Boolean {
return withContext(dispatchers.io) { shouldSaveUserWalletStore.getSyncOrNull() ?: false }
return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
}
override fun shouldSaveUserWallets(): Flow<Boolean> = shouldSaveUserWalletStore.get()
override fun shouldSaveUserWallets(): Flow<Boolean> {
return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
}
override suspend fun saveShouldSaveUserWallets(item: Boolean) {
withContext(dispatchers.io) { shouldSaveUserWalletStore.store(item = item) }
appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item)
}
}

View file

@ -1,9 +1,8 @@
package com.tangem.data.wallets.di
import com.tangem.data.wallets.DefaultWalletsRepository
import com.tangem.datasource.local.userwallet.ShouldSaveUserWalletStore
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -12,17 +11,11 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object WalletsDataModule {
internal object WalletsDataModule {
@Provides
@Singleton
fun providesWalletsRepository(
shouldSaveUserWalletStore: ShouldSaveUserWalletStore,
coroutineDispatcherProvider: CoroutineDispatcherProvider,
): WalletsRepository {
return DefaultWalletsRepository(
shouldSaveUserWalletStore = shouldSaveUserWalletStore,
dispatchers = coroutineDispatcherProvider,
)
fun providesWalletsRepository(appPreferencesStore: AppPreferencesStore): WalletsRepository {
return DefaultWalletsRepository(appPreferencesStore = appPreferencesStore)
}
}

View file

@ -10,6 +10,7 @@ import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
@ -200,6 +201,7 @@ class DefaultWalletManagersFacade(
val itemsResult = walletManager.getTransactionsHistory(
request = TransactionHistoryRequest(
address = walletManager.wallet.address,
decimals = currency.decimals,
page = TransactionHistoryRequest.Page(number = page, size = pageSize),
filterType = when (currency) {
is CryptoCurrency.Coin -> TransactionHistoryRequest.FilterType.Coin
@ -323,6 +325,10 @@ class DefaultWalletManagersFacade(
}
override suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address> {
return getAddresses(userWalletId, network).sortedBy { it.type }
}
override suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address> {
val blockchain = Blockchain.fromId(network.id.value)
return getOrCreateWalletManager(
@ -332,7 +338,6 @@ class DefaultWalletManagersFacade(
)
?.wallet
?.addresses
?.sortedBy { it.type }
.orEmpty()
}
@ -408,6 +413,24 @@ class DefaultWalletManagersFacade(
}
}
override suspend fun getFee(
amount: Amount,
destination: String,
userWalletId: UserWalletId,
network: Network,
): Result<TransactionFee>? {
val blockchain = Blockchain.fromId(network.id.value)
val walletManager = getOrCreateWalletManager(
userWalletId = userWalletId,
blockchain = blockchain,
derivationPath = network.derivationPath.value,
)
return (walletManager as? TransactionSender)?.getFee(
amount = amount,
destination = destination,
)
}
private fun updateWalletManagerTokensIfNeeded(walletManager: WalletManager, tokens: Set<CryptoCurrency.Token>) {
if (tokens.isEmpty()) return

View file

@ -2,10 +2,13 @@ package com.tangem.domain.walletmanager
import arrow.core.Either
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -114,6 +117,13 @@ interface WalletManagersFacade {
*/
suspend fun getAddress(userWalletId: UserWalletId, network: Network): List<Address>
/** Returns list of all addresses for all currencies in selected wallet
*
* @param userWalletId selected wallet id
* @param network required to create wallet manager
*/
suspend fun getAddresses(userWalletId: UserWalletId, network: Network): Set<Address>
/**
* Returns info about rent if wallet manager implemented [RentProvider], otherwise null
*
@ -136,4 +146,19 @@ interface WalletManagersFacade {
network: Network,
signedHashes: Int,
): Either<Throwable, Unit>
/**
* Returns fee for transaction
*
* @param amount of transaction
* @param destination address
* @param userWalletId selected wallet id
* @param network network of currency
*/
suspend fun getFee(
amount: Amount,
destination: String,
userWalletId: UserWalletId,
network: Network,
): Result<TransactionFee>?
}

View file

@ -4,9 +4,6 @@ import kotlinx.coroutines.flow.Flow
interface AppRatingRepository {
// FIXME: We must to initialize all stores before calling [isReadyToShow], otherwise flow will not emit data
suspend fun initialize()
suspend fun setWalletWithFundsFound()
fun isReadyToShow(): Flow<Boolean>

1
domain/transaction/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,23 @@
plugins {
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
id("configuration")
}
android {
namespace = "com.tangem.domain.transaction"
}
dependencies {
implementation(deps.kotlin.coroutines)
implementation(deps.arrow.core)
implementation(projects.core.utils)
implementation(deps.tangem.blockchain)
implementation(projects.domain.legacy)
implementation(projects.domain.wallets.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
}

View file

@ -0,0 +1,5 @@
package com.tangem.domain.transaction.error
sealed class GetFeeError {
object DataError : GetFeeError()
}

View file

@ -0,0 +1,65 @@
package com.tangem.domain.transaction.usecase
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.blockchain.extensions.Result
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.transaction.error.GetFeeError
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.math.BigDecimal
/**
* Use case to get transaction fee
*/
class GetFeeUseCase(
private val walletManagersFacade: WalletManagersFacade,
private val dispatcher: CoroutineDispatcherProvider,
) {
suspend operator fun invoke(
amount: BigDecimal,
destination: String,
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<Either<GetFeeError.DataError, TransactionFee>> {
return flow {
val result = walletManagersFacade.getFee(
amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount),
destination = destination,
userWalletId = userWalletId,
network = cryptoCurrency.network,
)
val maybeFee = when (result) {
is Result.Success -> result.data.right()
else -> GetFeeError.DataError.left()
}
emit(maybeFee)
}.flowOn(dispatcher.io)
}
private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount(
currencySymbol = cryptoCurrency.symbol,
value = amount,
decimals = cryptoCurrency.decimals,
type = when (cryptoCurrency) {
is CryptoCurrency.Coin -> AmountType.Coin
is CryptoCurrency.Token -> AmountType.Token(
token = Token(
symbol = cryptoCurrency.symbol,
contractAddress = cryptoCurrency.contractAddress,
decimals = cryptoCurrency.decimals,
),
)
},
)
}

View file

@ -4,8 +4,6 @@ import kotlinx.coroutines.flow.Flow
interface WalletsRepository {
suspend fun initialize()
suspend fun shouldSaveUserWalletsSync(): Boolean
fun shouldSaveUserWallets(): Flow<Boolean>

View file

@ -0,0 +1,42 @@
package com.tangem.managetokens.presentation.common.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.managetokens.impl.R
internal sealed class AlertState {
abstract val message: TextReference
class DefaultAlert(
override val message: TextReference,
) : AlertState()
class TokenUnavailable(
val onUpvoteClick: () -> Unit,
) : AlertState() {
override val message: TextReference = resourceReference(R.string.manage_tokens_unavailable_description)
val confirmButtonText: TextReference = resourceReference(R.string.common_close)
val dismissButtonText: TextReference = resourceReference(R.string.manage_tokens_unavailable_vote)
}
object NonNative : AlertState() {
override val message: TextReference = resourceReference(R.string.manage_tokens_network_selector_non_native_info)
}
object TokensUnsupported : AlertState() {
override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_message)
}
object TokensUnsupportedCurve : AlertState() {
override val message: TextReference = resourceReference(R.string.alert_manage_tokens_unsupported_curve_message)
}
class TokensUnsupportedBlockchainByCard(val token: String) : AlertState() {
override val message: TextReference = resourceReference(
id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message,
formatArgs = wrappedList(token),
)
}
}

Some files were not shown because too many files have changed in this diff Show more