Updated on 2026-08-14
This commit is contained in:
commit
fe2cfac54d
1140 changed files with 23493 additions and 8579 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -50,5 +50,13 @@
|
|||
{
|
||||
"name": "HOT_WALLET_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NFT_SEND_REDESIGN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "TANGEM_PAY_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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>,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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> */
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.datasource.local.swaptx
|
||||
package com.tangem.datasource.local.swap
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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 :
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">You’re 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 Tangem’s best-in-class hardware wallet.</string>
|
||||
<string name="hw_upgrade_to_cold_banner_title">Upgrade wallet with a hardware
backup</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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 $"),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
},
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -2,5 +2,4 @@ package com.tangem.core.ui.test
|
|||
|
||||
object DialogTestTags {
|
||||
const val DIALOG_CONTAINER = "DIALOG_CONTAINER"
|
||||
const val BUTTON = "DIALOG_BUTTON"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object NotificationTestTags {
|
||||
const val TITLE = "NOTIFICATION_TITLE"
|
||||
const val TEXT = "NOTIFICATION_TEXT"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object ResidenceSettingsScreenTestTags {
|
||||
const val COUNTRY_NAME = "RESIDENCE_SETTINGS_SCREEN_COUNTRY_NAME"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue