Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-21 12:43:59 +03:00
commit fe2cfac54d
1140 changed files with 23493 additions and 8579 deletions

View file

@ -125,6 +125,12 @@ sealed class AnalyticsParam {
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("NFT"), TxData
data class SendWithSwap(
override val blockchain: String,
override val token: String,
override val feeType: FeeType,
) : TxSentFrom("Send&Swap"), TxData
}
sealed interface TxData {
@ -229,5 +235,10 @@ sealed class AnalyticsParam {
const val STANDARD = "Standard"
const val NO_COLLECTION = "No collection"
const val EMULATION_STATUS = "Emulation Status"
const val SEND_TOKEN = "Send Token"
const val RECEIVE_TOKEN = "Receive Token"
const val SEND_BLOCKCHAIN = "Send Blockchain"
const val RECEIVE_BLOCKCHAIN = "Receive Blockchain"
const val CHOSEN_TOKEN = "Token Chosen"
}
}

View file

@ -50,5 +50,13 @@
{
"name": "HOT_WALLET_ENABLED",
"version": "undefined"
},
{
"name": "NFT_SEND_REDESIGN_ENABLED",
"version": "undefined"
},
{
"name": "TANGEM_PAY_ENABLED",
"version": "undefined"
}
]

View file

@ -14,6 +14,7 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.runBlocking
import javax.inject.Singleton
@Module
@ -41,6 +42,12 @@ internal object FeatureTogglesManagerModule {
localTogglesStorage = localTogglesStorage,
versionProvider = versionProvider,
)
}.also {
// We need to initialize during the hilt graph creation
// in order to provide the feature toggles correctly to other dependencies.
runBlocking {
it.init()
}
}
}
}

View file

@ -9,7 +9,6 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in DEV build
@ -24,10 +23,14 @@ internal class DevFeatureTogglesManager(
private val versionProvider: VersionProvider,
) : MutableFeatureTogglesManager {
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
private var localFeatureTogglesMap: Map<String, Boolean> by Delegates.notNull()
private var featureTogglesMap: MutableMap<String, Boolean>? = null
private var localFeatureTogglesMap: Map<String, Boolean>? = null
override suspend fun init() {
if (featureTogglesMap != null && localFeatureTogglesMap != null) {
return // Already initialized
}
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
@ -46,21 +49,21 @@ internal class DevFeatureTogglesManager(
.toMutableMap()
}
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] ?: false
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap!!
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
featureTogglesMap[name] ?: return
featureTogglesMap[name] = isEnabled
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap)
featureTogglesMap!![name] ?: return
featureTogglesMap!![name] = isEnabled
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!)
}
override suspend fun recoverLocalConfig() {
featureTogglesMap = localFeatureTogglesMap.toMutableMap()
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap)
featureTogglesMap = localFeatureTogglesMap!!.toMutableMap()
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!)
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)

View file

@ -5,7 +5,6 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.storage.TogglesStorage
import com.tangem.core.configtoggle.utils.associateToggles
import com.tangem.core.configtoggle.version.VersionProvider
import kotlin.properties.Delegates
/**
* Feature toggles manager implementation in PROD build
@ -18,18 +17,22 @@ internal class ProdFeatureTogglesManager(
private val versionProvider: VersionProvider,
) : FeatureTogglesManager {
private var featureToggles: Map<String, Boolean> by Delegates.notNull()
private var featureToggles: Map<String, Boolean>? = null
override suspend fun init() {
if (featureToggles != null) {
return // Already initialized
}
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
featureToggles = localTogglesStorage.toggles
.associateToggles(currentVersion = versionProvider.get() ?: "")
}
override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] ?: false
override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun getProdFeatureToggles() = featureToggles
fun getProdFeatureToggles() = featureToggles!!
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun setProdFeatureToggles(map: Map<String, Boolean>) {

View file

@ -52,6 +52,7 @@ dependencies {
/** Coroutines */
implementation(deps.kotlin.coroutines)
implementation(deps.kotlin.coroutines.rx2)
implementation(deps.kotlin.datetime)
/** Logging */
implementation(deps.timber)
@ -76,7 +77,7 @@ dependencies {
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
mockedImplementation(deps.chucker)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)

View file

@ -8,12 +8,12 @@ interface AuthProvider {
/**
* Returns authToken for tangem tech api
*/
fun getCardPublicKey(): String
suspend fun getCardPublicKey(): String
fun getCardId(): String
suspend fun getCardId(): String
/**
* Returns map where keys(cardId) associated with cardPublicKey
*/
fun getCardsPublicKeys(): Map<String, String>
suspend fun getCardsPublicKeys(): Map<String, String>
}

View file

@ -15,12 +15,14 @@ import okio.IOException
* Switch api environment [Interceptor]
*
* @property id api config id [ApiConfig.ID]
* @property baseUrls base urls for all api config environments
* @property apiConfigsManager api configs manager
*
[REDACTED_AUTHOR]
*/
internal class SwitchEnvironmentInterceptor(
private val id: ApiConfig.ID,
private val baseUrls: Set<String>,
private val apiConfigsManager: ApiConfigsManager,
) : Interceptor {
@ -39,10 +41,13 @@ internal class SwitchEnvironmentInterceptor(
return chain.proceed(request)
}
private fun HttpUrl.adjustBaseUrl(url: String): HttpUrl {
return this.newBuilder()
.host(host = url.toHttpUrl().host)
.build()
private fun HttpUrl.adjustBaseUrl(newBaseUrl: String): HttpUrl {
val currentUrl = this.toString()
val currentBaseUrl = baseUrls.first { currentUrl.contains(it) }
return currentUrl
.replace(oldValue = currentBaseUrl, newValue = newBaseUrl)
.toHttpUrl()
}
private fun Request.Builder.addHeaders(headers: Map<String, ProviderSuspend<String>>): Request.Builder {

View file

@ -1,5 +1,6 @@
package com.tangem.datasource.api.common.config
import com.tangem.datasource.BuildConfig
import com.tangem.lib.auth.StakeKitAuthProvider
import com.tangem.utils.ProviderSuspend
@ -14,20 +15,44 @@ internal class StakeKit(
private val stakeKitAuthProvider: StakeKitAuthProvider,
) : ApiConfig() {
override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD
override val defaultEnvironment: ApiEnvironment = getInitialEnvironment()
override val environmentConfigs: List<ApiEnvironmentConfig> = listOf(
createProdEnvironment(),
createMockEnvironment(),
)
private fun getInitialEnvironment(): ApiEnvironment {
return when (BuildConfig.BUILD_TYPE) {
MOCKED_BUILD_TYPE,
-> ApiEnvironment.MOCK
DEBUG_BUILD_TYPE,
INTERNAL_BUILD_TYPE,
EXTERNAL_BUILD_TYPE,
RELEASE_BUILD_TYPE,
-> ApiEnvironment.PROD
else -> error("Unknown build type [${BuildConfig.BUILD_TYPE}]")
}
}
private fun createProdEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.PROD,
baseUrl = "https://api.stakek.it/v1/",
headers = mapOf(
"X-API-KEY" to ProviderSuspend(stakeKitAuthProvider::getApiKey),
"accept" to ProviderSuspend { "application/json" },
),
headers = createHeaders(),
)
}
private fun createMockEnvironment(): ApiEnvironmentConfig {
return ApiEnvironmentConfig(
environment = ApiEnvironment.MOCK,
baseUrl = "[REDACTED_ENV_URL]",
headers = createHeaders(),
)
}
private fun createHeaders() = buildMap {
put(key = "X-API-KEY", value = ProviderSuspend(stakeKitAuthProvider::getApiKey))
put(key = "accept", value = ProviderSuspend { "application/json" })
}
}

View file

@ -1,19 +1,7 @@
package com.tangem.datasource.api.pay
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.pay.models.request.ActivationByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationByCustomerWalletRequest
import com.tangem.datasource.api.pay.models.request.ActivationStatusRequest
import com.tangem.datasource.api.pay.models.request.ExchangeAccessTokenRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GenerateNoneByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.GetAccessTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.GetCardWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.GetCustomerWalletAcceptanceRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardIdRequest
import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest
import com.tangem.datasource.api.pay.models.request.SetPinCodeRequest
import com.tangem.datasource.api.pay.models.request.*
import com.tangem.datasource.api.pay.models.response.*
import retrofit2.http.Body
import retrofit2.http.GET
@ -33,9 +21,17 @@ interface TangemPayApi {
@Body request: GenerateNoneByCardWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/challenge")
suspend fun generateNonceByCustomerWallet(
@Body request: GenerateNonceByCustomerWalletRequest,
): ApiResponse<GenerateNonceResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getTokenByCustomerWallet(@Body request: GetTokenByCustomerWalletRequest): ApiResponse<JWTResponse>
@POST("v1/auth/token")
suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): ApiResponse<JWTResponse>

View file

@ -0,0 +1,10 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GenerateNonceByCustomerWalletRequest(
@Json(name = "auth_type") val authType: String = "customer_wallet",
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
)

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.api.pay.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetTokenByCustomerWalletRequest(
@Json(name = "auth_type") val authType: String = "customer_wallet",
@Json(name = "session_id") val sessionId: String,
@Json(name = "signature") val signature: String,
@Json(name = "message_format") val messageFormat: String,
)

View file

@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody.GasArgs
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.models.staking.action.StakingActionType
@JsonClass(generateAdapter = true)
data class PendingActionRequestBody(

View file

@ -1,9 +1,11 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.promotion.models.PromotionInfoResponse
import com.tangem.datasource.api.promotion.models.StoryContentResponse
import com.tangem.datasource.api.tangemTech.models.*
import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse
import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.local.config.providers.models.ProviderModel
import retrofit2.http.*
@ -30,9 +32,6 @@ interface TangemTechApi {
@Query("limit") limit: Int? = null,
): ApiResponse<CoinsResponse>
@GET("v1/rates")
suspend fun getRates(@Query("currencyId") currencyId: String, @Query("coinIds") coinIds: String): RatesResponse
@GET("v1/currencies")
suspend fun getCurrencyList(
@Header("Cache-Control") cacheControl: String = "max-age=600",
@ -68,46 +67,11 @@ interface TangemTechApi {
@Query("fields") fields: String,
): ApiResponse<QuotesResponse>
@GET("v1/promotion")
suspend fun getPromotionInfo(
@Query("programName") name: String,
@Header("Cache-Control") cacheControl: String = "max-age=600",
): ApiResponse<PromotionInfoResponse>
@GET("v1/settings/{wallet_id}")
suspend fun getUserTokensSettings(@Path("wallet_id") walletId: String): ApiResponse<UserTokensSettingsResponse>
@PUT("v1/settings/{wallet_id}")
suspend fun saveUserTokensSettings(
@Path("wallet_id") walletId: String,
@Body userTokensSettings: UserTokensSettingsResponse,
): ApiResponse<Unit>
@POST("v1/user-network-account")
suspend fun createUserNetworkAccount(
@Body body: CreateUserNetworkAccountBody,
): ApiResponse<CreateUserNetworkAccountResponse>
@POST("v1/account")
suspend fun createUserTokensAccount(
@Body body: CreateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}")
suspend fun updateUserTokensAccount(
@Path("account_id") accountId: Int,
@Body body: UpdateUserTokensAccountBody,
): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}/archive")
suspend fun archiveUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
@PUT("v1/account/{account_id}/unarchive")
suspend fun restoreUserTokensAccount(@Path("account_id") accountId: Int): ApiResponse<UserTokensAccountResponse>
@GET("v1/features")
suspend fun getFeatures(): ApiResponse<FeaturesResponse>
@ReadTimeout(duration = 5, unit = TimeUnit.SECONDS)
@GET("v1/networks/providers")
suspend fun getBlockchainProviders(): Map<String, List<ProviderModel>>
@ -160,7 +124,7 @@ interface TangemTechApi {
suspend fun setNotificationsEnabled(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
// endregion
// region wallets
// region user-wallets
@PATCH("v1/user-wallets/wallets/{wallet_id}")
suspend fun updateWallet(@Path("wallet_id") walletId: String, @Body body: WalletBody): ApiResponse<Unit>
@ -176,4 +140,20 @@ interface TangemTechApi {
@GET("v1/user-wallets/wallets/by-app/{app_id}")
suspend fun getWallets(@Path("app_id") appId: String): ApiResponse<List<WalletResponse>>
// endregion
// region account
@GET("/v1/wallets/{walletId}/accounts")
suspend fun getWalletAccounts(@Path("walletId") walletId: String): ApiResponse<GetWalletAccountsResponse>
@PUT("/v1/wallets/{walletId}/accounts")
suspend fun saveWalletAccounts(
@Path("walletId") walletId: String,
@Header("If-Match") ifMatch: String,
): ApiResponse<SaveWalletAccountsResponse>
@GET("/v1/wallets/{walletId}/accounts/archived")
suspend fun getWalletArchivedAccounts(
@Path("walletId") walletId: String,
): ApiResponse<GetWalletArchivedAccountsResponse>
// endregion
}

View file

@ -1,17 +0,0 @@
package com.tangem.datasource.api.tangemTech
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.tangemTech.models.v2.UserTokensResponseV2
import retrofit2.http.*
interface TangemTechApiV2 {
@GET("user-tokens/{wallet_id}")
suspend fun getUserTokens(@Path("wallet_id") walletId: String): ApiResponse<UserTokensResponseV2>
@PUT("user-tokens/{wallet_id}")
suspend fun saveUserTokens(
@Path("wallet_id") walletId: String,
@Body userTokens: UserTokensResponseV2,
): ApiResponse<Unit>
}

View file

@ -16,6 +16,7 @@ data class UserTokensResponse(
@JsonClass(generateAdapter = true)
data class Token(
@Json(name = "id") val id: String? = null,
@Json(name = "accountId") val accountId: String? = null,
@Json(name = "networkId") val networkId: String,
@Json(name = "derivationPath") val derivationPath: String? = null,
@Json(name = "name") val name: String,

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.GroupType
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse.SortType
@JsonClass(generateAdapter = true)
data class GetWalletAccountsResponse(
@Json(name = "wallet") val wallet: Wallet,
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
@Json(name = "unassignedTokens") val unassignedTokens: List<UserTokensResponse.Token>,
) {
@JsonClass(generateAdapter = true)
data class Wallet(
@Json(name = "version") val version: Int,
@Json(name = "group") val group: GroupType,
@Json(name = "sort") val sort: SortType,
@Json(name = "totalAccounts") val totalAccounts: Int,
)
}

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetWalletArchivedAccountsResponse(
@Json(name = "archivedAccounts") val accounts: List<WalletAccountDTO>,
)

View file

@ -0,0 +1,9 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SaveWalletAccountsResponse(
@Json(name = "accounts") val accounts: List<WalletAccountDTO>,
)

View file

@ -0,0 +1,17 @@
package com.tangem.datasource.api.tangemTech.models.account
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
@JsonClass(generateAdapter = true)
data class WalletAccountDTO(
@Json(name = "id") val id: String,
@Json(name = "name") val name: String,
@Json(name = "derivation") val derivationIndex: Int,
@Json(name = "icon") val icon: String,
@Json(name = "iconColor") val iconColor: String,
@Json(name = "tokens") val tokens: List<UserTokensResponse.Token>? = null,
@Json(name = "totalTokens") val totalTokens: Int? = null,
@Json(name = "totalNetworks") val totalNetworks: Int? = null,
)

View file

@ -1,26 +0,0 @@
package com.tangem.datasource.api.tangemTech.models.v2
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
@JsonClass(generateAdapter = true)
data class UserTokensResponseV2(
@Json(name = "accounts")
val accounts: List<TokensAccount>,
) {
@JsonClass(generateAdapter = true)
data class TokensAccount(
@Json(name = "id")
val id: Int,
@Json(name = "title")
val title: String,
@Json(name = "tokens")
val tokens: List<UserTokensResponse.Token>? = null,
@Json(name = "tokensCount")
val tokensCount: Int? = null,
@Json(name = "archived")
val isArchived: Boolean,
)
}

View file

@ -1,8 +1,5 @@
package com.tangem.datasource.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.blockaid.BlockAidApi
import com.tangem.datasource.api.common.config.ApiConfig
@ -12,44 +9,29 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.config.managers.DevApiConfigsManager
import com.tangem.datasource.api.common.config.managers.MockApiConfigsManager
import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.onramp.OnrampApi
import com.tangem.datasource.api.pay.TangemPayApi
import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechApiV2
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.di.utils.RetrofitApiBuilder
import com.tangem.datasource.di.utils.RetrofitApiBuilder.Timeouts
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.utils.*
import com.tangem.datasource.utils.RequestHeader.AppVersionPlatformHeaders
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object NetworkModule {
private const val PROD_V2_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v2/"
private const val TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS = 60L
private const val STAKE_KIT_API_TIMEOUT_SECONDS = 60L
private val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
ApiConfig.ID.StakeKit,
)
@Provides
@Singleton
fun provideApiConfigManager(
@ -66,286 +48,76 @@ internal object NetworkModule {
@Provides
@Singleton
fun provideExpressApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): TangemExpressApi {
return createApi(
id = ApiConfig.ID.Express,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
)
},
fun provideExpressApi(retrofitApiBuilder: RetrofitApiBuilder): TangemExpressApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.Express,
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideStakeKitApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
apiConfigsManager: ApiConfigsManager,
analyticsErrorHandler: AnalyticsErrorHandler,
appLogsStore: AppLogsStore,
): StakeKitApi {
return createApi(
id = ApiConfig.ID.StakeKit,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
fun provideStakeKitApi(retrofitApiBuilder: RetrofitApiBuilder): StakeKitApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.StakeKit,
applyTimeoutAnnotations = false,
timeouts = Timeouts(
callTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
connectTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
readTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
writeTimeoutSeconds = STAKE_KIT_API_TIMEOUT_SECONDS,
),
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
)
},
)
}
@Provides
@Singleton
fun provideOnrampApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): OnrampApi {
return createApi(
id = ApiConfig.ID.Express,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
)
},
fun provideOnrampApi(retrofitApiBuilder: RetrofitApiBuilder): OnrampApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.Express,
applyTimeoutAnnotations = false,
)
}
@Provides
@Singleton
fun provideTangemTechApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
): TangemTechApi {
return createApi(
id = ApiConfig.ID.TangemTech,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = { applyTimeoutAnnotations() },
)
}
// TODO: It will be deleted in the future or refactored using ApiConfig
@Provides
@Singleton
fun provideTangemTechApiV2(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
): TangemTechApiV2 {
return provideTangemTechApiInternal(
moshi = moshi,
context = context,
appVersionProvider = appVersionProvider,
baseUrl = PROD_V2_TANGEM_TECH_BASE_URL,
analyticsErrorHandler = analyticsErrorHandler,
appInfoProvider = appInfoProvider,
fun provideTangemTechApi(retrofitApiBuilder: RetrofitApiBuilder): TangemTechApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.TangemTech,
applyTimeoutAnnotations = true,
)
}
@Provides
@Singleton
fun provideTangemTechMarketsApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
): TangemTechMarketsApi {
return createApi(
id = ApiConfig.ID.TangemTech,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
this.callTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.connectTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.readTimeout(TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.applyTimeoutAnnotations()
},
fun provideTangemTechMarketsApi(retrofitApiBuilder: RetrofitApiBuilder): TangemTechMarketsApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.TangemTech,
applyTimeoutAnnotations = false,
timeouts = Timeouts(
callTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
connectTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
readTimeoutSeconds = TANGEM_TECH_MARKETS_SERVICE_TIMEOUT_SECONDS,
),
logsSaving = false,
)
}
@Provides
@Singleton
fun provideTangemVisaApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): TangemPayApi {
return createApi<TangemPayApi>(
id = ApiConfig.ID.TangemPay,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
).applyTimeoutAnnotations()
},
fun provideTangemVisaApi(retrofitApiBuilder: RetrofitApiBuilder): TangemPayApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.TangemPay,
applyTimeoutAnnotations = false,
)
}
@Suppress("LongParameterList")
@Deprecated("use createApi instead")
private inline fun <reified T> provideTangemTechApiInternal(
moshi: Moshi,
context: Context,
appVersionProvider: AppVersionProvider,
appInfoProvider: AppInfoProvider,
baseUrl: String,
analyticsErrorHandler: AnalyticsErrorHandler,
timeouts: Timeouts = Timeouts(),
requestHeaders: List<RequestHeader> = listOf(AppVersionPlatformHeaders(appVersionProvider, appInfoProvider)),
): T {
val client = OkHttpClient.Builder()
.applyTimeoutAnnotations()
.let { builder ->
var b = builder
if (timeouts.callTimeoutSeconds != null) {
b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.connectTimeoutSeconds != null) {
b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.readTimeoutSeconds != null) {
b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.writeTimeoutSeconds != null) {
b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS)
}
b
}
.addHeaders(
*requestHeaders.toTypedArray(),
// TODO("refactor header init") get auth data after biometric auth to avoid race condition
// AuthenticationHeader(authProvider),
)
.addLoggers(context)
.build()
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler))
.baseUrl(baseUrl)
.client(client)
.build()
.create(T::class.java)
}
@Provides
@Singleton
fun provideBlockAidApi(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
analyticsErrorHandler: AnalyticsErrorHandler,
apiConfigsManager: ApiConfigsManager,
appLogsStore: AppLogsStore,
): BlockAidApi {
return createApi<BlockAidApi>(
id = ApiConfig.ID.BlockAid,
moshi = moshi,
context = context,
apiConfigsManager = apiConfigsManager,
analyticsErrorHandler = analyticsErrorHandler,
clientBuilder = {
addInterceptor(
NetworkLogsSaveInterceptor(appLogsStore),
).applyTimeoutAnnotations()
},
fun provideBlockAidApi(retrofitApiBuilder: RetrofitApiBuilder): BlockAidApi {
return retrofitApiBuilder.build(
apiConfigId = ApiConfig.ID.BlockAid,
applyTimeoutAnnotations = false,
)
}
private inline fun <reified T> createApi(
id: ApiConfig.ID,
moshi: Moshi,
context: Context,
apiConfigsManager: ApiConfigsManager,
analyticsErrorHandler: AnalyticsErrorHandler,
timeouts: Timeouts = Timeouts(),
clientBuilder: OkHttpClient.Builder.() -> OkHttpClient.Builder = { this },
): T {
val environmentConfig = apiConfigsManager.getEnvironmentConfig(id)
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler))
.baseUrl(environmentConfig.baseUrl)
.client(
OkHttpClient.Builder()
.applyApiConfig(id, apiConfigsManager)
.applyTimeoutAnnotations()
.let { builder ->
var b = builder
if (timeouts.callTimeoutSeconds != null) {
b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.connectTimeoutSeconds != null) {
b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.readTimeoutSeconds != null) {
b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.writeTimeoutSeconds != null) {
b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS)
}
b
}
.addLoggers(context = context, id = id)
.clientBuilder()
.build(),
)
.build()
.create(T::class.java)
}
private fun OkHttpClient.Builder.addLoggers(context: Context, id: ApiConfig.ID): OkHttpClient.Builder {
if (id in excludedApiForLogging) return this
return addLoggers(context)
}
private data class Timeouts(
val callTimeoutSeconds: Long? = null,
val connectTimeoutSeconds: Long? = null,
val readTimeoutSeconds: Long? = null,
val writeTimeoutSeconds: Long? = null,
)
}

View file

@ -0,0 +1,34 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.swap.DefaultSwapBestRateAnimationStore
import com.tangem.datasource.local.swap.DefaultSwapTransactionStatusStore
import com.tangem.datasource.local.swap.SwapBestRateAnimationStore
import com.tangem.datasource.local.swap.SwapTransactionStatusStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object SwapStoreModule {
@Provides
@Singleton
fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore {
return DefaultSwapTransactionStatusStore(
dataStore = RuntimeDataStore(),
)
}
@Provides
@Singleton
fun provideSwapBestRateAnimationStore(): SwapBestRateAnimationStore {
return DefaultSwapBestRateAnimationStore(
dataStore = RuntimeSharedStore(),
)
}
}

View file

@ -1,23 +0,0 @@
package com.tangem.datasource.di
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.swaptx.DefaultSwapTransactionStatusStore
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object SwapTransactionStatusStoreModule {
@Provides
@Singleton
fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore {
return DefaultSwapTransactionStatusStore(
dataStore = RuntimeDataStore(),
)
}
}

View file

@ -0,0 +1,201 @@
package com.tangem.datasource.di.utils
import android.content.Context
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.squareup.moshi.Moshi
import com.tangem.core.analytics.api.AnalyticsErrorHandler
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfigs
import com.tangem.datasource.api.common.config.ApiEnvironmentConfig
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
import com.tangem.datasource.api.utils.ConnectTimeout
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.utils.WriteTimeout
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.utils.NetworkLogsSaveInterceptor
import com.tangem.datasource.utils.addHeaders
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Invocation
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
/**
* A builder class for creating Retrofit API instances
*
* @property apiConfigsManager manages API configurations for different environments
* @property moshi moshi
* @property analyticsErrorHandler handles analytics-related errors
* @property context application context
* @property appLogsStore application logs store
*
[REDACTED_AUTHOR]
*/
@Singleton
internal class RetrofitApiBuilder @Inject constructor(
private val apiConfigs: ApiConfigs,
private val apiConfigsManager: ApiConfigsManager,
@NetworkMoshi private val moshi: Moshi,
private val analyticsErrorHandler: AnalyticsErrorHandler,
@ApplicationContext private val context: Context,
private val appLogsStore: AppLogsStore,
) {
private val configsBaseUrls: Map<ApiConfig.ID, Set<String>> = getConfigsBaseUrls()
/**
* Builds a Retrofit API instance for the specified API configuration ID
*
* @param apiConfigId the ID of the API configuration to use
* @param applyTimeoutAnnotations whether to apply timeout annotations to the requests. See [ReadTimeout], etc.
* @param timeouts optional timeouts for the requests
* @param logsSaving whether to enable logs saving
*
* @return an instance [T] of the specified API interface
*/
inline fun <reified T> build(
apiConfigId: ApiConfig.ID,
applyTimeoutAnnotations: Boolean,
timeouts: Timeouts? = null,
logsSaving: Boolean = true,
): T {
val environmentConfig = apiConfigsManager.getEnvironmentConfig(apiConfigId)
return Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create(analyticsErrorHandler))
.baseUrl(environmentConfig.baseUrl)
.client(
OkHttpClient.Builder()
.applyApiConfig(apiConfigId = apiConfigId, environmentConfig = environmentConfig)
.let {
if (applyTimeoutAnnotations) it.applyTimeoutAnnotations() else it
}
.applyTimeouts(timeouts = timeouts)
.let {
if (logsSaving) it.applyLogsSaving() else it
}
.addLoggers(apiConfigId = apiConfigId, context = context)
.build(),
)
.build()
.create(T::class.java)
}
data class Timeouts(
val callTimeoutSeconds: Long? = null,
val connectTimeoutSeconds: Long? = null,
val readTimeoutSeconds: Long? = null,
val writeTimeoutSeconds: Long? = null,
)
private fun getConfigsBaseUrls(): Map<ApiConfig.ID, Set<String>> {
return apiConfigs.associate { config ->
val allBaseUrls = config.environmentConfigs.mapTo(hashSetOf(), ApiEnvironmentConfig::baseUrl)
config.id to allBaseUrls
}
}
private fun OkHttpClient.Builder.applyApiConfig(
apiConfigId: ApiConfig.ID,
environmentConfig: ApiEnvironmentConfig,
): OkHttpClient.Builder {
return if (BuildConfig.TESTER_MENU_ENABLED) {
addInterceptor(
interceptor = SwitchEnvironmentInterceptor(
id = apiConfigId,
baseUrls = configsBaseUrls[apiConfigId]
?: error("Base URLs for ApiConfig with id [$apiConfigId] not found"),
apiConfigsManager = apiConfigsManager,
),
)
} else {
val headers = environmentConfig.headers
this.addHeaders(headers)
}
}
private fun OkHttpClient.Builder.applyTimeouts(timeouts: Timeouts?): OkHttpClient.Builder {
if (timeouts == null) return this
var b = this
if (timeouts.callTimeoutSeconds != null) {
b = b.callTimeout(timeouts.callTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.connectTimeoutSeconds != null) {
b = b.connectTimeout(timeouts.connectTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.readTimeoutSeconds != null) {
b = b.readTimeout(timeouts.readTimeoutSeconds, TimeUnit.SECONDS)
}
if (timeouts.writeTimeoutSeconds != null) {
b = b.writeTimeout(timeouts.writeTimeoutSeconds, TimeUnit.SECONDS)
}
return b
}
/**
* Apply timeout annotations [Interceptor].
* Add this [Interceptor] to [OkHttpClient] if use timeout annotations for retrofit requests.
*/
private fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builder {
return addInterceptor(
Interceptor { chain ->
val request = chain.request()
val tag = request.tag(Invocation::class.java)
val connectionTimeout = tag?.method()?.getAnnotation(ConnectTimeout::class.java)
val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java)
val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java)
chain
.run {
connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } ?: this
}
.run {
readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } ?: this
}
.run {
writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } ?: this
}
.proceed(request)
},
)
}
private fun OkHttpClient.Builder.applyLogsSaving(): OkHttpClient.Builder {
return addInterceptor(
interceptor = NetworkLogsSaveInterceptor(appLogsStore),
)
}
private fun OkHttpClient.Builder.addLoggers(apiConfigId: ApiConfig.ID, context: Context): OkHttpClient.Builder {
if (apiConfigId in excludedApiForLogging) return this
return if (BuildConfig.LOG_ENABLED) {
addInterceptor(interceptor = ChuckerInterceptor(context))
addInterceptor(interceptor = createNetworkLoggingInterceptor())
} else {
this
}
}
private companion object {
val excludedApiForLogging: Set<ApiConfig.ID> = setOf(
// ApiConfig.ID.StakeKit,
)
}
}

View file

@ -84,6 +84,10 @@ object PreferencesKeys {
val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") }
val REQUIRE_ACCESS_CODE_KEY by lazy { booleanPreferencesKey(name = "requireAccessCode") }
val USE_BIOMETRIC_AUTHENTICATION_KEY by lazy { booleanPreferencesKey(name = "useBiometricAuthentication") }
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
val MARKETS_STAKING_NOTIFICATION_HIDE_CLICKED_KEY by lazy {
@ -149,6 +153,8 @@ object PreferencesKeys {
val TRON_NETWORK_FEE_NOTIFICATION_SHOW_COUNT_KEY by lazy {
intPreferencesKey(name = "tronNetworkFeeNotificationShowCount")
}
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
// endregion
// region Promo
@ -163,6 +169,18 @@ object PreferencesKeys {
fun getShouldShowInitialPermissionScreen(permission: String) =
booleanPreferencesKey("shouldShowInitialPushPermissionScreen_$permission")
// endregion
// region Hot Wallet unlock attempts
fun getHotWalletUnlockAttemptsKey(attemptId: String) =
intPreferencesKey(name = "hotWalletUnlockAttempts_$attemptId")
fun getHotWalletUnlockBootKey(attemptId: String) = intPreferencesKey(name = "hotWalletUnlockBootCount_$attemptId")
fun getHotWalletUnlockDeadlineKey(attemptId: String) =
longPreferencesKey(name = "hotWalletUnlockDeadline_$attemptId")
// endregion
}
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */

View file

@ -0,0 +1,21 @@
package com.tangem.datasource.local.swap
import com.tangem.datasource.local.datastore.RuntimeSharedStore
internal class DefaultSwapBestRateAnimationStore(
private val dataStore: RuntimeSharedStore<Boolean>,
) : SwapBestRateAnimationStore, RuntimeSharedStore<Boolean> by dataStore {
/**
* Returns flag indicating whether should show best rate animation in current session.
* Animation should appear once per session
*
* If true, reset flag to false
*/
override suspend fun getSyncOrNull(): Boolean {
val value = dataStore.getSyncOrNull() ?: true
if (value) {
dataStore.store(false)
}
return value
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.local.swaptx
package com.tangem.datasource.local.swap
import com.tangem.datasource.local.datastore.core.StringKeyDataStore

View file

@ -0,0 +1,11 @@
package com.tangem.datasource.local.swap
/**
* Stores flag indicating whether should show best rate animation in current session.
* Animation should appear once per session
*
* If true, reset flag to false
*/
interface SwapBestRateAnimationStore {
suspend fun getSyncOrNull(): Boolean
}

View file

@ -1,4 +1,4 @@
package com.tangem.datasource.local.swaptx
package com.tangem.datasource.local.swap
/**
* Runtime cache for storing swap transactions statuses sent to analytics

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.BalanceTypeDTO
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.models.staking.BalanceType
import com.tangem.utils.converter.Converter
internal object BalanceTypeConverter : Converter<BalanceTypeDTO, BalanceType> {

View file

@ -1,8 +1,8 @@
package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.PendingActionConstraints
import com.tangem.domain.models.staking.PendingAction
import com.tangem.domain.models.staking.PendingActionConstraints
import com.tangem.utils.converter.Converter
internal object PendingActionConstraintsConverter :

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.models.staking.PendingAction
import com.tangem.utils.converter.Converter
internal object PendingActionConverter : Converter<BalanceDTO.PendingAction, PendingAction> {

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.models.staking.action.StakingActionType
import com.tangem.utils.converter.Converter
@Suppress("CyclomaticComplexMethod")

View file

@ -1,7 +1,7 @@
package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO
import com.tangem.domain.staking.model.stakekit.NetworkType
import com.tangem.domain.models.staking.NetworkType
import com.tangem.utils.converter.TwoWayConverter
@Suppress("CyclomaticComplexMethod", "LongMethod")

View file

@ -1,53 +1,61 @@
package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceItem
import com.tangem.domain.models.staking.BalanceItem
import com.tangem.domain.models.staking.StakingID
import com.tangem.domain.models.staking.YieldBalance
import com.tangem.domain.models.staking.YieldBalanceItem
import com.tangem.utils.converter.Converter
import kotlinx.datetime.Instant
class YieldBalanceConverter(
private val source: StatusSource,
) : Converter<YieldBalanceWrapperDTO, YieldBalance> {
) : Converter<YieldBalanceWrapperDTO, YieldBalance?> {
constructor(isCached: Boolean) : this(source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL)
override fun convert(value: YieldBalanceWrapperDTO): YieldBalance {
override fun convert(value: YieldBalanceWrapperDTO): YieldBalance? {
val stakingId = StakingID(
integrationId = value.integrationId ?: return null,
address = value.addresses.address,
)
return if (value.balances.isEmpty()) {
YieldBalance.Empty(
integrationId = value.integrationId,
address = value.addresses.address,
source = source,
)
YieldBalance.Empty(stakingId = stakingId, source = source)
} else {
YieldBalance.Data(
integrationId = value.integrationId,
address = value.addresses.address,
stakingId = stakingId,
balance = YieldBalanceItem(
items = value.balances.map { item ->
BalanceItem(
groupId = item.groupId,
token = TokenConverter.convert(item.tokenDTO),
type = BalanceTypeConverter.convert(item.type),
amount = item.amount,
rawCurrencyId = item.tokenDTO.coinGeckoId,
// tron-specific. operates validatorAddresses instead of validatorAddress
validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0),
date = item.date?.toDateTime(),
pendingActions = PendingActionConverter
.convertList(item.pendingActions)
.sortedBy { it.passthrough },
pendingActionsConstraints = PendingActionConstraintsConverter
.convertList(item.pendingActionConstraints.orEmpty()),
isPending = false,
)
}
.sortedWith(compareBy({ it.type }, { it.amount })),
items = value.balances
.map { item -> item.toBalanceItem() }
.sortedWith(comparator = compareBy({ it.type }, { it.amount })),
integrationId = value.integrationId,
),
source = source,
)
}
}
private fun BalanceDTO.toBalanceItem(): BalanceItem {
val item = this
return BalanceItem(
groupId = item.groupId,
token = YieldTokenConverter.convert(item.tokenDTO),
type = BalanceTypeConverter.convert(item.type),
amount = item.amount,
rawCurrencyId = item.tokenDTO.coinGeckoId,
// tron-specific. operates validatorAddresses instead of validatorAddress
validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0),
date = item.date?.toString()?.let { Instant.parse(it) },
pendingActions = PendingActionConverter
.convertList(item.pendingActions)
.sortedBy { it.passthrough },
pendingActionsConstraints = PendingActionConstraintsConverter
.convertList(item.pendingActionConstraints.orEmpty()),
isPending = false,
)
}
}

View file

@ -1,13 +1,13 @@
package com.tangem.datasource.local.token.converter
import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO
import com.tangem.domain.staking.model.stakekit.Token
import com.tangem.domain.models.staking.YieldToken
import com.tangem.utils.converter.TwoWayConverter
object TokenConverter : TwoWayConverter<TokenDTO, Token> {
object YieldTokenConverter : TwoWayConverter<TokenDTO, YieldToken> {
override fun convert(value: TokenDTO): Token {
return Token(
override fun convert(value: TokenDTO): YieldToken {
return YieldToken(
name = value.name,
network = StakingNetworkTypeConverter.convert(value.network),
symbol = value.symbol,
@ -19,7 +19,7 @@ object TokenConverter : TwoWayConverter<TokenDTO, Token> {
)
}
override fun convertBack(value: Token): TokenDTO {
override fun convertBack(value: YieldToken): TokenDTO {
return TokenDTO(
name = value.name,
network = StakingNetworkTypeConverter.convertBack(value.network),

View file

@ -5,6 +5,10 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
@Deprecated(
message = "Use UserWalletsListRepository instead",
replaceWith = ReplaceWith("UserWalletsListRepository"),
)
interface UserWalletsStore {
val selectedUserWalletOrNull: UserWallet?
@ -15,8 +19,6 @@ interface UserWalletsStore {
fun getSyncStrict(key: UserWalletId): UserWallet
suspend fun getAllSyncOrNull(): List<UserWallet>?
suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,

View file

@ -1,21 +1,9 @@
package com.tangem.datasource.utils
import android.content.Context
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.tangem.datasource.BuildConfig
import com.tangem.datasource.api.common.SwitchEnvironmentInterceptor
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.api.utils.ConnectTimeout
import com.tangem.datasource.api.utils.ReadTimeout
import com.tangem.datasource.api.utils.WriteTimeout
import com.tangem.utils.ProviderSuspend
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Invocation
/** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */
internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeader): OkHttpClient.Builder {
@ -24,30 +12,6 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
)
}
/**
* Apply timeout annotations [Interceptor].
* Add this [Interceptor] to [OkHttpClient] if use timeout annotations for retrofit requests.
*/
internal fun OkHttpClient.Builder.applyTimeoutAnnotations(): OkHttpClient.Builder {
return addInterceptor(
Interceptor { chain ->
val request = chain.request()
val tag = request.tag(Invocation::class.java)
val connectionTimeout = tag?.method()?.getAnnotation(ConnectTimeout::class.java)
val readTimeout = tag?.method()?.getAnnotation(ReadTimeout::class.java)
val writeTimeout = tag?.method()?.getAnnotation(WriteTimeout::class.java)
chain.run {
connectionTimeout?.let { withConnectTimeout(timeout = it.duration, unit = it.unit) } ?: this
}.run {
readTimeout?.let { withReadTimeout(timeout = it.duration, unit = it.unit) } ?: this
}.run {
writeTimeout?.let { withWriteTimeout(timeout = it.duration, unit = it.unit) } ?: this
}.proceed(request)
},
)
}
/** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */
internal fun OkHttpClient.Builder.addHeaders(
requestHeaders: Map<String, ProviderSuspend<String>>,
@ -66,44 +30,4 @@ internal fun OkHttpClient.Builder.addHeaders(
chain.proceed(request)
},
)
}
/**
* Extension for logging each [OkHttpClient] request
*
* @param context context
*/
internal fun OkHttpClient.Builder.addLoggers(context: Context? = null): OkHttpClient.Builder {
return if (BuildConfig.LOG_ENABLED) {
context?.let {
addInterceptor(interceptor = ChuckerInterceptor(it))
}
addInterceptor(interceptor = createNetworkLoggingInterceptor())
} else {
this
}
}
/**
* Apply api config
*
* @param id class of [ApiConfig]
* @param apiConfigsManager api configs manager
*/
internal fun OkHttpClient.Builder.applyApiConfig(
id: ApiConfig.ID,
apiConfigsManager: ApiConfigsManager,
): OkHttpClient.Builder {
return if (BuildConfig.TESTER_MENU_ENABLED || BuildConfig.BUILD_TYPE == MOCKED_BUILD_TYPE) {
addInterceptor(
interceptor = SwitchEnvironmentInterceptor(
id = id,
apiConfigsManager = apiConfigsManager,
),
)
} else {
val headers = apiConfigsManager.getEnvironmentConfig(id).headers
this.addHeaders(headers)
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.utils.ProviderSuspend
import com.tangem.utils.info.AppInfoProvider
import com.tangem.utils.version.AppVersionProvider
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
@ -55,8 +56,8 @@ internal class ProdApiConfigsManagerTest {
every { appVersionProvider.versionName } returns VERSION_NAME
every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID
every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY
every { appAuthProvider.getCardId() } returns APP_CARD_ID
every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
coEvery { appAuthProvider.getCardId() } returns APP_CARD_ID
coEvery { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY
every { appInfoProvider.osVersion } returns "Android 16"
}

View file

@ -478,6 +478,8 @@
<string name="hw_backup_close_description">実行すると、最初からやり直す必要があります。</string>
<string name="hw_backup_google_drive_description">Googleドライブのバックアップに保存されている既存のウォレットを復元する</string>
<string name="hw_backup_google_drive_title">Googleドライブのバックアップ</string>
<string name="hw_backup_hardware_description">Tangemの業界最高水準のハードウェアウォレットで、今すぐセキュリティをアップグレードしましょう。</string>
<string name="hw_backup_hardware_title">ハードウェアウォレット</string>
<string name="hw_backup_need_action">バックアップへ移動</string>
<string name="hw_backup_need_description">アクセスコードを使用してウォレットを保護するには、まずバックアップを完了してください。</string>
<string name="hw_backup_need_title">まずバックアップを完了する</string>
@ -491,6 +493,19 @@
<string name="hw_create_title">モバイルウォレットを作成する</string>
<string name="hw_import_seed_phrase_already_imported">このリカバリーフレーズはすでにインポートされています。</string>
<string name="hw_mobile_wallet">モバイルウォレット</string>
<string name="hw_upgrade_funds_access_description">手続き中も資金は安全に保管され、完全にアクセス可能です</string>
<string name="hw_upgrade_funds_access_title">資金へのアクセス</string>
<string name="hw_upgrade_general_security_description">すべてのプライベートウォレットデータはモバイルアプリから削除され、Tangemデバイスにのみ安全に保存されます。</string>
<string name="hw_upgrade_general_security_title">セキュリティ全般</string>
<string name="hw_upgrade_key_migration_description">秘密鍵は、アプリからTangemカード・リングに移動します</string>
<string name="hw_upgrade_key_migration_title">鍵の移行</string>
<string name="hw_upgrade_scan_device">デバイスをスキャン</string>
<string name="hw_upgrade_start_action">アップグレードを開始</string>
<string name="hw_upgrade_start_description">ウォレットをTangemウォレットにアップグレードします。これにより、コールドストレージで資産を安全に保管できます。</string>
<string name="hw_upgrade_start_title">Tangemウォレット</string>
<string name="hw_upgrade_title">ハードウェアウォレットにアップグレード</string>
<string name="hw_upgrade_to_cold_banner_description">Tangemの業界最高水準のハードウェアウォレットで、暗号資産を安全に保管しましょう。</string>
<string name="hw_upgrade_to_cold_banner_title">ハードウェアバックアップでウォレットをアップグレード</string>
<string name="information_generated_with_ai">この情報はAIで生成されました。 \nエラーが見つかった場合は、ここをタップしてください。</string>
<string name="initial_message_change_access_code_body">アクセスコードを変更するには、上図のようにカードまたはリングをタップし、操作が終了するまで取り外さないでください。</string>
<string name="initial_message_change_passcode_body">パスコードを変更するには、上記のようにカードをタップし、操作が終了するまで取り外さないでください。</string>

View file

@ -485,6 +485,8 @@
<string name="hw_backup_close_description">If you do, you\'ll need to start over.</string>
<string name="hw_backup_google_drive_description">Recover an existing wallet stored in your Google Drive backup</string>
<string name="hw_backup_google_drive_title">Google Drive Backup</string>
<string name="hw_backup_hardware_description">Upgrade your security right away with a best in class hardware wallet from Tangem.</string>
<string name="hw_backup_hardware_title">Hardware Wallet</string>
<string name="hw_backup_need_action">Go to backup</string>
<string name="hw_backup_need_description">To secure your wallet with a Access Code, complete the backup first.</string>
<string name="hw_backup_need_title">Finish Backup First</string>
@ -498,6 +500,19 @@
<string name="hw_create_title">Create Mobile Wallet</string>
<string name="hw_import_seed_phrase_already_imported">This recovery phrase has already been imported</string>
<string name="hw_mobile_wallet">Mobile Wallet</string>
<string name="hw_upgrade_funds_access_description">Your funds stay safe and fully accessible during the process</string>
<string name="hw_upgrade_funds_access_title">Funds access</string>
<string name="hw_upgrade_general_security_description">All private wallet data will be removed from the mobile app and stored securely on your Tangem device only</string>
<string name="hw_upgrade_general_security_title">General Security</string>
<string name="hw_upgrade_key_migration_description">Private keys will be moved from the app to your Tangem card or ring</string>
<string name="hw_upgrade_key_migration_title">Key Migration</string>
<string name="hw_upgrade_scan_device">Scan device</string>
<string name="hw_upgrade_start_action">Start upgrade</string>
<string name="hw_upgrade_start_description">Youre about to upgrade your wallet to Tangem Wallet. This will keep your assets safe with cold storage.</string>
<string name="hw_upgrade_start_title">Tangem Wallet</string>
<string name="hw_upgrade_title">Upgrade to Hardware Wallet</string>
<string name="hw_upgrade_to_cold_banner_description">Keep your crypto safe with Tangems best-in-class hardware wallet.</string>
<string name="hw_upgrade_to_cold_banner_title">Upgrade wallet with a hardwarebackup</string>
<string name="information_generated_with_ai">This information was generated with AI.\nTap here, if you find any errors.</string>
<string name="initial_message_change_access_code_body">To change the access code tap the card or ring as shown above and do not remove until the end of the operation</string>
<string name="initial_message_change_passcode_body">To change the passcode tap the card as shown above and do not remove until the end of the operation</string>

View file

@ -42,7 +42,6 @@ dependencies {
/** Compose */
implementation(deps.compose.constraintLayout)
implementation(deps.compose.foundation)
implementation(deps.compose.material)
implementation(deps.compose.material3)
implementation(deps.compose.paging)
implementation(deps.compose.ui.tooling)

View file

@ -32,9 +32,9 @@ fun AppBarWithBackButton(
TangemTopAppBar(
modifier = modifier,
title = text,
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = iconRes ?: R.drawable.ic_back_24,
onIconClicked = onBackClick,
onClicked = onBackClick,
),
containerColor = containerColor,
)

View file

@ -27,14 +27,14 @@ fun AppBarWithBackButtonAndIcon(
title = text,
subtitle = subtitle,
containerColor = backgroundColor,
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = backIconRes ?: R.drawable.ic_back_24,
onIconClicked = onBackClick,
onClicked = onBackClick,
),
endButton = if (iconRes != null && onIconClick != null) {
TopAppBarButtonUM(
TopAppBarButtonUM.Icon(
iconRes = iconRes,
onIconClicked = onIconClick,
onClicked = onIconClick,
)
} else {
null

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -20,6 +21,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TopAppBarTestTags
/**
* [TangemTopAppBar] height options.
@ -127,6 +129,7 @@ fun TangemTopAppBar(
TopAppBarButton(
button = endButton,
tint = iconTint,
modifier = Modifier.testTag(TopAppBarTestTags.MORE_BUTTON),
)
}
},
@ -172,6 +175,7 @@ fun TangemTopAppBar(
TopAppBarButton(
button = startButton,
tint = iconTint,
modifier = Modifier.testTag(TopAppBarTestTags.CLOSE_BUTTON),
)
}
}
@ -222,6 +226,7 @@ private fun TopAppBarTitle(
color = textColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.testTag(TopAppBarTestTags.TITLE),
)
AnimatedVisibility(
@ -296,25 +301,25 @@ private class BasicTopAppBarPMPreviewProvider : PreviewParameterProvider<BasicTo
height = TangemTopAppBarHeight.BOTTOM_SHEET,
),
BasicTopAppBarPM(
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_scan_24,
onIconClicked = {},
onClicked = {},
),
endButton = TopAppBarButtonUM(
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_more_vertical_24,
onIconClicked = {},
onClicked = {},
),
),
BasicTopAppBarPM(
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_scan_24,
onIconClicked = {},
onClicked = {},
),
),
BasicTopAppBarPM(
endButton = TopAppBarButtonUM(
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_more_vertical_24,
onIconClicked = {},
onClicked = {},
),
height = TangemTopAppBarHeight.BOTTOM_SHEET,
),
@ -322,13 +327,13 @@ private class BasicTopAppBarPMPreviewProvider : PreviewParameterProvider<BasicTo
title = "1234567891011121314151617181920",
subtitle = "12345678910111213141516171819202122232425",
titleAlignment = Alignment.Start,
startButton = TopAppBarButtonUM(
startButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_scan_24,
onIconClicked = {},
onClicked = {},
),
endButton = TopAppBarButtonUM(
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_more_vertical_24,
onIconClicked = {},
onClicked = {},
),
),
)

View file

@ -1,27 +1,49 @@
package com.tangem.core.ui.components.appbar
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
fun TopAppBarButton(button: TopAppBarButtonUM, tint: Color, modifier: Modifier = Modifier) {
IconButton(
enabled = button.enabled,
modifier = modifier.size(TangemTheme.dimens.size32),
onClick = button.onIconClicked,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = button.iconRes),
tint = tint,
contentDescription = null,
)
when (button) {
is TopAppBarButtonUM.Icon -> {
IconButton(
enabled = button.enabled,
modifier = modifier.size(TangemTheme.dimens.size32),
onClick = button.onClicked,
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size24),
painter = painterResource(id = button.iconRes),
tint = tint,
contentDescription = null,
)
}
}
is TopAppBarButtonUM.Text -> {
Text(
modifier = modifier
.conditional(button.enabled) {
clickable { button.onClicked() }
}
.padding(4.dp),
text = button.text.resolveReference(),
color = tint,
style = TangemTheme.typography.body1,
)
}
}
}

View file

@ -2,21 +2,39 @@ package com.tangem.core.ui.components.appbar.models
import androidx.annotation.DrawableRes
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
data class TopAppBarButtonUM(
@DrawableRes val iconRes: Int,
val onIconClicked: () -> Unit,
val enabled: Boolean = true,
sealed class TopAppBarButtonUM(
open val onClicked: () -> Unit,
open val enabled: Boolean = true,
) {
data class Icon(
@DrawableRes val iconRes: Int,
override val onClicked: () -> Unit,
override val enabled: Boolean = true,
) : TopAppBarButtonUM(onClicked, enabled)
data class Text(
val text: TextReference,
override val onClicked: () -> Unit,
override val enabled: Boolean = true,
) : TopAppBarButtonUM(onClicked, enabled)
@Suppress("FunctionName")
companion object {
fun Back(onBackClicked: () -> Unit) = Back(true, onBackClicked)
fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = TopAppBarButtonUM(
fun Back(enabled: Boolean = true, onBackClicked: () -> Unit) = Icon(
iconRes = R.drawable.ic_back_24,
onIconClicked = onBackClicked,
onClicked = onBackClicked,
enabled = enabled,
)
fun Text(text: TextReference, onTextClicked: () -> Unit, enabled: Boolean = true) = Text(
text = text,
onClicked = onTextClicked,
enabled = enabled,
)
}

View file

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -38,6 +39,7 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) {
)
Text(
modifier = Modifier.weight(1f),
text = model.text.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = when (model.accentType) {
@ -48,6 +50,8 @@ fun BlockItem(model: BlockUM, modifier: Modifier = Modifier) {
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
model.label?.let { Label(it) }
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.core.ui.components.block.model
import androidx.annotation.DrawableRes
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
data class BlockUM(
@ -8,6 +9,7 @@ data class BlockUM(
@DrawableRes val iconRes: Int,
val onClick: () -> Unit,
val accentType: AccentType = AccentType.NONE,
val label: LabelUM? = null,
) {
enum class AccentType {

View file

@ -179,6 +179,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
@ -190,6 +191,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
}
@ -200,58 +202,62 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
@Composable
private fun TangemModalBottomSheet_Preview() {
TangemThemePreview {
TangemModalBottomSheet<TangemBottomSheetConfigContentPreviewConfig>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContentPreviewConfig(),
),
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = {},
)
},
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
Box(
Modifier.background(TangemTheme.colors.background.tertiary),
) {
TangemModalBottomSheet<TangemBottomSheetConfigContentPreviewConfig>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = TangemBottomSheetConfigContentPreviewConfig(),
),
title = {
TangemModalBottomSheetTitle(
endIconRes = R.drawable.ic_close_24,
onEndClick = {},
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Go it",
onClick = {},
)
}
},
)
},
content = {
Column(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.size(56.dp)
.clip(RoundedCornerShape(100))
.background(TangemTheme.colors.icon.informative.copy(alpha = 0.1f))
.padding(12.dp),
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_alert_24),
),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
SpacerH24()
Text(
text = "Unsuported networks",
style = TangemTheme.typography.h3,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = "Tangem does not currently support a required network by React App.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH(48.dp)
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = "Go it",
onClick = {},
)
}
},
)
}
}
}

View file

@ -32,6 +32,7 @@ import com.tangem.core.ui.res.LocalBottomSheetAlwaysVisible
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.toPx
/**
* Modal bottom sheet with [content], [footer] and optional [title].
@ -154,6 +155,21 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
val initial = 0
val scrollState = rememberScrollState(initial = initial)
val isKeyboardOpen by rememberIsKeyboardVisible()
val buttonHeight = TangemTheme.dimens.spacing80
val contentBottomPadding = TangemTheme.dimens.spacing80
// Offset calculation for keyboard scroll adjustment:
// 1) Button height (footer)
// 2) Column content bottom padding
// 3) Additional spacing (40dp) for visual comfort when keyboard is open
val scrollOffset = buttonHeight.toPx() + buttonHeight.toPx() + 40.dp.toPx()
LaunchedEffect(isKeyboardOpen) {
if (isKeyboardOpen) {
scrollState.animateScrollTo(scrollState.value + scrollOffset.toInt())
}
}
Column(
modifier = Modifier
.systemBarsPadding()
@ -186,7 +202,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(bottom = TangemTheme.dimens.spacing80),
.padding(bottom = contentBottomPadding),
) {
content(model)
}
@ -199,7 +215,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
Box(
modifier = Modifier
.fillMaxWidth()
.height(80.dp)
.height(buttonHeight)
.align(Alignment.BottomCenter),
) {
footer(model)
@ -219,6 +235,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
@ -230,6 +247,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
}

View file

@ -192,6 +192,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
onBack = onBack,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
@ -203,6 +204,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
}

View file

@ -57,9 +57,9 @@ private fun Preview_TangemBottomSheetTitle() {
TangemThemePreview {
TangemBottomSheetTitle(
title = "Title",
endButton = TopAppBarButtonUM(
endButton = TopAppBarButtonUM.Icon(
iconRes = R.drawable.ic_information_24,
onIconClicked = {},
onClicked = {},
),
containerColor = TangemTheme.colors.background.secondary,
)

View file

@ -11,6 +11,7 @@ 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.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -21,6 +22,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -32,7 +34,9 @@ fun HorizontalActionChips(
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens.spacing0),
) {
LazyRow(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.testTag(TokenDetailsScreenTestTags.HORIZONTAL_ACTION_CHIPS),
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
contentPadding = contentPadding,

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -33,6 +34,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
/**
* Rounded action button
@ -98,7 +100,7 @@ fun ActionButton(
),
)
},
modifier = modifier,
modifier = modifier.testTag(TokenDetailsScreenTestTags.ACTION_BUTTON),
color = color,
containerColor = containerColor,
)

View file

@ -25,10 +25,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.DialogTestTags
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.utils.MultipleClickPreventer
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LongMethod")
@Composable
fun TangemButton(
text: String,
@ -51,7 +51,7 @@ fun TangemButton(
Button(
modifier = modifier
.heightIn(min = size.toHeightDp())
.testTag(DialogTestTags.BUTTON),
.testTag(BaseButtonTestTags.BUTTON),
onClick = {
multipleClickPreventer.processEvent { if (!showProgress) onClick() }
},
@ -78,7 +78,8 @@ fun TangemButton(
ResizableText(
modifier = Modifier
.weight(1f, fill = false)
.heightIn(MinButtonContentSize, maxContentSize),
.heightIn(MinButtonContentSize, maxContentSize)
.testTag(BaseButtonTestTags.TEXT),
text = text,
style = textStyle,
color = colors.contentColor(enabled = enabled).value,
@ -92,7 +93,8 @@ fun TangemButton(
Icon(
modifier = Modifier
.buttonContentSize(maxContentSize)
.padding(vertical = 2.dp),
.padding(vertical = 2.dp)
.testTag(BaseButtonTestTags.ICON),
painter = painterResource(id = iconResId),
tint = colors.contentColor(enabled = enabled).value,
contentDescription = null,

View file

@ -3,10 +3,11 @@ package com.tangem.core.ui.components.buttons.small
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -41,20 +42,19 @@ fun TangemIconButton(
background: Color = TangemTheme.colors.button.secondary,
iconTint: Color = TangemTheme.colors.icon.secondary,
) {
IconButton(
onClick = onClick,
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)),
contentDescription = "",
tint = iconTint,
modifier = modifier
.size(24.dp)
.clip(shape)
.background(background)
.size(24.dp),
) {
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(iconRes)),
contentDescription = "",
tint = iconTint,
modifier = Modifier.size(16.dp),
)
}
.padding(4.dp)
.clickable(
onClick = onClick,
),
)
}
// region Preview

View file

@ -5,7 +5,7 @@ import com.tangem.core.ui.extensions.getTintForTokenIcon
import com.tangem.core.ui.extensions.networkIconResId
import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.converter.Converter
/**

View file

@ -12,12 +12,8 @@ import androidx.compose.ui.Alignment.Companion.TopCenter
import androidx.compose.ui.Alignment.Companion.TopStart
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
@ -28,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingSendScreenTestTags
import com.tangem.core.ui.utils.*
import java.math.BigDecimal
import java.text.DecimalFormat
@ -77,24 +74,10 @@ fun AmountTextField(
) {
val decimalFormat = rememberDecimalFormat()
BoxWithConstraints(modifier = modifier) {
var fontSize = textStyle.fontSize
if (isAutoResize) {
val calculateIntrinsics = @Composable {
val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text
ParagraphIntrinsics(
text = transformedText,
style = textStyle.copy(fontSize = fontSize),
density = LocalDensity.current,
fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
)
}
var intrinsics = calculateIntrinsics()
with(LocalDensity.current) {
while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
fontSize *= reduceFactor
intrinsics = calculateIntrinsics()
}
}
val fontSize = if (isAutoResize) {
resizeFont(visualTransformation, value, textStyle, reduceFactor)
} else {
textStyle.fontSize
}
val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color
SimpleTextField(
@ -121,7 +104,9 @@ fun AmountTextField(
singleLine = true,
readOnly = !isEnabled,
visualTransformation = visualTransformation,
modifier = Modifier.background(backgroundColor),
modifier = Modifier
.background(backgroundColor)
.testTag(StakingSendScreenTestTags.INPUT_TEXT_FIELD),
)
}
}

View file

@ -0,0 +1,204 @@
package com.tangem.core.ui.components.fields
import android.annotation.SuppressLint
import androidx.annotation.FloatRange
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ParagraphIntrinsics
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.TextUnit
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Simple text field for auto size input.
* Can display aligned placeholder.
*
* @param value initial text
* @param onValueChange callback
* @param isAutoResize is text font auto resize
* @param reduceFactor font resize factor
* @param textStyle text and placeholder styles
* @param textFieldModifier modifier for [SimpleTextField]
* @param boxModifier modifier for [BoxWithConstraints]
* @see [SimpleTextField] for other text field params
*/
@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable
fun AutoSizeTextField(
value: String,
onValueChange: (String) -> Unit,
// region AutoSize
isAutoResize: Boolean = true,
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
reduceFactor: Double = 0.9,
// region TextField
textFieldModifier: Modifier = Modifier,
boxModifier: Modifier = Modifier,
placeholder: TextReference? = null,
singleLine: Boolean = isAutoResize,
centered: Boolean = false,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
color: Color = TangemTheme.colors.text.primary1,
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
placeholderColor: Color = TangemTheme.colors.text.disabled,
readOnly: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> Unit = {},
decorationBox: (@Composable (innerTextField: @Composable () -> Unit) -> Unit)? = null,
) {
BoxWithConstraints(modifier = boxModifier) {
val fontSize = if (isAutoResize) {
resizeFont(visualTransformation, value, textStyle, reduceFactor)
} else {
textStyle.fontSize
}
val textColor = if (value.isBlank()) TangemTheme.colors.text.disabled else color
SimpleTextField(
value = value,
onValueChange = onValueChange,
textStyle = textStyle.copy(
fontSize = fontSize,
textDirection = TextDirection.ContentOrLtr,
),
isValuePasted = isValuePasted,
onValuePastedTriggerDismiss = onValuePastedTriggerDismiss,
color = textColor,
keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions,
placeholder = placeholder,
placeholderColor = placeholderColor,
singleLine = singleLine,
interactionSource = interactionSource,
readOnly = readOnly,
centered = centered,
visualTransformation = visualTransformation,
decorationBox = decorationBox,
modifier = textFieldModifier,
)
}
}
@Composable
internal fun BoxWithConstraintsScope.resizeFont(
visualTransformation: VisualTransformation,
value: String,
textStyle: TextStyle,
reduceFactor: Double,
): TextUnit {
var result = textStyle.fontSize
val calculateIntrinsics = @Composable {
val transformedText = visualTransformation.filter(AnnotatedString(value)).text.text
ParagraphIntrinsics(
text = transformedText,
style = textStyle.copy(fontSize = result),
density = LocalDensity.current,
fontFamilyResolver = createFontFamilyResolver(LocalContext.current),
)
}
var intrinsics = calculateIntrinsics()
with(LocalDensity.current) {
while (intrinsics.maxIntrinsicWidth > maxWidth.toPx()) {
result *= reduceFactor
intrinsics = calculateIntrinsics()
}
}
return result
}
// region preview
@Preview(widthDp = 360, showBackground = true)
@Composable
private fun AmountTextFieldPreview(
@PreviewParameter(AutoSizeTextFieldPreviewProvider::class) data: AutoSizeTextFieldPreviewData,
) {
var text by remember { mutableStateOf(data.value) }
TangemThemePreview {
AutoSizeTextField(
textFieldModifier = Modifier.fillMaxWidth(),
value = text,
onValueChange = { text = it },
centered = data.centered,
isAutoResize = data.isAutoResize,
placeholder = data.placeholder,
)
}
}
private class AutoSizeTextFieldPreviewProvider : PreviewParameterProvider<AutoSizeTextFieldPreviewData> {
override val values = sequenceOf(
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextField",
placeholder = stringReference("placeholder"),
isAutoResize = true,
centered = false,
),
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField",
placeholder = stringReference("placeholder"),
isAutoResize = true,
centered = false,
),
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField",
placeholder = stringReference("Placeholder"),
isAutoResize = true,
centered = false,
),
AutoSizeTextFieldPreviewData(
value = "",
placeholder = stringReference("Placeholder"),
isAutoResize = true,
centered = false,
),
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextField",
placeholder = stringReference("Placeholder"),
isAutoResize = false,
centered = true,
),
AutoSizeTextFieldPreviewData(
value = "AutoSizeTextFieldAutoSizeTextFieldAutoSizeTextFieldAutoSizeTextField",
placeholder = stringReference("Placeholder"),
isAutoResize = false,
centered = true,
),
AutoSizeTextFieldPreviewData(
value = "",
placeholder = stringReference("Placeholder"),
isAutoResize = false,
centered = true,
),
)
}
private data class AutoSizeTextFieldPreviewData(
val value: String,
val placeholder: TextReference,
val isAutoResize: Boolean,
val centered: Boolean,
)
// endregion

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.components.fields
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -36,9 +37,9 @@ fun PinTextField(
value: String,
length: Int,
isPasswordVisual: Boolean,
pinTextColor: PinTextColor,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
wrongCode: Boolean = false,
) {
val focusRequester = remember { FocusRequester() }
val textFieldValue = remember(value) {
@ -72,7 +73,7 @@ fun PinTextField(
CellDecoration(
length = length,
isPasswordVisual = isPasswordVisual,
wrongCode = wrongCode,
pinTextColor = pinTextColor,
value = value,
)
},
@ -84,17 +85,25 @@ fun PinTextField(
}
}
@Suppress("MagicNumber")
enum class PinTextColor {
Primary,
WrongCode,
Success,
}
@Suppress("MagicNumber", "LongMethod")
@Composable
private fun CellDecoration(
length: Int,
wrongCode: Boolean,
pinTextColor: PinTextColor,
value: String,
modifier: Modifier = Modifier,
isPasswordVisual: Boolean = false,
) {
val textMeasurer = rememberTextMeasurer()
val width = textMeasurer.measure("0")
val minSize = textMeasurer.measure("0")
val minWidth = maxOf(minSize.size.width.dp + 8.dp, 24.dp + 3.dp) // 24.dp is the minimum width of a pin cell
val minHeight = maxOf(minSize.size.height.dp, 48.dp) // 48.dp is the minimum height of a pin cell
Row(
modifier = modifier,
@ -107,6 +116,18 @@ private fun CellDecoration(
""
}
val color = when (pinTextColor) {
PinTextColor.Primary -> {
if (isPasswordVisual) {
TangemTheme.colors.icon.informative
} else {
TangemTheme.colors.text.primary1
}
}
PinTextColor.WrongCode -> TangemTheme.colors.icon.warning
PinTextColor.Success -> TangemTheme.colors.icon.accent
}
Box(
modifier = Modifier
.background(
@ -119,26 +140,34 @@ private fun CellDecoration(
targetState = char,
transitionSpec = {
(
fadeIn(animationSpec = tween(220, delayMillis = 90)) +
slideInVertically(animationSpec = tween(330, delayMillis = 0))
fadeIn(animationSpec = tween(90, delayMillis = 90)) +
slideInVertically(animationSpec = tween(220, delayMillis = 0))
)
.togetherWith(
fadeOut(animationSpec = tween(90)) + slideOutVertically(tween(220)),
)
},
) { text ->
Text(
modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp),
text = text,
style = TangemTheme.typography.h3,
color = if (wrongCode) {
TangemTheme.colors.text.warning
} else {
TangemTheme.colors.text.primary1
},
textAlign = TextAlign.Center,
lineHeight = 48.sp,
)
if (isPasswordVisual && text.isNotEmpty()) {
Canvas(
Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight),
) {
drawCircle(
color = color,
radius = 4.dp.toPx(),
center = center,
)
}
} else {
Text(
modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight),
text = text,
style = TangemTheme.typography.h3,
color = color,
textAlign = TextAlign.Center,
lineHeight = 48.sp,
)
}
}
}
}
@ -152,10 +181,18 @@ private fun Preview() {
var text by remember { mutableStateOf("123") }
Column {
PinTextField(
value = text,
onValueChange = { text = it },
isPasswordVisual = true,
pinTextColor = PinTextColor.Success,
length = 6,
)
PinTextField(
value = text,
onValueChange = { text = it },
isPasswordVisual = false,
pinTextColor = PinTextColor.Primary,
length = 6,
)

View file

@ -11,16 +11,20 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
@ -35,6 +39,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags
@Composable
fun SearchBar(
@ -45,6 +50,7 @@ fun SearchBar(
) {
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() }
val interactionSource = remember { MutableInteractionSource() }
BasicTextField(
@ -57,7 +63,9 @@ fun SearchBar(
} else {
state.onActiveChange(false)
}
},
}
.focusRequester(focusRequester)
.testTag(SelectCountryBottomSheetTestTags.SEARCH_BAR),
enabled = enabled,
value = state.query,
onValueChange = state.onQueryChange,
@ -89,6 +97,10 @@ fun SearchBar(
)
},
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
@Suppress("LongParameterList")

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -17,6 +18,7 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -39,6 +41,7 @@ fun SimpleTextField(
textStyle: TextStyle = TangemTheme.typography.body2.copy(color = color),
placeholderColor: Color = TangemTheme.colors.text.disabled,
readOnly: Boolean = false,
centered: Boolean = false,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
isValuePasted: Boolean = false,
onValuePastedTriggerDismiss: () -> Unit = {},
@ -80,6 +83,8 @@ fun SimpleTextField(
onValuePastedTriggerDismiss()
}
}
var textStyle = textStyle.copy(color = color)
if (centered) textStyle = textStyle.copy(textAlign = TextAlign.Center)
BasicTextField(
value = textFieldValue,
@ -91,7 +96,7 @@ fun SimpleTextField(
if (stringChangedSinceLastInvocation) onValueChange(newTextFieldValueState.text)
},
textStyle = textStyle.copy(color = color),
textStyle = textStyle,
cursorBrush = SolidColor(TangemTheme.colors.text.primary1),
singleLine = singleLine,
readOnly = readOnly,
@ -105,6 +110,7 @@ fun SimpleTextField(
value = value,
textStyle = textStyle,
textValue = textValue,
centered = centered,
color = placeholderColor,
)
},
@ -118,10 +124,11 @@ private fun SimpleTextPlaceholder(
placeholder: TextReference?,
value: String,
textStyle: TextStyle,
centered: Boolean,
textValue: @Composable () -> Unit,
color: Color = TangemTheme.colors.text.disabled,
) {
Box {
Box(contentAlignment = if (centered) Alignment.Center else Alignment.TopStart) {
if (value.isBlank() && placeholder != null) {
AnimatedContent(
targetState = placeholder,

View file

@ -16,6 +16,7 @@ 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.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -26,6 +27,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.BaseBlockTestTags
/**
* [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4)
@ -64,20 +66,24 @@ fun InputRowDefault(
) {
Column(
modifier = Modifier
.weight(1f),
.weight(1f)
.testTag(BaseBlockTestTags.BLOCK),
) {
title?.let {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = titleColor,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing8),
modifier = Modifier
.padding(bottom = TangemTheme.dimens.spacing8)
.testTag(BaseBlockTestTags.BLOCK_TITLE),
)
}
Text(
text = text.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier.testTag(BaseBlockTestTags.BLOCK_TEXT),
)
}
iconRes?.let {

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment.Companion.CenterEnd
@ -19,6 +20,7 @@ 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 androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.buttons.small.TangemIconButton
import com.tangem.core.ui.components.fields.SimpleTextField
@ -69,11 +71,12 @@ fun InputRowRecipient(
showDivider: Boolean = false,
isLoading: Boolean = false,
isValuePasted: Boolean = false,
resolvedAddress: String? = null,
) {
val (titleText, color) = if (isError && error != null) {
error to TangemTheme.colors.text.warning
} else {
title to TangemTheme.colors.text.secondary
title to TangemTheme.colors.text.tertiary
}
DividerContainer(
modifier = modifier,
@ -144,11 +147,15 @@ fun InputRowRecipient(
} else {
TangemTheme.colors.text.primary2
},
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8),
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
)
}
}
ResolvedAddressRow(
isLoading = isLoading,
resolvedAddress = resolvedAddress,
)
}
}
}
@ -203,6 +210,38 @@ private fun RowScope.InputIcon(isLoading: Boolean, value: String) {
}
}
@Composable
private fun ResolvedAddressRow(isLoading: Boolean, resolvedAddress: String?) {
AnimatedContent(
targetState = if (resolvedAddress.isNullOrBlank() || isLoading) {
ResolvedState.Hide
} else {
ResolvedState.Show(resolvedAddress)
},
label = "Resolved Address",
) { state ->
if (state is ResolvedState.Show) {
Column {
HorizontalDivider(
thickness = 0.5.dp,
modifier = Modifier.padding(top = 12.dp, bottom = 12.dp),
color = TangemTheme.colors.stroke.primary,
)
Text(
text = state.address,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
private sealed interface ResolvedState {
data object Hide : ResolvedState
data class Show(val address: String) : ResolvedState
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -224,6 +263,7 @@ private fun InputRowRecipientPreview(
onQrCodeClick = {},
modifier = Modifier.background(TangemTheme.colors.background.primary),
isRedesignEnabled = false,
resolvedAddress = value.resolvedAddress,
)
}
}
@ -232,6 +272,7 @@ private data class InputRowRecipientPreviewData(
val value: String,
val isError: Boolean,
val isLoading: Boolean = false,
val resolvedAddress: String? = null,
)
private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider<InputRowRecipientPreviewData> {
@ -250,6 +291,12 @@ private class InputRowRecipientPreviewDataProvider : PreviewParameterProvider<In
isLoading = true,
isError = true,
),
InputRowRecipientPreviewData(
value = "vitalik.eth",
isLoading = false,
isError = true,
resolvedAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470",
),
)
}
//endregion

View file

@ -0,0 +1,104 @@
package com.tangem.core.ui.components.label
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Label component
*
* @param state component state
* @param modifier composable modifier
*
* @see <a href="https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=4480-1459&t=2QTpi1G7FeTexTFS-4">Figma</a>
*/
@Composable
fun Label(state: LabelUM, modifier: Modifier = Modifier) {
val backgroundColor by animateColorAsState(
targetValue = when (state.style) {
LabelStyle.ACCENT -> TangemTheme.colors.text.accent.copy(alpha = 0.1f)
LabelStyle.REGULAR -> TangemTheme.colors.control.unchecked
LabelStyle.WARNING -> TangemTheme.colors.text.warning.copy(alpha = 0.1f)
},
)
val textColor by animateColorAsState(
targetValue = when (state.style) {
LabelStyle.ACCENT -> TangemTheme.colors.text.accent
LabelStyle.REGULAR -> TangemTheme.colors.text.secondary
LabelStyle.WARNING -> TangemTheme.colors.text.warning
},
)
AnimatedContent(targetState = state.text) { text ->
Box(
modifier = modifier
.padding(horizontal = 4.dp)
.background(
color = backgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = text.resolveReference(),
style = TangemTheme.typography.caption1,
color = textColor,
)
}
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun LabelPreview() {
TangemThemePreview {
Column(
modifier = Modifier.padding(16.dp),
) {
Label(
state = LabelUM(
text = TextReference.Str("Regular Label"),
style = LabelStyle.REGULAR,
),
)
Spacer(modifier = Modifier.height(8.dp))
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
),
)
Spacer(modifier = Modifier.height(8.dp))
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
),
)
}
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.ui.components.label.entity
import com.tangem.core.ui.extensions.TextReference
data class LabelUM(
val text: TextReference,
val style: LabelStyle,
)
enum class LabelStyle {
REGULAR, ACCENT, WARNING,
}

View file

@ -20,7 +20,7 @@ import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.utils.StringsSigns.DASH_SIGN
/**
* Market price block
@ -120,7 +120,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) {
)
}
} else {
Price(price = BigDecimalFormatter.EMPTY_BALANCE_SIGN, modifier = priceModifier)
Price(price = DASH_SIGN, modifier = priceModifier)
}
}
}

View file

@ -20,6 +20,7 @@ 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.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
@ -35,6 +36,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.NotificationTestTags
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState as NotificationButtonsState
/**
@ -206,6 +208,7 @@ internal fun TextsBlock(
text = titleText,
color = titleColor,
style = TangemTheme.typography.button,
modifier = Modifier.testTag(NotificationTestTags.TITLE),
)
SpacerH(height = TangemTheme.dimens.spacing2)
@ -217,6 +220,7 @@ internal fun TextsBlock(
text = subtitleText,
color = subtitleColor,
style = TangemTheme.typography.caption2,
modifier = Modifier.testTag(NotificationTestTags.TEXT),
)
}
}

View file

@ -23,6 +23,7 @@ import androidx.constraintlayout.compose.Visibility
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.badge.Badge
@ -50,8 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f
fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
ConstraintLayout(
modifier = modifier
.background(TangemTheme.colors.background.action)
.clip(RoundedCornerShape(14.dp))
.selectedBorder(isSelected = providerChooseUM.isSelected)
.clickable(
enabled = !providerChooseUM.hasError(),
@ -133,13 +132,23 @@ private fun IconContent(iconUrl: String, modifier: Modifier = Modifier) {
SubcomposeAsyncImage(
modifier = modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.background(TangemColorPalette.Light1),
.clip(RoundedCornerShape(8.dp)),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = {
RectangleShimmer(radius = 8.dp)
},
error = {
Box(
modifier = Modifier.background(
color = TangemColorPalette.Light1,
shape = RoundedCornerShape(8.dp),
),
)
},
contentDescription = null,
)
}

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
@ -23,6 +24,7 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StakingDetailsScreenTestTags
@Suppress("LongParameterList")
@Composable
@ -54,7 +56,8 @@ fun RoundableCornersRow(
.padding(
horizontal = TangemTheme.dimens.spacing16,
vertical = TangemTheme.dimens.spacing12,
),
)
.testTag(StakingDetailsScreenTestTags.PARAMETER_BLOCK),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
@ -63,6 +66,7 @@ fun RoundableCornersRow(
color = startTextColor,
maxLines = 1,
style = startTextStyle,
modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_NAME),
)
if (iconResId != null && iconClick != null) {
Icon(
@ -85,6 +89,7 @@ fun RoundableCornersRow(
color = endTextColor,
maxLines = 1,
style = endTextStyle,
modifier = Modifier.testTag(StakingDetailsScreenTestTags.PARAMETER_VALUE),
)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.core.ui.components.rows
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -13,6 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
@ -22,14 +22,15 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.utils.StringsSigns
@Composable
fun SelectorRowItem(
@StringRes titleRes: Int,
title: TextReference,
@DrawableRes iconRes: Int,
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(TangemTheme.dimens.spacing12),
@ -68,7 +69,8 @@ fun SelectorRowItem(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues),
.padding(paddingValues)
.testTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
@ -77,7 +79,7 @@ fun SelectorRowItem(
contentDescription = null,
)
Text(
text = stringResourceSafe(titleRes),
text = title.resolveReference(),
style = textStyle,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing8),
@ -148,7 +150,7 @@ private fun RowScope.SelectorValueContent(
private fun SelectorRowItemPreview() {
TangemThemePreview {
SelectorRowItem(
titleRes = R.string.common_fee_selector_option_slow,
title = resourceReference(R.string.common_fee_selector_option_slow),
iconRes = R.drawable.ic_tortoise_24,
preDot = TextReference.Str("1000 ETH"),
postDot = TextReference.Str("1000 $"),

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -28,6 +29,7 @@ import com.tangem.core.ui.components.stories.model.StoryConfig
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SwapStoriesScreenTestTags
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -101,7 +103,8 @@ inline fun <reified T : StoryConfig> StoriesContainer(
interactionSource = remember { MutableInteractionSource() },
indication = LocalIndication.current,
onClick = { config.onClose(watchedCounter) },
),
)
.testTag(SwapStoriesScreenTestTags.CLOSE_BUTTON),
)
}
}

View file

@ -94,6 +94,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
"pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22
else -> R.drawable.ic_alert_24
}
}
@ -186,6 +187,7 @@ fun getActiveIconResByCoinId(coinId: String): Int {
"zklink", "zklink/test" -> R.drawable.img_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.img_vanar_22
"pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22
else -> R.drawable.ic_alert_24
}
}
@ -281,6 +283,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int {
"zklink", "zklink/test" -> R.drawable.ic_zklink_22
"vanar-chain", "vanar-chain/test" -> R.drawable.ic_vanar_22
"pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22
"hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_22
else -> R.drawable.ic_alert_24
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavController
import timber.log.Timber
/**
* The ViewModel is scoped to the parent route Navigation graph
* and is provided using the Hilt-generated ViewModel factory
*
* ```
* val navController = rememberNavController()
*
* navigation(
* route = "parent",
* startDestination = "parent/1"
* ) {
* composable("route/1") { entry ->
* val viewModel = entry.parentHiltViewModel(navController)
* }
* composable("route/2") { entry ->
* val viewModel = entry.parentHiltViewModel(navController)
* }
* composable("route/3") { entry ->
* val viewModel = entry.parentHiltViewModel(navController)
* }
* }
* ```
*
* @param navController NavController within the common NavGraph
* @throws Exception if there is no parent route
*/
@Composable
inline fun <reified T : ViewModel> NavBackStackEntry.parentHiltViewModel(navController: NavController): T {
val viewModelStoreOwner = remember(this) {
try {
navController.getBackStackEntry(this.destination.parent!!.id)
} catch (e: Exception) {
Timber.tag("scopedViewModel").e(e, "There is no parent route'")
throw e
}
}
return hiltViewModel<T>(viewModelStoreOwner)
}

View file

@ -1,43 +0,0 @@
package com.tangem.core.ui.extensions
import android.R
import android.content.Context
import android.graphics.Color.*
import android.view.WindowManager
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import kotlin.math.sqrt
@Deprecated("Use only in legacy fragments")
fun Fragment.setStatusBarColor(@ColorRes colorResId: Int) {
with(requireActivity().window) {
clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
statusBarColor = ContextCompat.getColor(requireContext(), colorResId)
val view = view ?: return
val windowInsetsController = WindowCompat.getInsetsController(this, view)
windowInsetsController.isAppearanceLightStatusBars = luminance(requireContext(), colorResId)
}
}
// TODO replace by android.graphics.luminance() after bump min API to 24
@Suppress("MagicNumber")
fun luminance(context: Context, @ColorRes colorRes: Int): Boolean {
val color = context.resources.getColor(colorRes, null)
if (R.color.transparent == color) return true
var rtnValue = false
val rgb = intArrayOf(red(color), green(color), blue(color))
val brightness = sqrt(
rgb[0] * rgb[0] * .241 +
rgb[1] * rgb[1] * .691 +
rgb[2] * rgb[2] * .068,
).toInt()
// color is light
if (brightness >= 200) {
rtnValue = true
}
return rtnValue
}

View file

@ -4,7 +4,6 @@ import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -72,26 +71,24 @@ fun Modifier.conditionalCompose(
fun Modifier.selectedBorder(
isSelected: Boolean,
width: Dp = 2.5.dp,
color: Color = TangemTheme.colors.text.accent.copy(alpha = 0.1f),
color: Color = TangemTheme.colors.text.accent,
radius: Dp = 16.dp,
) = conditionalCompose(
condition = isSelected,
modifier = {
border(
outsetBorder(
width = width,
color = color,
shape = RoundedCornerShape(radius),
color = color.copy(alpha = 0.15f),
shape = RoundedCornerShape(radius + 2.dp),
)
.padding(width)
.border(
width = 1.dp,
color = TangemTheme.colors.text.accent,
shape = RoundedCornerShape(radius - 2.dp),
color = color,
shape = RoundedCornerShape(radius),
)
.clip(RoundedCornerShape(radius - 2.dp))
.clip(RoundedCornerShape(radius))
},
otherModifier = {
padding(width)
.clip(RoundedCornerShape(radius - 2.dp))
clip(RoundedCornerShape(radius))
},
)

View file

@ -121,7 +121,10 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value ->
private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD
private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
/**
* Returns amount with correct scale
*/
fun getFiatPriceAmountWithScale(value: BigDecimal): Pair<BigDecimal, Int> {
return if (value < BigDecimal.ONE) {
val leadingZeroes = value.scale() - value.precision()
val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES

View file

@ -16,6 +16,7 @@ class TangemColors internal constructor(
control: Control,
stroke: Stroke,
field: Field,
overlay: Overlay,
) {
var text by mutableStateOf(text)
private set
@ -31,6 +32,7 @@ class TangemColors internal constructor(
private set
var field by mutableStateOf(field)
private set
var overlay by mutableStateOf(overlay)
@Stable
class Text internal constructor(
@ -220,6 +222,22 @@ class TangemColors internal constructor(
}
}
@Stable
class Overlay internal constructor(
primary: Color,
secondary: Color,
) {
var primary by mutableStateOf(primary)
private set
var secondary by mutableStateOf(secondary)
private set
fun update(other: Overlay) {
primary = other.primary
secondary = other.secondary
}
}
fun update(other: TangemColors) {
text.update(other.text)
icon.update(other.icon)
@ -228,5 +246,6 @@ class TangemColors internal constructor(
control.update(other.control)
stroke.update(other.stroke)
field.update(other.field)
overlay.update(other.overlay)
}
}

View file

@ -4,9 +4,9 @@ import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material.Colors
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color
@ -90,7 +90,7 @@ fun TangemTheme(
val rootBackgroundColor = rememberedColors.background.secondary
MaterialTheme(
colors = materialThemeColors(colors = themeColors, isDark = isDark),
colorScheme = tangemColorScheme(colors = themeColors),
) {
CompositionLocalProvider(
LocalTangemColors provides rememberedColors,
@ -143,21 +143,51 @@ object TangemTheme {
@Stable
@Composable
private fun materialThemeColors(colors: TangemColors, isDark: Boolean): Colors {
return Colors(
private fun tangemColorScheme(colors: TangemColors): ColorScheme {
return ColorScheme(
primary = colors.background.primary,
primaryVariant = colors.background.secondary,
secondary = colors.button.primary,
secondaryVariant = colors.text.accent,
background = colors.background.primary,
surface = colors.background.secondary,
error = colors.text.warning,
onPrimary = colors.text.primary1,
primaryContainer = colors.background.secondary,
onPrimaryContainer = colors.background.action,
inversePrimary = colors.background.action,
secondary = colors.button.primary,
onSecondary = colors.text.primary1,
secondaryContainer = colors.background.secondary,
onSecondaryContainer = colors.text.primary1,
tertiary = colors.background.tertiary,
onTertiary = colors.text.tertiary,
tertiaryContainer = colors.background.tertiary,
onTertiaryContainer = colors.text.tertiary,
background = colors.background.primary,
onBackground = colors.text.primary1,
surface = colors.background.secondary,
surfaceVariant = colors.background.tertiary,
onSurface = colors.text.primary1,
onSurfaceVariant = colors.text.secondary,
surfaceTint = colors.background.tertiary,
inverseSurface = colors.button.disabled,
inverseOnSurface = colors.button.primary,
surfaceBright = colors.background.secondary,
surfaceDim = colors.background.tertiary,
surfaceContainer = colors.background.tertiary,
surfaceContainerHigh = colors.background.tertiary,
surfaceContainerHighest = colors.background.tertiary,
surfaceContainerLow = colors.background.tertiary,
surfaceContainerLowest = colors.background.tertiary,
error = colors.text.warning,
errorContainer = colors.background.tertiary,
onErrorContainer = colors.text.primary2,
onError = colors.text.primary2,
isLight = !isDark,
outline = colors.stroke.primary,
outlineVariant = colors.stroke.secondary,
scrim = colors.stroke.transparency,
)
}
@ -208,6 +238,10 @@ private fun lightThemeColors(): TangemColors {
primary = TangemColorPalette.Light1,
focused = TangemColorPalette.Light2,
),
overlay = TangemColors.Overlay(
primary = TangemColorPalette.Black.copy(alpha = 0.4f),
secondary = TangemColorPalette.Black.copy(alpha = 0.7f),
),
)
}
@ -258,6 +292,10 @@ private fun darkThemeColors(): TangemColors {
primary = TangemColorPalette.Dark5,
focused = TangemColorPalette.Dark4,
),
overlay = TangemColors.Overlay(
primary = TangemColorPalette.Black.copy(alpha = 0.4f),
secondary = TangemColorPalette.Black.copy(alpha = 0.7f),
),
)
}

View file

@ -1,72 +0,0 @@
package com.tangem.core.ui.screen
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.FloatRange
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
/**
* An abstract base class for bottom sheet dialogs that use Compose for UI rendering.
* Extends [BottomSheetDialogFragment] and implements [ComposeScreen] interface.
*/
abstract class ComposeBottomSheetFragment : BottomSheetDialogFragment(), ComposeScreen {
/**
* The initial state of the bottom sheet. Default is [BottomSheetBehavior.STATE_EXPANDED].
*/
open val initialBottomSheetState = BottomSheetBehavior.STATE_EXPANDED
/**
* The fraction of the screen height that the bottom sheet should take when expanded.
* Default is `null`, indicating that the height will be determined by the content.
*/
@FloatRange(from = 0.0, to = 1.0)
open val expandedHeightFraction: Float? = null
override val screenModifier: Modifier
@Composable
@ReadOnlyComposable
get() = Modifier
.fillMaxWidth()
.let {
if (expandedHeightFraction != null) it.fillMaxHeight(expandedHeightFraction!!) else it
}
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.bottomSheet,
)
override fun getTheme(): Int = R.style.AppTheme_TransparentBottomSheetDialog
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return createComposeView(
context = inflater.context,
activity = requireActivity(),
overrideSystemBarColors = false,
)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState)
(dialog as BottomSheetDialog).behavior.apply {
state = initialBottomSheetState
skipCollapsed = true
}
return dialog
}
}

View file

@ -1,50 +0,0 @@
package com.tangem.core.ui.screen
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.core.ui.R
/**
* An abstract base class for fragments that use Compose for UI rendering.
* Extends [Fragment] and implements [ComposeScreen] interface.
*/
abstract class ComposeFragment : Fragment(), ComposeScreen {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val isTransitionsInflated = TransitionInflater.from(requireContext()).inflateTransitions()
return createComposeView(inflater.context, requireActivity()).also {
it.isTransitionGroup = isTransitionsInflated
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
/*
* We need to manually dispatch configuration changes to the Compose view.
*
* `android:configChanges="uiMode"` is set in the manifest.
* */
view?.dispatchConfigurationChanged(newConfig)
}
/**
* Inflates transitions for the fragment. Override this method to customize
* enter and exit transitions for the fragment.
*
* @return `true` if transitions were inflated; `false` otherwise.
*/
protected open fun TransitionInflater.inflateTransitions(): Boolean {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
return true
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.core.ui.test
object BaseBlockTestTags {
const val BLOCK = "BASE_BLOCK"
const val BLOCK_TITLE = "BASE_BLOCK_TITLE"
const val BLOCK_TEXT = "BASE_BLOCK_REWARDS_TEXT"
}

View file

@ -0,0 +1,7 @@
package com.tangem.core.ui.test
object BaseButtonTestTags {
const val BUTTON = "BASE_BUTTON"
const val ICON = "BASE_BUTTON_ICON"
const val TEXT = "BASE_BUTTON_TEXT"
}

View file

@ -0,0 +1,16 @@
package com.tangem.core.ui.test
object BuyTokenDetailsScreenTestTags {
const val EXPAND_FIAT_LIST_BUTTON = "BUY_TOKEN_DETAILS_SCREEN_EXPAND_FIAT_LIST_BUTTON"
const val FIAT_CURRENCY_ICON = "BUY_TOKEN_DETAILS_SCREEN_FIAT_CURRENCY_ICON"
const val FIAT_AMOUNT_TEXT_FIELD = "BUY_TOKEN_DETAILS_SCREEN_FIAT_AMOUNT_TEXT_FIELD"
const val TOKEN_AMOUNT = "BUY_TOKEN_DETAILS_SCREEN_TOKEN_AMOUNT"
const val PROVIDER_LOADING_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE"
const val PROVIDER_LOADING_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_LOADING_TITLE"
const val PROVIDER_TITLE = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TITLE"
const val PROVIDER_TEXT = "BUY_TOKEN_DETAILS_SCREEN_PROVIDER_TEXT"
const val TOS_BLOCK = "BUY_TOKEN_DETAILS_SCREEN_TOS_BLOCK"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object BuyTokenFiatListTestTags {
const val LAZY_LIST = "BUY_TOKEN_FIAT_LIST_LAZY_LIST"
const val LAZY_LIST_ITEM = "BUY_TOKEN_FIAT_LIST_LAZY_LIST_ITEM"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object BuyTokenScreenTestTags {
const val LAZY_LIST = "BUY_TOKEN_SCREEN_LAZY_LIST"
const val LAZY_LIST_ITEM = "BUY_TOKEN_SCREEN_LAZY_LIST_ITEM"
}

View file

@ -2,5 +2,4 @@ package com.tangem.core.ui.test
object DialogTestTags {
const val DIALOG_CONTAINER = "DIALOG_CONTAINER"
const val BUTTON = "DIALOG_BUTTON"
}

View file

@ -3,4 +3,5 @@ package com.tangem.core.ui.test
object DisclaimerScreenTestTags {
const val SCREEN_CONTAINER = "DISCLAIMER_SCREEN_CONTAINER"
const val ACCEPT_BUTTON = "DISCLAIMER_SCREEN_ACCEPT_BUTTON"
const val WEB_VIEW = "DISCLAIMER_SCREEN_WEB_VIEW"
}

View file

@ -8,4 +8,5 @@ object MainScreenTestTags {
const val WALLET_BALANCE = "MAIN_SCREEN_WALLET_BALANCE"
const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM"
const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON"
const val MULTI_CURRENCY_ACTION_BUTTON = "MAIN_SCREEN_MULTI_CURRENCY_ACTION_BUTTON"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object NotificationTestTags {
const val TITLE = "NOTIFICATION_TITLE"
const val TEXT = "NOTIFICATION_TEXT"
}

View file

@ -0,0 +1,8 @@
package com.tangem.core.ui.test
object ReferralProgramScreenTestTags {
const val IMAGE = "REFERRAL_PROGRAM_SCREEN_IMAGE"
const val CONDITION_BLOCK = "REFERRAL_PROGRAM_SCREEN_CONDITION_BLOCK"
const val INFO_FOR_YOU_TEXT = "REFERRAL_PROGRAM_SCREEN_INFO_FOR_YOU_TEXT"
const val INFO_FOR_YOUR_FRIEND_TEXT = "REFERRAL_PROGRAM_INFO_FOR_YOUR_FRIEND_TEXT"
}

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui.test
object ResidenceSettingsScreenTestTags {
const val COUNTRY_NAME = "RESIDENCE_SETTINGS_SCREEN_COUNTRY_NAME"
}

View file

@ -0,0 +1,12 @@
package com.tangem.core.ui.test
object SelectCountryBottomSheetTestTags {
const val LAZY_LIST = "SELECT_COUNTRY_BOTTOM_SHEET_LAZY_LIST"
const val COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ITEM"
const val UNAVAILABLE_COUNTRY_ITEM = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ITEM"
const val SEARCH_BAR = "SELECT_COUNTRY_BOTTOM_SHEET_SEARCH_BAR"
const val COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_ICON"
const val UNAVAILABLE_COUNTRY_ICON = "SELECT_COUNTRY_BOTTOM_SHEET_UNAVAILABLE_COUNTRY_ICON"
const val COUNTRY_NAME = "SELECT_COUNTRY_BOTTOM_SHEET_COUNTRY_NAME"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object SelectNetworkFeeBottomSheetTestTags {
const val READ_MORE_TEXT = "SELECT_NETWORK_FEE_READ_MORE_TEXT"
const val SELECTOR_ITEM = "SELECT_NETWORK_FEE_SELECTOR_ITEM"
}

View file

@ -0,0 +1,7 @@
package com.tangem.core.ui.test
object SelectPaymentMethodBottomSheetTestTags {
const val LAZY_LIST = "SELECT_PAYMENT_METHOD_LAZY_LIST"
const val PAYMENT_METHOD_ICON = "PAYMENT_METHOD_NAME"
}

View file

@ -0,0 +1,18 @@
package com.tangem.core.ui.test
object SelectProviderBottomSheetTestTags {
const val PAYMENT_METHOD_ICON = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_ICON"
const val PAYMENT_METHOD_TITLE = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_TITLE"
const val PAYMENT_METHOD_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_NAME"
const val PAYMENT_METHOD_EXPAND_BUTTON = "SELECT_PROVIDER_BOTTOM_SHEET_PAYMENT_METHOD_EXPAND_BUTTON"
const val TOKEN_AMOUNT = "SELECT_PROVIDER_BOTTOM_SHEET_TOKEN_AMOUNT"
const val AVAILABLE_PROVIDER_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_AVAILABLE_PROVIDER_NAME"
const val AVAILABLE_PROVIDER_ITEM = "SELECT_PROVIDER_BOTTOM_SHEET_AVAILABLE_PROVIDER_ITEM"
const val UNAVAILABLE_PROVIDER_ITEM = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_ITEM"
const val UNAVAILABLE_PROVIDER_NAME = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_NAME"
const val UNAVAILABLE_PROVIDER_SUBTITLE = "SELECT_PROVIDER_BOTTOM_SHEET_UNAVAILABLE_PROVIDER_SUBTITLE"
const val MORE_PROVIDERS_ICON = "SELECT_PROVIDER_BOTTOM_SHEET_MORE_PROVIDERS_ICON"
const val MORE_PROVIDERS_TEXT = "SELECT_PROVIDER_BOTTOM_SHEET_MORE_PROVIDERS_TEXT"
const val BEST_RATE_LABEL = "SELECT_PROVIDER_BOTTOM_SHEET_BEST_RATE_LABEL"
}

View file

@ -0,0 +1,15 @@
package com.tangem.core.ui.test
object StakingDetailsScreenTestTags {
const val SCREEN_CONTAINER = "TOKEN_DETAILS_SCREEN_CONTAINER"
const val BANNER_IMAGE = "TOKEN_DETAILS_SCREEN_BANNER_IMAGE"
const val BANNER_TEXT = "TOKEN_DETAILS_SCREEN_BANNER_TEXT"
const val PARAMETER_BLOCK = "STAKING_DETAILS_PARAMETER_BLOCK"
const val PARAMETER_NAME = "STAKING_DETAILS_PARAMETER_NAME"
const val PARAMETER_VALUE = "STAKING_DETAILS_PARAMETER_VALUE"
const val TOS_TEXT = "STAKING_DETAILS_TOS_TEXT"
const val ACTIVE_STAKING_BLOCK = "STAKING_DETAILS_ACTIVE_STAKING_BLOCK"
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.test
object StakingSendDetailsScreenTestTags {
const val PRIMARY_AMOUNT = "STAKING_SEND_DETAILS_SCREEN_PRIMARY_AMOUNT"
const val SECONDARY_AMOUNT = "TAKING_SEND_DETAILS_SCREEN_SECONDARY_AMOUNT"
const val VALIDATOR_BLOCK = "TAKING_SEND_DETAILS_SCREEN_VALIDATOR_BLOCK"
const val NETWORK_FEE_BLOCK = "TAKING_SEND_DETAILS_SCREEN_NETWORK_FEE_BLOCK"
}

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