Updated on 2026-08-14
This commit is contained in:
commit
af09ce9ca4
877 changed files with 16091 additions and 6613 deletions
|
|
@ -17,7 +17,6 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Other libraries */
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Core modules */
|
||||
implementation(projects.core.analytics.models)
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import com.amplitude.experiment.ExperimentUser
|
|||
import com.tangem.core.abtests.manager.ABTestsManager
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AmplitudeABTestsManager(
|
||||
val application: Application,
|
||||
|
|
@ -21,7 +21,7 @@ internal class AmplitudeABTestsManager(
|
|||
|
||||
override fun init() {
|
||||
if (::client.isInitialized) {
|
||||
Timber.w("AB Tests manager already initialized, skipping")
|
||||
TangemLogger.w("AB Tests manager already initialized, skipping")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ internal class AmplitudeABTestsManager(
|
|||
val allVariants = client.all()
|
||||
logAllVariants(allVariants)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception, "Failed to fetch AB test variants")
|
||||
TangemLogger.e("Failed to fetch AB test variants", exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -69,23 +69,23 @@ internal class AmplitudeABTestsManager(
|
|||
}
|
||||
|
||||
private fun logAllVariants(allVariants: Map<String, com.amplitude.experiment.Variant>) {
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
Timber.d("AB Tests: Fetched ${allVariants.size} variants")
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
|
||||
TangemLogger.d("AB Tests: Fetched ${allVariants.size} variants")
|
||||
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
|
||||
|
||||
if (allVariants.isEmpty()) {
|
||||
Timber.d("No variants available")
|
||||
TangemLogger.d("No variants available")
|
||||
} else {
|
||||
allVariants.entries.forEachIndexed { index, (key, variant) ->
|
||||
Timber.d("[${index + 1}/${allVariants.size}] Key: $key")
|
||||
Timber.d(" → Value: ${variant.value ?: "null"}")
|
||||
Timber.d(" → Payload: ${variant.payload ?: "null"}")
|
||||
Timber.d(" → Key: ${variant.key ?: "null"}")
|
||||
Timber.d("-".repeat(SEPARATOR_LENGTH))
|
||||
TangemLogger.d("[${index + 1}/${allVariants.size}] Key: $key")
|
||||
TangemLogger.d(" → Value: ${variant.value ?: "null"}")
|
||||
TangemLogger.d(" → Payload: ${variant.payload ?: "null"}")
|
||||
TangemLogger.d(" → Key: ${variant.key ?: "null"}")
|
||||
TangemLogger.d("-".repeat(SEPARATOR_LENGTH))
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("=".repeat(SEPARATOR_LENGTH))
|
||||
TangemLogger.d("=".repeat(SEPARATOR_LENGTH))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
|||
|
|
@ -295,6 +295,7 @@ sealed class AnalyticsParam {
|
|||
const val REFERRAL = "Referral"
|
||||
const val REFERRAL_ID = "Referral_ID"
|
||||
const val SEARCHED = "Searched"
|
||||
const val RATE_TYPE = "Rate Type"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,4 +35,19 @@ sealed class TechAnalyticsEvent(
|
|||
put("isTrusted", isTrusted.toString())
|
||||
},
|
||||
)
|
||||
|
||||
class MediaTekVulnerability(
|
||||
model: String,
|
||||
manufacturer: String,
|
||||
hardware: String,
|
||||
patch: String,
|
||||
) : TechAnalyticsEvent(
|
||||
event = "MediaTek Vulnerability",
|
||||
params = mapOf(
|
||||
"SocModel" to model,
|
||||
"SocManufacturer" to manufacturer,
|
||||
"SocHardware" to hardware,
|
||||
"Patch" to patch,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -73,7 +73,6 @@ dependencies {
|
|||
/** Other libraries */
|
||||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.timber)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
|
||||
/** Core modules */
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "MULTI_ADDRESS_UTXO_ENABLED",
|
||||
"name": "DYNAMIC_ADDRESSES_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.core.configtoggle.version
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Presentation of application version (<major>.<minor>.<fix?>).
|
||||
|
|
@ -67,7 +67,7 @@ internal class Version private constructor(value: String) : Comparable<Version>
|
|||
return try {
|
||||
Version(value)
|
||||
} catch (exception: Exception) {
|
||||
Timber.e(exception, "Invalid version - %s", value)
|
||||
TangemLogger.e("Invalid version - $value", exception)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
core/datasource/CLAUDE.md
Normal file
30
core/datasource/CLAUDE.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# core/datasource
|
||||
|
||||
## API Integration Guide
|
||||
|
||||
### Config Structure
|
||||
|
||||
- `ApiConfig` — base API config with `id` (`ApiConfig.ID`), `defaultEnvironment` (`ApiEnvironment`), and `environmentConfigs` (list of `ApiEnvironmentConfig`)
|
||||
- `ApiEnvironmentConfig` — per-environment settings: `environment`, `baseUrl`, and `headers` (map of header name to `Provider<String>`)
|
||||
|
||||
### Config Management
|
||||
|
||||
- `ApiConfigsManager` — DI-available component for accessing configs via `getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig`
|
||||
- Two implementations: `ProdApiConfigsManager` (release) and `DevApiConfigsManager` (extends `MutableApiConfigsManager`, used when `BuildConfig.TESTER_MENU_ENABLED`)
|
||||
- `MutableApiConfigsManager` allows runtime environment switching via Tester Menu without app restart
|
||||
|
||||
### Adding a New API
|
||||
|
||||
1. Create `ApiConfig` subclass in `com.tangem.datasource.api.common.config` — override `defaultEnvironment` and `environmentConfigs`. DI dependencies can be injected via constructor
|
||||
2. Register the new config ID in `ApiConfig.initializeId(...)`
|
||||
3. Provide the config in `ApiConfigsModule` using `@Provides @IntoSet`
|
||||
4. Provide the API Retrofit service in `NetworkModule`:
|
||||
- Get environment config: `apiConfigsManager.getEnvironmentConfig(id)`
|
||||
- Use `environmentConfig.baseUrl` for Retrofit base URL
|
||||
- Apply headers via `OkHttpClient.Builder().applyApiConfig(id, apiConfigsManager)`
|
||||
|
||||
### Testing
|
||||
|
||||
- Add the new config to `API_CONFIGS` list in `ProdApiConfigsManagerTest`
|
||||
- Add a test model in the `data` method with expected `ApiEnvironmentConfig` values
|
||||
- If the config has constructor dependencies, mock them and set up behavior in `setup()`
|
||||
|
|
@ -97,7 +97,6 @@ dependencies {
|
|||
implementation(deps.kotlin.datetime)
|
||||
|
||||
/** Logging */
|
||||
implementation(deps.timber)
|
||||
|
||||
/** Network */
|
||||
implementation(deps.moshi)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
package com.tangem.datasource.api.common.response
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import okhttp3.Request
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
|
||||
internal class ApiResponseCallDelegate<T : Any>(
|
||||
private val wrappedCall: Call<T>,
|
||||
|
|
@ -41,10 +41,10 @@ internal class ApiResponseCallDelegate<T : Any>(
|
|||
val error = try {
|
||||
t.toApiError()
|
||||
} catch (e: ApiResponseError) {
|
||||
Timber.e(e, "error map toApiError")
|
||||
TangemLogger.e("error map toApiError", e)
|
||||
e
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "onFailure UnknownException")
|
||||
TangemLogger.e("onFailure UnknownException", e)
|
||||
ApiResponseError.UnknownException(e)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ package com.tangem.datasource.api.common.response
|
|||
|
||||
import com.tangem.core.analytics.api.AnalyticsErrorHandler
|
||||
import com.tangem.datasource.api.common.response.analytics.ApiErrorEvent
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import retrofit2.Response
|
||||
import timber.log.Timber
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
|
|
@ -31,7 +31,7 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(analyticsErrorHandler: Anal
|
|||
ApiResponseError.HttpException(code, message(), errorBody)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "UnknownException occured")
|
||||
TangemLogger.e("UnknownException occured", e)
|
||||
ApiResponseError.UnknownException(e)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ interface TangemExpressApi {
|
|||
@Query("refundExtraId") refundExtraId: String?, // for cex only
|
||||
@Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send
|
||||
@Query("toExtraId") toExtraId: String?, // swap-and-send memo
|
||||
@Query("quoteId") quoteId: String?, // fixed rate quoteId
|
||||
): ApiResponse<ExchangeDataResponse>
|
||||
|
||||
@GET("exchange-status")
|
||||
|
|
|
|||
|
|
@ -25,4 +25,7 @@ data class ExchangeQuoteResponse(
|
|||
@Json(name = "minAmount")
|
||||
val minAmount: BigDecimal,
|
||||
|
||||
@Json(name = "quoteId")
|
||||
val quoteId: String? = null,
|
||||
|
||||
)
|
||||
|
|
@ -104,6 +104,8 @@ data class TokenMarketInfoResponse(
|
|||
data class Metrics(
|
||||
@Json(name = "market_rating")
|
||||
val marketRating: Int?,
|
||||
@Json(name = "market_rating_change_24h")
|
||||
val marketRatingChange24h: Int?,
|
||||
@Json(name = "circulating_supply")
|
||||
val circulatingSupply: BigDecimal?,
|
||||
@Json(name = "market_cap")
|
||||
|
|
|
|||
|
|
@ -191,12 +191,12 @@ interface TangemTechApi {
|
|||
suspend fun getPromoBannerDisplays(
|
||||
@Query("walletId") walletId: String,
|
||||
@Query("placeholder") placeholder: String,
|
||||
@Query("locale") locale: String,
|
||||
@Query("lang") languageISOCode: String,
|
||||
): ApiResponse<PromoBannerDisplaysResponse>
|
||||
|
||||
@PATCH("v1/displays/{displayId}")
|
||||
@PATCH("v1/banner/displays/{displayId}")
|
||||
suspend fun dismissPromoBannerDisplay(
|
||||
@Path("displayId") displayId: String,
|
||||
@Path("displayId") displayId: Int,
|
||||
@Body body: DismissPromoBannerRequest,
|
||||
): ApiResponse<DismissPromoBannerResponse>
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ data class QuotesResponse(
|
|||
val priceChange1w: BigDecimal?,
|
||||
@Json(name = "priceChange30d")
|
||||
val priceChange30d: BigDecimal?,
|
||||
@Json(name = "priceUsd")
|
||||
val priceUsd: BigDecimal?,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
|
@ -29,6 +31,7 @@ data class QuotesResponse(
|
|||
priceChange24h = null,
|
||||
priceChange1w = null,
|
||||
priceChange30d = null,
|
||||
priceUsd = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,31 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PromoBannerDisplaysResponse(
|
||||
@Json(name = "items") val items: List<PromoBannerDisplayDTO>,
|
||||
@Json(name = "items")
|
||||
val items: List<PromoBannerDisplayDTO>,
|
||||
)
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PromoBannerDisplayDTO(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "placeholder") val placeholder: String,
|
||||
@Json(name = "priority") val priority: String,
|
||||
@Json(name = "title") val title: String,
|
||||
@Json(name = "subtitle") val subtitle: String,
|
||||
@Json(name = "iconUrl") val iconUrl: String?,
|
||||
@Json(name = "deeplink") val deeplink: String?,
|
||||
@Json(name = "buttonEnabled") val buttonEnabled: Boolean,
|
||||
@Json(name = "buttonText") val buttonText: String?,
|
||||
@Json(name = "dismissable") val dismissable: Boolean,
|
||||
@Json(name = "id")
|
||||
val id: Int,
|
||||
@Json(name = "placeholder")
|
||||
val placeholder: String,
|
||||
@Json(name = "priority")
|
||||
val priority: String,
|
||||
@Json(name = "title")
|
||||
val title: String,
|
||||
@Json(name = "subtitle")
|
||||
val subtitle: String,
|
||||
@Json(name = "iconUrl")
|
||||
val iconUrl: String?,
|
||||
@Json(name = "deeplink")
|
||||
val deeplink: String?,
|
||||
@Json(name = "buttonEnabled")
|
||||
val buttonEnabled: Boolean,
|
||||
@Json(name = "buttonText")
|
||||
val buttonText: String?,
|
||||
@Json(name = "dismissable")
|
||||
val dismissable: Boolean,
|
||||
)
|
||||
|
|
@ -9,8 +9,8 @@ import com.tangem.datasource.asset.reader.AssetReader
|
|||
import com.tangem.datasource.di.NetworkMoshi
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -49,7 +49,10 @@ class AssetLoader @Inject constructor(
|
|||
json = json,
|
||||
)
|
||||
|
||||
Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
|
||||
TangemLogger.e(
|
||||
"Error",
|
||||
IllegalStateException("Parsed config [$fileName] is null"),
|
||||
)
|
||||
}
|
||||
|
||||
parsedConfig
|
||||
|
|
@ -61,51 +64,65 @@ class AssetLoader @Inject constructor(
|
|||
json = json,
|
||||
)
|
||||
|
||||
Timber.e(throwable, "Failed to load config [$fileName] from assets")
|
||||
TangemLogger.e("Failed to load config [$fileName] from assets", throwable)
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Load list [V] values of asset file [fileName] */
|
||||
suspend inline fun <reified V> loadList(fileName: String): List<V> = runCatching(dispatchers.io) {
|
||||
val json = assetReader.read(fullFileName = "$fileName.json")
|
||||
suspend inline fun <reified V> loadList(fileName: String): List<V> {
|
||||
val result = runCatching(dispatchers.io) {
|
||||
val json = assetReader.read(fullFileName = "$fileName.json")
|
||||
|
||||
val type = Types.newParameterizedType(List::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<List<V>>(type)
|
||||
val type = Types.newParameterizedType(List::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<List<V>>(type)
|
||||
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
.fold(
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
return result.fold(
|
||||
onSuccess = { parsedConfig ->
|
||||
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
|
||||
if (parsedConfig == null) {
|
||||
TangemLogger.e(
|
||||
"Error",
|
||||
IllegalStateException("Parsed config [$fileName] is null"),
|
||||
)
|
||||
}
|
||||
parsedConfig.orEmpty()
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
Timber.e(throwable, "Failed to load config [$fileName] from assets")
|
||||
TangemLogger.e("Failed to load config [$fileName] from assets", throwable)
|
||||
emptyList()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Load map [String] keys and [V] values of asset file [fileName] */
|
||||
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> = runCatching(dispatchers.io) {
|
||||
val json = assetReader.read(fullFileName = "$fileName.json")
|
||||
suspend inline fun <reified V> loadMap(fileName: String): Map<String, V> {
|
||||
val result = runCatching(dispatchers.io) {
|
||||
val json = assetReader.read(fullFileName = "$fileName.json")
|
||||
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
.fold(
|
||||
adapter.fromJson(json)
|
||||
}
|
||||
return result.fold(
|
||||
onSuccess = { parsedConfig ->
|
||||
if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null"))
|
||||
if (parsedConfig == null) {
|
||||
TangemLogger.e(
|
||||
"Error",
|
||||
IllegalStateException("Parsed config [$fileName] is null"),
|
||||
)
|
||||
}
|
||||
parsedConfig.orEmpty()
|
||||
},
|
||||
onFailure = { throwable ->
|
||||
Timber.e(throwable, "Failed to load config [$fileName] from assets")
|
||||
TangemLogger.e("Failed to load config [$fileName] from assets", throwable)
|
||||
emptyMap()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun sendException(fileName: String, isParsingSuccess: Boolean, json: String?) {
|
||||
analyticsExceptionHandler.sendException(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter
|
|||
import com.tangem.datasource.api.common.adapter.*
|
||||
import com.tangem.datasource.local.config.providers.models.ProviderModel
|
||||
import com.tangem.datasource.local.network.entity.NetworkStatusDM
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM
|
||||
import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM
|
||||
import com.tangem.datasource.utils.SerializeNullsFactory
|
||||
import com.tangem.domain.models.scan.serialization.*
|
||||
import dagger.Module
|
||||
|
|
@ -47,13 +47,12 @@ class MoshiModule {
|
|||
.withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"),
|
||||
)
|
||||
.add(
|
||||
NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java)
|
||||
.withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created")
|
||||
.withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status")
|
||||
.withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card")
|
||||
.withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked")
|
||||
.withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance")
|
||||
.withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"),
|
||||
NamePolymorphicAdapterFactory.of(PaymentAccountStatusValueDM::class.java)
|
||||
.withSubtype(PaymentAccountStatusValueDM.NotCreated::class.java, "not_created")
|
||||
.withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status")
|
||||
.withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card")
|
||||
.withSubtype(PaymentAccountStatusValueDM.ActiveCard::class.java, "active_card")
|
||||
.withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"),
|
||||
)
|
||||
.add(
|
||||
PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc")
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import timber.log.Timber
|
||||
|
||||
class StatusCodeInterceptor : Interceptor {
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ class StatusCodeInterceptor : Interceptor {
|
|||
val originalResponse = chain.proceed(chain.request())
|
||||
|
||||
if (shouldInterceptResponse(originalResponse)) {
|
||||
Timber.e("StatusCodeInterceptor INTERCEPTED%s", originalResponse.request.url.toString())
|
||||
TangemLogger.e("StatusCodeInterceptor INTERCEPTED${originalResponse.request.url}")
|
||||
|
||||
val body = getBody().toResponseBody("application/json".toMediaTypeOrNull())
|
||||
val code = getCode()
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
|||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import com.tangem.domain.wallets.models.AppsFlyerConversionData
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
internal class DefaultAppsFlyerStore(
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
|
|
@ -18,14 +18,14 @@ internal class DefaultAppsFlyerStore(
|
|||
val dto = appPreferencesStore.getObjectSyncOrNull<ConversionDataDTO>(CONVERSION_DATA_KEY) ?: return null
|
||||
|
||||
return ConversionDataConverter.convertBack(value = dto).also {
|
||||
Timber.i("Getting conversion data from store: $it")
|
||||
TangemLogger.i("Getting conversion data from store: $it")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getUID(): String? = appPreferencesStore.getSyncOrNull(UID_KEY)
|
||||
|
||||
override suspend fun store(value: AppsFlyerConversionData) {
|
||||
Timber.i("Storing conversion data to store: $value")
|
||||
TangemLogger.i("Storing conversion data to store: $value")
|
||||
|
||||
val dto = ConversionDataConverter.convert(value)
|
||||
|
||||
|
|
@ -33,24 +33,24 @@ internal class DefaultAppsFlyerStore(
|
|||
}
|
||||
|
||||
override suspend fun storeIfAbsent(value: AppsFlyerConversionData) {
|
||||
Timber.i("Storing conversion data to store if absent: $value")
|
||||
TangemLogger.i("Storing conversion data to store if absent: $value")
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val saved = preferences[CONVERSION_DATA_KEY]
|
||||
|
||||
if (saved == null) {
|
||||
Timber.i("Conversion data is absent, storing $value")
|
||||
TangemLogger.i("Conversion data is absent, storing $value")
|
||||
preferences.setObject(CONVERSION_DATA_KEY, ConversionDataConverter.convert(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun storeUIDIfAbsent(value: String) {
|
||||
Timber.i("Storing UID to store if absent: $value")
|
||||
TangemLogger.i("Storing UID to store if absent: $value")
|
||||
appPreferencesStore.editData { preferences ->
|
||||
val saved = preferences[UID_KEY]
|
||||
|
||||
if (saved == null) {
|
||||
Timber.i("UID is absent, storing $value")
|
||||
TangemLogger.i("UID is absent, storing $value")
|
||||
preferences[UID_KEY] = value
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
package com.tangem.datasource.local.config.environment.converter
|
||||
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converts [EnvironmentConfigModel] to [BlockchainSdkConfig]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object BlockchainSDKConfigConverter : Converter<EnvironmentConfigModel, BlockchainSdkConfig> {
|
||||
|
||||
override fun convert(value: EnvironmentConfigModel): BlockchainSdkConfig {
|
||||
return BlockchainSdkConfig(
|
||||
blockchairCredentials = BlockchairCredentials(
|
||||
apiKey = value.blockchairApiKeys,
|
||||
authToken = value.blockchairAuthorizationToken,
|
||||
),
|
||||
blockcypherTokens = value.blockcypherTokens,
|
||||
quickNodeSolanaCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodeApiKey,
|
||||
subdomain = value.quiknodeSubdomain,
|
||||
),
|
||||
quickNodeBscCredentials = QuickNodeCredentials(
|
||||
apiKey = value.bscQuiknodeApiKey,
|
||||
subdomain = value.bscQuiknodeSubdomain,
|
||||
),
|
||||
quickNodePlasmaCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodePlasmaApiKey,
|
||||
subdomain = value.quiknodePlasmaSubdomain,
|
||||
),
|
||||
quickNodeMonadCredentials = QuickNodeCredentials(
|
||||
apiKey = value.quiknodeMonadApiKey,
|
||||
subdomain = value.quiknodeMonadSubdomain,
|
||||
),
|
||||
infuraProjectId = value.infuraProjectId,
|
||||
tronGridApiKey = value.tronGridApiKey,
|
||||
nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey),
|
||||
getBlockCredentials = createGetBlockCredentials(value),
|
||||
kaspaSecondaryApiUrl = value.kaspaSecondaryApiUrl,
|
||||
tonCenterCredentials = TonCenterCredentials(
|
||||
mainnetApiKey = value.tonCenterKeys.mainnet,
|
||||
testnetApiKey = value.tonCenterKeys.testnet,
|
||||
),
|
||||
chiaFireAcademyApiKey = value.chiaFireAcademyApiKey,
|
||||
chiaTangemApiKey = value.chiaTangemApiKey,
|
||||
hederaArkhiaApiKey = value.hederaArkhiaKey,
|
||||
polygonScanApiKey = value.polygonScanApiKey,
|
||||
bittensorDwellirApiKey = value.bittensorDwellirApiKey,
|
||||
bittensorOnfinalityApiKey = value.bittensorOnfinalityKey,
|
||||
dwellirApiKey = value.dwellirApiKey,
|
||||
koinosProApiKey = value.koinosProApiKey,
|
||||
alephiumApiKey = value.alephiumTangemApiKey,
|
||||
moralisApiKey = value.moralisApiKey,
|
||||
etherscanApiKey = value.etherScanApiKey,
|
||||
blinkApiKey = value.blinkApiKey,
|
||||
tatumApiKey = value.tatumApiKey,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createGetBlockCredentials(configValues: EnvironmentConfigModel): GetBlockCredentials? {
|
||||
return configValues.getBlockAccessTokens?.let { accessTokens ->
|
||||
GetBlockCredentials(
|
||||
xrp = GetBlockAccessToken(jsonRpc = accessTokens.xrp?.jsonRPC),
|
||||
cardano = GetBlockAccessToken(rosetta = accessTokens.cardano?.rosetta),
|
||||
avalanche = GetBlockAccessToken(jsonRpc = accessTokens.avalanche?.jsonRPC),
|
||||
eth = GetBlockAccessToken(jsonRpc = accessTokens.eth?.jsonRPC),
|
||||
etc = GetBlockAccessToken(jsonRpc = accessTokens.etc?.jsonRPC),
|
||||
fantom = GetBlockAccessToken(jsonRpc = accessTokens.fantom?.jsonRPC),
|
||||
rsk = GetBlockAccessToken(jsonRpc = accessTokens.rsk?.jsonRPC),
|
||||
bsc = GetBlockAccessToken(jsonRpc = accessTokens.bsc?.jsonRPC),
|
||||
polygon = GetBlockAccessToken(jsonRpc = accessTokens.polygon?.jsonRPC),
|
||||
gnosis = GetBlockAccessToken(jsonRpc = accessTokens.gnosis?.jsonRPC),
|
||||
cronos = GetBlockAccessToken(jsonRpc = accessTokens.cronos?.jsonRPC),
|
||||
solana = GetBlockAccessToken(jsonRpc = accessTokens.solana?.jsonRPC),
|
||||
ton = GetBlockAccessToken(jsonRpc = accessTokens.ton?.jsonRPC),
|
||||
tron = GetBlockAccessToken(rest = accessTokens.tron?.rest),
|
||||
cosmos = GetBlockAccessToken(rest = accessTokens.cosmos?.rest),
|
||||
near = GetBlockAccessToken(jsonRpc = accessTokens.near?.jsonRPC),
|
||||
aptos = GetBlockAccessToken(rest = accessTokens.aptos?.rest),
|
||||
dogecoin = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.dogecoin?.jsonRPC,
|
||||
blockBookRest = accessTokens.dogecoin?.blockBookRest,
|
||||
),
|
||||
litecoin = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.litecoin?.jsonRPC,
|
||||
blockBookRest = accessTokens.litecoin?.blockBookRest,
|
||||
),
|
||||
dash = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.dash?.jsonRPC,
|
||||
blockBookRest = accessTokens.dash?.blockBookRest,
|
||||
),
|
||||
bitcoin = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.bitcoin?.jsonRPC,
|
||||
blockBookRest = accessTokens.bitcoin?.blockBookRest,
|
||||
),
|
||||
algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest),
|
||||
zkSyncEra = GetBlockAccessToken(jsonRpc = accessTokens.zksync?.jsonRPC),
|
||||
polygonZkEvm = GetBlockAccessToken(jsonRpc = accessTokens.polygonZkevm?.jsonRPC),
|
||||
base = GetBlockAccessToken(jsonRpc = accessTokens.base?.jsonRPC),
|
||||
blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC),
|
||||
filecoin = GetBlockAccessToken(jsonRpc = accessTokens.filecoin?.jsonRPC),
|
||||
arbitrum = GetBlockAccessToken(jsonRpc = accessTokens.arbitrum?.jsonRPC),
|
||||
bitcoinCash = GetBlockAccessToken(
|
||||
jsonRpc = accessTokens.bitcoinCash?.jsonRPC,
|
||||
blockBookRest = accessTokens.bitcoinCash?.blockBookRest,
|
||||
),
|
||||
kusama = GetBlockAccessToken(jsonRpc = accessTokens.kusama?.jsonRPC),
|
||||
moonbeam = GetBlockAccessToken(jsonRpc = accessTokens.moonbeam?.jsonRPC),
|
||||
optimism = GetBlockAccessToken(jsonRpc = accessTokens.optimism?.jsonRPC),
|
||||
polkadot = GetBlockAccessToken(jsonRpc = accessTokens.polkadot?.jsonRPC),
|
||||
shibarium = GetBlockAccessToken(jsonRpc = accessTokens.shibarium?.jsonRPC),
|
||||
sui = GetBlockAccessToken(jsonRpc = accessTokens.sui?.jsonRPC),
|
||||
telos = GetBlockAccessToken(jsonRpc = accessTokens.telos?.jsonRPC),
|
||||
tezos = GetBlockAccessToken(rest = accessTokens.tezos?.rest),
|
||||
monad = GetBlockAccessToken(rest = accessTokens.monad?.rest),
|
||||
stellar = GetBlockAccessToken(rest = accessTokens.stellar?.rest),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package com.tangem.datasource.local.config.environment.converter
|
||||
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from [EnvironmentConfigModel] to [EnvironmentConfig]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object EnvironmentConfigConverter : Converter<EnvironmentConfigModel, EnvironmentConfig> {
|
||||
|
||||
override fun convert(value: EnvironmentConfigModel): EnvironmentConfig {
|
||||
return EnvironmentConfig(
|
||||
moonPayApiKey = value.moonPayApiKey,
|
||||
moonPayApiSecretKey = value.moonPayApiSecretKey,
|
||||
mercuryoWidgetId = value.mercuryoWidgetId,
|
||||
mercuryoSecret = value.mercuryoSecret,
|
||||
blockchainSdkConfig = BlockchainSDKConfigConverter.convert(value = value),
|
||||
amplitudeApiKey = value.amplitudeApiKey,
|
||||
appsFlyerApiKey = value.appsFlyer.appsFlyerDevKey,
|
||||
appsAppId = value.appsFlyer.appsFlyerAppID,
|
||||
walletConnectProjectId = value.walletConnectProjectId,
|
||||
express = value.express,
|
||||
devExpress = value.devExpress,
|
||||
stakeKitApiKey = value.stakeKitApiKey,
|
||||
p2pApiKey = value.p2pApiKey,
|
||||
blockAidApiKey = value.blockaidApiKey,
|
||||
tangemApiKey = value.tangemApiKey,
|
||||
tangemApiKeyDev = value.tangemApiKeyDev,
|
||||
tangemApiKeyStage = value.tangemApiKeyStage,
|
||||
yieldModuleApiKey = value.yieldModuleApiKey,
|
||||
yieldModuleApiKeyDev = value.yieldModuleApiKeyDev,
|
||||
bffStaticToken = value.bffStaticToken,
|
||||
bffStaticTokenDev = value.bffStaticTokenDev,
|
||||
gaslessTxApiKeyDev = value.gaslessTxApiKeyDev,
|
||||
gaslessTxApiKey = value.gaslessTxApiKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
package com.tangem.datasource.local.config.environment.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@JsonClass(generateAdapter = true)
|
||||
class EnvironmentConfigModel(
|
||||
@Json(name = "mercuryoWidgetId") val mercuryoWidgetId: String,
|
||||
@Json(name = "mercuryoSecret") val mercuryoSecret: String,
|
||||
@Json(name = "moonPayApiKey") val moonPayApiKey: String,
|
||||
@Json(name = "moonPayApiSecretKey") val moonPayApiSecretKey: String,
|
||||
@Json(name = "blockchairApiKeys") val blockchairApiKeys: List<String>,
|
||||
@Json(name = "blockchairAuthorizationToken") val blockchairAuthorizationToken: String?,
|
||||
@Json(name = "quiknodeSubdomain") val quiknodeSubdomain: String,
|
||||
@Json(name = "quiknodeApiKey") val quiknodeApiKey: String,
|
||||
@Json(name = "bscQuiknodeSubdomain") val bscQuiknodeSubdomain: String,
|
||||
@Json(name = "bscQuiknodeApiKey") val bscQuiknodeApiKey: String,
|
||||
@Json(name = "quiknodePlasmaSubdomain") val quiknodePlasmaSubdomain: String,
|
||||
@Json(name = "quiknodePlasmaApiKey") val quiknodePlasmaApiKey: String,
|
||||
@Json(name = "quiknodeMonadSubdomain") val quiknodeMonadSubdomain: String,
|
||||
@Json(name = "quiknodeMonadApiKey") val quiknodeMonadApiKey: String,
|
||||
@Json(name = "nowNodesApiKey") val nowNodesApiKey: String,
|
||||
@Json(name = "getBlockAccessTokens") val getBlockAccessTokens: GetBlockAccessTokens?,
|
||||
@Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys,
|
||||
@Json(name = "blockcypherTokens") val blockcypherTokens: Set<String>?,
|
||||
@Json(name = "infuraProjectId") val infuraProjectId: String?,
|
||||
@Json(name = "tronGridApiKey") val tronGridApiKey: String,
|
||||
@Json(name = "amplitudeApiKey") val amplitudeApiKey: String,
|
||||
@Json(name = "appsFlyer") val appsFlyer: AppsFlyerModel,
|
||||
@Json(name = "kaspaSecondaryApiUrl") val kaspaSecondaryApiUrl: String,
|
||||
@Json(name = "walletConnectProjectId") val walletConnectProjectId: String,
|
||||
@Json(name = "chiaFireAcademyApiKey") val chiaFireAcademyApiKey: String?,
|
||||
@Json(name = "chiaTangemApiKey") val chiaTangemApiKey: String?,
|
||||
@Json(name = "devExpress") val devExpress: ExpressModel?,
|
||||
@Json(name = "express") val express: ExpressModel?,
|
||||
@Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?,
|
||||
@Json(name = "polygonScanApiKey") val polygonScanApiKey: String?,
|
||||
@Json(name = "stakeKitApiKey") val stakeKitApiKey: String?,
|
||||
@Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?,
|
||||
@Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?,
|
||||
@Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?,
|
||||
@Json(name = "dwellirApiKey") val dwellirApiKey: String?,
|
||||
@Json(name = "koinosProApiKey") val koinosProApiKey: String?,
|
||||
@Json(name = "alephiumTangemApiKey") val alephiumTangemApiKey: String?,
|
||||
@Json(name = "moralisApiKey") val moralisApiKey: String?,
|
||||
@Json(name = "nftScanApiKey") val nftScanApiKey: String?,
|
||||
@Json(name = "blockaidApiKey") val blockaidApiKey: String?,
|
||||
@Json(name = "tangemApiKey") val tangemApiKey: String?,
|
||||
@Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?,
|
||||
@Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?,
|
||||
@Json(name = "etherscanApiKey") val etherScanApiKey: String?,
|
||||
@Json(name = "yieldModuleApiKey") val yieldModuleApiKey: String?,
|
||||
@Json(name = "yieldModuleApiKeyDev") val yieldModuleApiKeyDev: String?,
|
||||
@Json(name = "blinkApiKey") val blinkApiKey: String?,
|
||||
@Json(name = "tatumApiKey") val tatumApiKey: String?,
|
||||
@Json(name = "bffStaticToken") val bffStaticToken: String?,
|
||||
@Json(name = "bffStaticTokenDev") val bffStaticTokenDev: String?,
|
||||
@Json(name = "gaslessTxApiKeyDev") val gaslessTxApiKeyDev: String?,
|
||||
@Json(name = "gaslessTxApiKey") val gaslessTxApiKey: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetBlockAccessTokens(
|
||||
@Json(name = "xrp") val xrp: GetBlockToken?,
|
||||
@Json(name = "cardano") val cardano: GetBlockToken?,
|
||||
@Json(name = "avalanche") val avalanche: GetBlockToken?,
|
||||
@Json(name = "ethereum") val eth: GetBlockToken?,
|
||||
@Json(name = "ethereumClassic") val etc: GetBlockToken?,
|
||||
@Json(name = "fantom") val fantom: GetBlockToken?,
|
||||
@Json(name = "rsk") val rsk: GetBlockToken?,
|
||||
@Json(name = "bsc") val bsc: GetBlockToken?,
|
||||
@Json(name = "polygon") val polygon: GetBlockToken?,
|
||||
@Json(name = "xdai") val gnosis: GetBlockToken?,
|
||||
@Json(name = "cronos") val cronos: GetBlockToken?,
|
||||
@Json(name = "solana") val solana: GetBlockToken?,
|
||||
@Json(name = "ton") val ton: GetBlockToken?,
|
||||
@Json(name = "tron") val tron: GetBlockToken?,
|
||||
@Json(name = "cosmos-hub") val cosmos: GetBlockToken?,
|
||||
@Json(name = "near") val near: GetBlockToken?,
|
||||
@Json(name = "aptos") val aptos: GetBlockToken?,
|
||||
@Json(name = "dogecoin") val dogecoin: GetBlockToken?,
|
||||
@Json(name = "litecoin") val litecoin: GetBlockToken?,
|
||||
@Json(name = "dash") val dash: GetBlockToken?,
|
||||
@Json(name = "bitcoin") val bitcoin: GetBlockToken?,
|
||||
@Json(name = "algorand") val algorand: GetBlockToken?,
|
||||
@Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?,
|
||||
@Json(name = "zksync") val zksync: GetBlockToken?,
|
||||
@Json(name = "base") val base: GetBlockToken?,
|
||||
@Json(name = "blast") val blast: GetBlockToken?,
|
||||
@Json(name = "filecoin") val filecoin: GetBlockToken?,
|
||||
@Json(name = "arbitrum-one") val arbitrum: GetBlockToken?,
|
||||
@Json(name = "bitcoinCash") val bitcoinCash: GetBlockToken?,
|
||||
@Json(name = "kusama") val kusama: GetBlockToken?,
|
||||
@Json(name = "moonbeam") val moonbeam: GetBlockToken?,
|
||||
@Json(name = "optimism") val optimism: GetBlockToken?,
|
||||
@Json(name = "polkadot") val polkadot: GetBlockToken?,
|
||||
@Json(name = "shibarium") val shibarium: GetBlockToken?,
|
||||
@Json(name = "sui") val sui: GetBlockToken?,
|
||||
@Json(name = "telos") val telos: GetBlockToken?,
|
||||
@Json(name = "tezos") val tezos: GetBlockToken?,
|
||||
@Json(name = "monad") val monad: GetBlockToken?,
|
||||
@Json(name = "stellar") val stellar: GetBlockToken?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TonCenterKeys(
|
||||
@Json(name = "mainnet") val mainnet: String,
|
||||
@Json(name = "testnet") val testnet: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class P2PKeys(
|
||||
@Json(name = "mainnet") val mainnet: String,
|
||||
@Json(name = "hoodi") val hoodi: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GetBlockToken(
|
||||
@Json(name = "jsonRpc") val jsonRPC: String?,
|
||||
@Json(name = "blockBookRest") val blockBookRest: String?,
|
||||
@Json(name = "rest") val rest: String?,
|
||||
@Json(name = "rosetta") val rosetta: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ExpressModel(
|
||||
@Json(name = "apiKey")
|
||||
val apiKey: String,
|
||||
@Json(name = "signVerifierPublicKey")
|
||||
val signVerifierPublicKey: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class AppsFlyerModel(
|
||||
@Json(name = "appsFlyerDevKey")
|
||||
val appsFlyerDevKey: String,
|
||||
@Json(name = "appsFlyerAppID")
|
||||
val appsFlyerAppID: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.local.config.environment.models
|
||||
|
||||
data class ExpressModel(val apiKey: String, val signVerifierPublicKey: String)
|
||||
|
||||
data class P2PKeys(val mainnet: String, val hoodi: String)
|
||||
|
|
@ -3,13 +3,14 @@ package com.tangem.datasource.local.logs
|
|||
import android.content.Context
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.format.DateTimeFormatterBuilder
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
|
@ -121,7 +122,7 @@ class AppLogsStore @Inject constructor(
|
|||
private fun createFileIfNotExist() {
|
||||
if (!logFile.exists()) {
|
||||
runCatching { logFile.createNewFile() }
|
||||
.onFailure(Timber::e)
|
||||
.onFailure { TangemLogger.e("Error", it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +130,7 @@ class AppLogsStore @Inject constructor(
|
|||
scope.launch {
|
||||
mutex.withLock {
|
||||
runCatching { callback() }
|
||||
.onFailure(Timber::e)
|
||||
.onFailure { TangemLogger.e("Error", it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_
|
|||
import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration
|
||||
import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Application preferences data store 'DataStore<Preferences>'.
|
||||
|
|
@ -49,7 +49,7 @@ internal object PreferencesDataStore {
|
|||
private fun createCorruptionHandler(): ReplaceFileCorruptionHandler<Preferences> {
|
||||
return ReplaceFileCorruptionHandler(
|
||||
produceNewData = { corruptionException ->
|
||||
Timber.w(corruptionException)
|
||||
TangemLogger.w("Error", corruptionException)
|
||||
emptyPreferences()
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,43 +11,58 @@ import java.math.BigDecimal
|
|||
/**
|
||||
* Payment account status for storage in the local cache.
|
||||
*
|
||||
* @see [com.tangem.domain.pay.PaymentAccountStatus]
|
||||
* @see [com.tangem.domain.models.account.AccountStatus.Payment]
|
||||
*/
|
||||
@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER)
|
||||
sealed interface PaymentAccountStatusDM {
|
||||
sealed interface PaymentAccountStatusValueDM {
|
||||
|
||||
@NameLabel("not_created")
|
||||
data class NotCreated(
|
||||
@Json(name = "not_created") val marker: Boolean = true,
|
||||
) : PaymentAccountStatusDM
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
||||
@NameLabel("kyc_status")
|
||||
data class UnderReview(
|
||||
@Json(name = "kyc_status") val kycStatus: KycStatus,
|
||||
) : PaymentAccountStatusDM
|
||||
@Json(name = "customer_id") val customerId: String,
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
||||
@NameLabel("issuing_card")
|
||||
data class IssuingCard(
|
||||
@Json(name = "issuing_card") val marker: Boolean = true,
|
||||
) : PaymentAccountStatusDM
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
||||
@NameLabel("locked")
|
||||
data class Locked(
|
||||
@Json(name = "locked") val marker: Boolean = true,
|
||||
) : PaymentAccountStatusDM
|
||||
|
||||
@NameLabel("balance")
|
||||
data class Loaded(
|
||||
@NameLabel("active_card")
|
||||
data class ActiveCard(
|
||||
@Json(name = "active_card") val isLocked: Boolean,
|
||||
@Json(name = "customer_id") val customerId: String,
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "last_four_digits") val lastFourDigits: String,
|
||||
@Json(name = "balance") val balance: BigDecimal,
|
||||
@Json(name = "currency_code") val currencyCode: String,
|
||||
@Json(name = "deposit_address") val depositAddress: String?,
|
||||
@Json(name = "is_pin_set") val isPinSet: Boolean,
|
||||
) : PaymentAccountStatusDM
|
||||
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
|
||||
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
||||
@NameLabel("card_issue_failed")
|
||||
data class CardIssueFailed(
|
||||
@Json(name = "card_issue_failed") val marker: Boolean = true,
|
||||
) : PaymentAccountStatusDM
|
||||
@Json(name = "customer_id") val customerId: String,
|
||||
) : PaymentAccountStatusValueDM
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class FiatBalanceDM(
|
||||
@Json(name = "available_balance") val availableBalance: BigDecimal,
|
||||
@Json(name = "currency") val currency: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CryptoBalanceDM(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "chain_id") val chainId: Long,
|
||||
@Json(name = "deposit_address") val depositAddress: String,
|
||||
@Json(name = "token_contract_address") val tokenContractAddress: String,
|
||||
@Json(name = "balance") val balance: BigDecimal,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance.
|
||||
|
|
@ -17,7 +17,7 @@ class WireMockRedirectInterceptor : Interceptor {
|
|||
|
||||
if (url.contains(WIREMOCK_REMOTE_URL)) {
|
||||
val newUrl = url.replace(WIREMOCK_REMOTE_URL, override.trimEnd('/'))
|
||||
Timber.d("WireMockRedirect: $url -> $newUrl")
|
||||
TangemLogger.d("WireMockRedirect: $url -> $newUrl")
|
||||
val newRequest = request.newBuilder()
|
||||
.url(newUrl)
|
||||
.build()
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ import com.google.common.truth.Truth
|
|||
import com.tangem.datasource.api.common.AuthProvider
|
||||
import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
||||
import com.tangem.utils.ProviderSuspend
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -38,7 +38,7 @@ class ApiConfigTest {
|
|||
// Actual
|
||||
val actual = allBaseUrls.all { it.endsWith("/") }
|
||||
|
||||
Timber.e(allBaseUrls.joinToString(separator = "\n"))
|
||||
TangemLogger.e(allBaseUrls.joinToString(separator = "\n"))
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isTrue()
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
implementation(deps.material)
|
||||
implementation(deps.reKotlin)
|
||||
implementation(deps.timber)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.core.navigation.deeplink
|
||||
|
||||
/**
|
||||
* Routes in-app content links to the appropriate destination.
|
||||
* - Deep link schemes (e.g. `tangem://`, `wc://`) → handled in-app
|
||||
* - Web URLs (`https://`) → opened in browser
|
||||
*/
|
||||
interface DeeplinkLauncher {
|
||||
|
||||
fun launch(link: String)
|
||||
}
|
||||
|
|
@ -12,7 +12,6 @@ dependencies {
|
|||
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(deps.timber)
|
||||
|
||||
// region Firebase libraries
|
||||
implementation(platform(deps.firebase.bom))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import androidx.annotation.PluralsRes
|
|||
import androidx.annotation.StringRes
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.tangem.utils.SupportedLanguages
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
|
||||
/**
|
||||
* Get a string resource safely or the resource name if an exception is thrown
|
||||
|
|
@ -87,7 +87,7 @@ private fun reportIssue(throwable: Throwable, resources: Resources, id: Int, var
|
|||
"\terror message: ${throwable.message.orEmpty()}\n",
|
||||
)
|
||||
|
||||
Timber.tag("Resources").e(exception)
|
||||
TangemLogger.withTag("Resources").e("Error", exception)
|
||||
|
||||
FirebaseCrashlytics.getInstance().recordException(exception)
|
||||
}
|
||||
|
|
@ -474,6 +474,18 @@
|
|||
<string name="domain_receive_assets_onboarding_description">Sending assets in other networks will result in permanent loss.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%s network</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Send funds using only</string>
|
||||
<string name="dynamic_addresses">Dynamic addresses</string>
|
||||
<string name="dynamic_addresses_enabled_toast_title">Dynamic addresses enabled</string>
|
||||
<string name="dynamic_addresses_enter_features_privacy_description">Use a new address for each transaction to reduce traceability and improve on-chain privacy.</string>
|
||||
<string name="dynamic_addresses_enter_features_privacy_title">Enhanced Privacy</string>
|
||||
<string name="dynamic_addresses_enter_features_receving_description">Easily receive funds in UTXO-based networks with automatic address generation — no manual address management required.</string>
|
||||
<string name="dynamic_addresses_enter_features_receving_title">Seamless receiving</string>
|
||||
<string name="dynamic_addresses_enter_main_button_title">Enable Dynamic Addresses</string>
|
||||
<string name="dynamic_addresses_enter_subtitle">Dynamic addresses create a new one each time for extra privacy — your total balance stays the same. </string>
|
||||
<string name="dynamic_addresses_error_has_custom_token_description">Dynamic Addresses cannot be enabled because some custom addresses/tokens use a modified derivation path, which doesn’t meet the required criteria.</string>
|
||||
<string name="dynamic_addresses_error_has_custom_token_title">Dynamic Addresses Unavailable</string>
|
||||
<string name="dynamic_addresses_error_service_unavailable_description">We can’t connect to the provider right now. Please try again later.</string>
|
||||
<string name="dynamic_addresses_error_service_unavailable_title">Service unavailable. Please try again.</string>
|
||||
<string name="earn_best_opportunities">Best opportunities</string>
|
||||
<string name="earn_clear_filter">Clear filter</string>
|
||||
<string name="earn_empty">The list is temporarily empty as it’s being refreshed. Check back in a moment.</string>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ interface DeviceSecurityInfoProvider {
|
|||
val isRooted: Boolean
|
||||
val isBootloaderUnlocked: Boolean
|
||||
val isXposed: Boolean
|
||||
val isVulnerableToMediaTekExploit: Boolean
|
||||
}
|
||||
|
||||
fun DeviceSecurityInfoProvider.isSecurityExposed(): Boolean = isRooted || isBootloaderUnlocked || isXposed
|
||||
|
|
@ -59,7 +59,6 @@ dependencies {
|
|||
implementation(deps.kotlin.immutable.collections)
|
||||
implementation(deps.zxing.qrCore)
|
||||
api(deps.jodatime)
|
||||
implementation(deps.timber)
|
||||
implementation(deps.markdown)
|
||||
api(deps.haze) {
|
||||
exclude(module = "activity-compose")
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.core.ui.components
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -10,6 +12,7 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.haze.hazeEffectTangem
|
||||
import com.tangem.core.ui.res.LocalPowerSavingState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
import dev.chrisbanes.haze.HazeStyle
|
||||
|
|
@ -61,6 +64,12 @@ fun BottomFade(gradientBrush: Brush, modifier: Modifier = Modifier) {
|
|||
@Composable
|
||||
fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) {
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
val isPowerSavingState by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
|
||||
|
||||
if (isPowerSavingState) {
|
||||
BottomFade(backgroundColor = backgroundColor, modifier = modifier)
|
||||
return
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package com.tangem.core.ui.components
|
|||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.indication
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -20,7 +20,7 @@ import androidx.compose.ui.semantics.Role
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.MarketsTestTags
|
||||
import com.tangem.core.ui.test.SwitchTestTags
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
|
|
@ -47,7 +47,8 @@ fun TangemSwitch(
|
|||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clickable(
|
||||
.toggleable(
|
||||
value = checked,
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple(
|
||||
bounded = false,
|
||||
|
|
@ -55,10 +56,9 @@ fun TangemSwitch(
|
|||
),
|
||||
enabled = enabled,
|
||||
role = Role.Switch,
|
||||
onClick = {
|
||||
onCheckedChange(!checked)
|
||||
},
|
||||
).testTag(MarketsTestTags.ADD_TO_PORTFOLIO_SWITCH),
|
||||
onValueChange = onCheckedChange,
|
||||
)
|
||||
.testTag(SwitchTestTags.SWITCH),
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ 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.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.CircleShimmer
|
||||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.utils.getGreyScaleColorFilter
|
||||
|
||||
/**
|
||||
|
|
@ -147,7 +149,9 @@ private fun BoxScope.ContentIconContainer(
|
|||
|
||||
if (icon.shouldShowCustomBadge) {
|
||||
CurrencyIconBottomBadge(
|
||||
modifier = Modifier.align(Alignment.BottomEnd),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.testTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_S
|
|||
import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode
|
||||
import com.tangem.core.ui.utils.defaultFormat
|
||||
import com.tangem.core.ui.utils.formatWithThousands
|
||||
import timber.log.Timber
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
|
|
@ -78,7 +78,7 @@ class AmountVisualTransformation(
|
|||
currency = formatterCurrency
|
||||
}
|
||||
val formatter = requireNotNull(numberFormatter as? DecimalFormat) {
|
||||
Timber.e("NumberFormat is null")
|
||||
TangemLogger.e("NumberFormat is null")
|
||||
return AnnotatedString(BigDecimalFormatConstants.EMPTY_BALANCE_SIGN)
|
||||
}
|
||||
return buildAnnotatedString {
|
||||
|
|
|
|||
|
|
@ -42,29 +42,30 @@ fun Modifier.hazeEffectTangem(
|
|||
|
||||
return hazeEffect(state, style) {
|
||||
fallbackTint = HazeTint(rootBackground.copy(alpha = 0.5f))
|
||||
if (isGlobalBlurEnabled) {
|
||||
configure()
|
||||
}
|
||||
configure()
|
||||
blurEnabled = blurEnabled && isGlobalBlurEnabled
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a haze foreground effect to the [Modifier] with consideration of power saving mode.
|
||||
* Applies a haze foreground effect to the [Modifier]
|
||||
*
|
||||
* @param style The [HazeStyle] to apply. Defaults to [HazeStyle.Unspecified].
|
||||
* @param isBlurEnabled A Boolean indicating whether blur is enabled. Defaults to true.
|
||||
* @param reactToPowerSavingMode A Boolean indicating whether the haze effect should react to power saving mode.
|
||||
* Defaults to false.
|
||||
* @param configure A lambda to configure the [HazeEffectScope].
|
||||
* @return A [Modifier] with the configured haze foreground effect applied.
|
||||
*/
|
||||
@Composable
|
||||
fun Modifier.hazeForegroundEffectTangem(
|
||||
style: HazeStyle = HazeStyle.Unspecified,
|
||||
reactToPowerSavingMode: Boolean = false,
|
||||
isBlurEnabled: Boolean = true,
|
||||
configure: HazeEffectScope.() -> Unit = {},
|
||||
): Modifier {
|
||||
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
|
||||
val isGlobalBlurEnabled = isBlurEnabled && !powerSavingEnabled.value
|
||||
val isGlobalBlurEnabled = isBlurEnabled && (!reactToPowerSavingMode || !powerSavingEnabled.value)
|
||||
|
||||
return hazeEffect(
|
||||
style = style,
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.isNullOrEmpty
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.ManageTokensScreenTestTags
|
||||
|
||||
@Composable
|
||||
inline fun RowContentContainer(
|
||||
|
|
@ -27,18 +29,21 @@ inline fun RowContentContainer(
|
|||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
content = icon,
|
||||
modifier = Modifier.testTag(ManageTokensScreenTestTags.NETWORK_ICON),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(min = TangemTheme.dimens.size22),
|
||||
.heightIn(min = TangemTheme.dimens.size22)
|
||||
.testTag(ManageTokensScreenTestTags.NETWORK_NAME),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
content = text,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredWidthIn(max = TangemTheme.dimens.size80)
|
||||
.heightIn(min = TangemTheme.dimens.size24),
|
||||
.heightIn(min = TangemTheme.dimens.size24)
|
||||
.testTag(ManageTokensScreenTestTags.SWITCH),
|
||||
contentAlignment = Alignment.CenterEnd,
|
||||
content = action,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ private fun ButtonContent(
|
|||
) {
|
||||
Column {
|
||||
AnimatedVisibility(text != null) {
|
||||
val wrappedText = remember(this) { text.orEmpty() }
|
||||
val wrappedText = remember(this, text) { text.orEmpty() }
|
||||
val textStyle = size.toTextStyle()
|
||||
Text(
|
||||
text = wrappedText.resolveReference(),
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ fun TangemContextMenu(
|
|||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset.Zero,
|
||||
properties: PopupProperties = PopupProperties(focusable = true),
|
||||
positionProvider: PopupPositionProvider? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val expandedStates = remember { MutableTransitionState(false) }
|
||||
|
|
@ -51,7 +52,7 @@ fun TangemContextMenu(
|
|||
if (expandedStates.currentState || expandedStates.targetState) {
|
||||
val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
|
||||
val density = LocalDensity.current
|
||||
val popupPositionProvider = DropdownMenuPositionProvider(
|
||||
val popupPositionProvider = positionProvider ?: DropdownMenuPositionProvider(
|
||||
offset,
|
||||
density,
|
||||
) { parentBounds, menuBounds ->
|
||||
|
|
@ -249,6 +250,57 @@ internal data class DropdownMenuPositionProvider(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [PopupPositionProvider] that centers the popup horizontally on the screen
|
||||
* and positions it below the anchor. If there is not enough space below,
|
||||
* it positions the popup above the anchor. If there is no space in either direction,
|
||||
* it reports the required vertical shift via [onAnchorShiftRequired] so the caller
|
||||
* can move the anchor upward to make room below.
|
||||
*/
|
||||
@Immutable
|
||||
class CenteredContextMenuPositionProvider(
|
||||
private val contentOffset: DpOffset,
|
||||
private val density: Density,
|
||||
private val onAnchorShiftRequired: (Int) -> Unit = {},
|
||||
) : PopupPositionProvider {
|
||||
override fun calculatePosition(
|
||||
anchorBounds: IntRect,
|
||||
windowSize: IntSize,
|
||||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize,
|
||||
): IntOffset {
|
||||
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
|
||||
val x = (windowSize.width - popupContentSize.width) / 2
|
||||
|
||||
val yBelow = anchorBounds.bottom + contentOffsetY
|
||||
val yAbove = anchorBounds.top - contentOffsetY - popupContentSize.height
|
||||
|
||||
val isFitsBelow = yBelow + popupContentSize.height <= windowSize.height
|
||||
val isFitsAbove = yAbove >= 0
|
||||
|
||||
val y = when {
|
||||
isFitsBelow -> {
|
||||
onAnchorShiftRequired(0)
|
||||
yBelow
|
||||
}
|
||||
isFitsAbove -> {
|
||||
onAnchorShiftRequired(0)
|
||||
yAbove
|
||||
}
|
||||
else -> {
|
||||
// Neither fits — calculate how much the anchor must shift up
|
||||
// so the popup fits below. Place popup at bottom edge of screen.
|
||||
val desiredY = windowSize.height - popupContentSize.height
|
||||
val shift = yBelow - desiredY
|
||||
onAnchorShiftRequired(shift)
|
||||
desiredY
|
||||
}
|
||||
}
|
||||
|
||||
return IntOffset(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ fun TangemSearchField(
|
|||
innerTextField = innerTextField,
|
||||
focusManager = focusManager,
|
||||
interactionSource = interactionSource,
|
||||
color = TangemTheme.colors2.field.backgroundDefault,
|
||||
color = TangemTheme.colors2.button.backgroundSecondary,
|
||||
keyboardController = keyboardController,
|
||||
)
|
||||
},
|
||||
|
|
@ -288,10 +288,9 @@ private fun CancelButton(
|
|||
if (state.query.isNotEmpty()) {
|
||||
state.onQueryChange("")
|
||||
}
|
||||
focusManager.clearFocus()
|
||||
keyboardController?.hide()
|
||||
state.onActiveChange(false)
|
||||
state.onClearClick()
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,12 +42,14 @@ data class TangemMessageUM(
|
|||
* @param text TextReference for the button label.
|
||||
* @param type TangemButtonType defining the style type of the button.
|
||||
* @param iconRes Drawable resource ID for the icon to be displayed in the button (optional).
|
||||
* @param isLoading Boolean indicating whether the button should show a loading state.
|
||||
* @param onClick Lambda to be invoked when the button is clicked.
|
||||
*/
|
||||
data class TangemMessageButtonUM(
|
||||
val text: TextReference,
|
||||
val type: TangemButtonType,
|
||||
@DrawableRes val iconRes: Int? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val onClick: () -> Unit,
|
||||
) {
|
||||
/** Creates a TangemButtonUM representation of this message button. */
|
||||
|
|
@ -58,6 +60,7 @@ data class TangemMessageButtonUM(
|
|||
iconRes = iconRes,
|
||||
iconPosition = TangemButtonIconPosition.End,
|
||||
type = type,
|
||||
isLoading = isLoading,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -117,7 +117,6 @@ fun TangemHeaderRow(
|
|||
* @param modifier Modifier for the composable
|
||||
* @param subtitle Optional subtitle as a TextReference
|
||||
* @param headTangemIconUM Optional TangemIconUM for the head icon
|
||||
* @param footerTangemIconRes Optional drawable resource ID for the footer icon
|
||||
* @param isEnabled Boolean indicating if the row is clickable
|
||||
* @param onItemClick Optional click callback for the row
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,18 +1,13 @@
|
|||
package com.tangem.core.ui.ds.row.token
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
|
@ -110,7 +105,7 @@ fun TangemTokenRow(
|
|||
.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -198,36 +193,10 @@ fun TangemTokenRow(
|
|||
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
|
||||
)
|
||||
},
|
||||
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = composed {
|
||||
val hapticFeedback = LocalHapticFeedback.current
|
||||
|
||||
val onClick = tokenRowUM.onItemClick
|
||||
val onLongClick = tokenRowUM.onItemLongClick
|
||||
val onHapticLongClick = if (onLongClick != null) {
|
||||
{
|
||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLongClick()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
when {
|
||||
onClick == null && onLongClick == null -> this
|
||||
onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onHapticLongClick)
|
||||
onClick != null && onLongClick == null -> combinedClickable(onClick = onClick)
|
||||
onClick != null && onLongClick != null -> {
|
||||
combinedClickable(onClick = onClick, onLongClick = onHapticLongClick)
|
||||
}
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui.ds.row.token
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.marketprice.PriceChangeState
|
||||
import com.tangem.core.ui.ds.badge.TangemBadgeUM
|
||||
|
|
@ -11,7 +12,9 @@ import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
|
|||
import com.tangem.core.ui.extensions.TextReference
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
sealed class TangemTokenRowUM : TangemRowUM {
|
||||
|
||||
|
|
@ -43,11 +46,12 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
abstract val onItemClick: (() -> Unit)?
|
||||
|
||||
/** Callback which will be called when an item is long clicked */
|
||||
abstract val onItemLongClick: (() -> Unit)?
|
||||
abstract val onItemLongClick: ((Offset, TangemTokenRowUM) -> Any)?
|
||||
|
||||
/**
|
||||
* Content state of [TangemTokenRowUM]
|
||||
*/
|
||||
@Serializable
|
||||
data class Content(
|
||||
override val id: String,
|
||||
override val headIconUM: TangemIconUM.Currency,
|
||||
|
|
@ -58,12 +62,13 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty,
|
||||
override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty,
|
||||
override val onItemClick: (() -> Unit)?,
|
||||
override val onItemLongClick: (() -> Unit)?,
|
||||
override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Any)?,
|
||||
) : TangemTokenRowUM()
|
||||
|
||||
/**
|
||||
* Loading state of [TangemTokenRowUM]
|
||||
*/
|
||||
@Serializable
|
||||
data class Loading(
|
||||
override val id: String,
|
||||
override val headIconUM: TangemIconUM.Currency = TangemIconUM.Currency(CurrencyIconState.Loading),
|
||||
|
|
@ -75,12 +80,13 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
|
||||
override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty
|
||||
override val onItemClick: (() -> Unit)? = null
|
||||
override val onItemLongClick: (() -> Unit)? = null
|
||||
override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Loading state of [TangemTokenRowUM]
|
||||
*/
|
||||
@Serializable
|
||||
data class Empty(
|
||||
override val id: String,
|
||||
) : TangemTokenRowUM() {
|
||||
|
|
@ -92,12 +98,13 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
|
||||
override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty
|
||||
override val onItemClick: (() -> Unit)? = null
|
||||
override val onItemLongClick: (() -> Unit)? = null
|
||||
override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Actionable state of [TangemTokenRowUM]
|
||||
*/
|
||||
@Serializable
|
||||
data class Actionable(
|
||||
override val id: String,
|
||||
override val headIconUM: TangemIconUM.Currency,
|
||||
|
|
@ -105,16 +112,17 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
override val subtitleUM: SubtitleUM,
|
||||
override val tailUM: TangemRowTailUM,
|
||||
override val onItemClick: (() -> Unit)?,
|
||||
override val onItemLongClick: (() -> Unit)?,
|
||||
override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)?,
|
||||
override val topEndContentUM: EndContentUM = EndContentUM.Empty,
|
||||
override val bottomEndContentUM: EndContentUM = EndContentUM.Empty,
|
||||
) : TangemTokenRowUM() {
|
||||
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
sealed class TitleUM {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
val text: TextReference,
|
||||
val hasPending: Boolean = false,
|
||||
|
|
@ -123,16 +131,20 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
val onBadgeClick: (() -> Unit)? = null,
|
||||
) : TitleUM()
|
||||
|
||||
@Serializable
|
||||
data object Loading : TitleUM()
|
||||
|
||||
@Serializable
|
||||
data object Placeholder : TitleUM()
|
||||
|
||||
@Serializable
|
||||
data object Empty : TitleUM()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
sealed class SubtitleUM {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
val text: TextReference,
|
||||
val isAvailable: Boolean = true,
|
||||
|
|
@ -142,16 +154,20 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
val badge: TangemBadgeUM? = null,
|
||||
) : SubtitleUM()
|
||||
|
||||
@Serializable
|
||||
data object Loading : SubtitleUM()
|
||||
|
||||
@Serializable
|
||||
data object Placeholder : SubtitleUM()
|
||||
|
||||
@Serializable
|
||||
data object Empty : SubtitleUM()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
sealed class EndContentUM {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
val text: TextReference,
|
||||
val isAvailable: Boolean = true,
|
||||
|
|
@ -161,13 +177,17 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
|
||||
) : EndContentUM()
|
||||
|
||||
@Serializable
|
||||
data object Loading : EndContentUM()
|
||||
|
||||
@Serializable
|
||||
data object Placeholder : EndContentUM()
|
||||
|
||||
@Serializable
|
||||
data object Empty : EndContentUM()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@Immutable
|
||||
sealed class PromoBannerUM {
|
||||
data class Content(
|
||||
|
|
@ -184,6 +204,7 @@ sealed class TangemTokenRowUM : TangemRowUM {
|
|||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data object Empty : PromoBannerUM()
|
||||
}
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ object TangemTokenRowPreviewData {
|
|||
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
|
||||
tailUM = TangemRowTailUM.Empty,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onItemLongClick = { _, _ -> },
|
||||
)
|
||||
|
||||
val defaultEllipsisState: TangemTokenRowUM.Content
|
||||
|
|
@ -155,7 +155,7 @@ object TangemTokenRowPreviewData {
|
|||
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
|
||||
tailUM = TangemRowTailUM.Empty,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onItemLongClick = { _, _ -> },
|
||||
)
|
||||
|
||||
val tokenState: TangemTokenRowUM.Content
|
||||
|
|
@ -169,7 +169,7 @@ object TangemTokenRowPreviewData {
|
|||
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
|
||||
tailUM = TangemRowTailUM.Empty,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onItemLongClick = { _, _ -> },
|
||||
)
|
||||
|
||||
val customTokenState: TangemTokenRowUM.Content
|
||||
|
|
@ -183,7 +183,7 @@ object TangemTokenRowPreviewData {
|
|||
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
|
||||
tailUM = TangemRowTailUM.Empty,
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onItemLongClick = { _, _ -> },
|
||||
)
|
||||
|
||||
val draggableState: TangemTokenRowUM.Actionable
|
||||
|
|
@ -194,7 +194,7 @@ object TangemTokenRowPreviewData {
|
|||
subtitleUM = subtitleUM,
|
||||
tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onItemLongClick = { _, _ -> },
|
||||
)
|
||||
|
||||
val draggableStateV2: TangemTokenRowUM.Actionable
|
||||
|
|
@ -207,7 +207,7 @@ object TangemTokenRowPreviewData {
|
|||
bottomEndContentUM = bottomEndContentUM,
|
||||
tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onItemLongClick = { _, _ -> },
|
||||
)
|
||||
|
||||
val loadingState: TangemTokenRowUM.Loading
|
||||
|
|
@ -252,7 +252,7 @@ object TangemTokenRowPreviewData {
|
|||
priceChangeUM = priceChangeState,
|
||||
),
|
||||
onItemClick = {},
|
||||
onItemLongClick = {},
|
||||
onItemLongClick = { _, _ -> },
|
||||
)
|
||||
|
||||
val accountLetterState: TangemTokenRowUM.Content
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui.ds.tabs
|
|||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -13,6 +14,7 @@ import androidx.compose.runtime.*
|
|||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
|
|
@ -52,7 +54,6 @@ fun TangemSegmentedPicker(
|
|||
items = tangemSegmentedPickerUM.items,
|
||||
modifier = modifier,
|
||||
initialSelectedItem = tangemSegmentedPickerUM.initialSelectedItem,
|
||||
hasSeparator = tangemSegmentedPickerUM.hasSeparator,
|
||||
isFixed = tangemSegmentedPickerUM.isFixed,
|
||||
isAltSurface = tangemSegmentedPickerUM.isAltSurface,
|
||||
onClick = onClick,
|
||||
|
|
@ -66,7 +67,6 @@ fun TangemSegmentedPicker(
|
|||
* @param items List of TangemSegmentUM representing the segments in the picker.
|
||||
* @param modifier Modifier to be applied to the segmented picker.
|
||||
* @param initialSelectedItem Optional TangemSegmentUM representing the initially selected segment.
|
||||
* @param hasSeparator Boolean indicating whether there is a separator between segments.
|
||||
* @param isFixed Boolean indicating whether the picker has a fixed width.
|
||||
* @param isAltSurface Boolean indicating whether to use an alternative surface style.
|
||||
* @param onClick Lambda function to be invoked when a segment is clicked.
|
||||
|
|
@ -76,7 +76,6 @@ fun TangemSegmentedPicker(
|
|||
items: ImmutableList<TangemSegmentUM>,
|
||||
modifier: Modifier = Modifier,
|
||||
initialSelectedItem: TangemSegmentUM? = null,
|
||||
hasSeparator: Boolean = false,
|
||||
isFixed: Boolean = false,
|
||||
isAltSurface: Boolean = false,
|
||||
minSegmentWidth: Dp = Dp.Unspecified,
|
||||
|
|
@ -91,11 +90,6 @@ fun TangemSegmentedPicker(
|
|||
val segmentHeight = remember { mutableStateOf(0.dp) }
|
||||
|
||||
val shape = RoundedCornerShape(TangemTheme.dimens2.x25)
|
||||
val spacing = if (hasSeparator) {
|
||||
TangemTheme.dimens2.x4
|
||||
} else {
|
||||
TangemTheme.dimens2.x1
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
|
|
@ -113,12 +107,8 @@ fun TangemSegmentedPicker(
|
|||
itemsWidths = itemsWidths,
|
||||
selectedIndex = selectedIndex.value,
|
||||
segmentHeight = segmentHeight.value,
|
||||
spacing = spacing,
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
items.fastForEachIndexed { index, item ->
|
||||
Segment(
|
||||
item = item,
|
||||
|
|
@ -135,15 +125,34 @@ fun TangemSegmentedPicker(
|
|||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (index != items.lastIndex) {
|
||||
val selected by selectedIndex
|
||||
val alpha by animateFloatAsState(
|
||||
targetValue = if (selected == index || selected == index + 1) 0f else 1f,
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
label = "separatorAlpha",
|
||||
)
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.alpha(alpha)
|
||||
.width(0.5.dp)
|
||||
.height(20.dp)
|
||||
.background(
|
||||
color = TangemTheme.colors2.border.neutral.tertiary.copy(alpha = 0.1f),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SegmentSelection(itemsWidths: SnapshotStateList<Dp>, selectedIndex: Int, segmentHeight: Dp, spacing: Dp) {
|
||||
private fun SegmentSelection(itemsWidths: SnapshotStateList<Dp>, selectedIndex: Int, segmentHeight: Dp) {
|
||||
val indicatorOffset by animateDpAsState(
|
||||
targetValue = itemsWidths.take(selectedIndex).fold(0.dp, Dp::plus) + spacing * selectedIndex,
|
||||
targetValue = itemsWidths.take(selectedIndex).fold(0.dp, Dp::plus),
|
||||
animationSpec = tween(durationMillis = 300),
|
||||
label = "indicatorOffset",
|
||||
)
|
||||
|
|
@ -228,7 +237,6 @@ private fun TangemSegmentedPicker_Preview(@PreviewParameter(PreviewProvider::cla
|
|||
) {
|
||||
TangemSegmentedPicker(
|
||||
isFixed = params.isFixed,
|
||||
hasSeparator = params.hasSeparator,
|
||||
isAltSurface = params.isAltSurface,
|
||||
items = params.items,
|
||||
initialSelectedItem = params.items.last(),
|
||||
|
|
@ -248,25 +256,21 @@ private class PreviewProvider : PreviewParameterProvider<TangemSegmentedPickerUM
|
|||
get() = sequenceOf(
|
||||
TangemSegmentedPickerUM(
|
||||
items = items,
|
||||
hasSeparator = false,
|
||||
isFixed = false,
|
||||
isAltSurface = false,
|
||||
),
|
||||
TangemSegmentedPickerUM(
|
||||
items = items,
|
||||
hasSeparator = true,
|
||||
isFixed = false,
|
||||
isAltSurface = true,
|
||||
),
|
||||
TangemSegmentedPickerUM(
|
||||
items = items,
|
||||
hasSeparator = false,
|
||||
isFixed = true,
|
||||
isAltSurface = false,
|
||||
),
|
||||
TangemSegmentedPickerUM(
|
||||
items = items,
|
||||
hasSeparator = true,
|
||||
isFixed = true,
|
||||
isAltSurface = true,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -8,14 +8,12 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
*
|
||||
* @param items List of TangemSegmentUM representing the segments in the picker.
|
||||
* @param initialSelectedItem Optional TangemSegmentUM representing the initially selected segment.
|
||||
* @param hasSeparator Boolean indicating whether there is a separator between segments.
|
||||
* @param isFixed Boolean indicating whether the picker has a fixed width.
|
||||
* @param isAltSurface Boolean indicating whether to use an alternative surface style.
|
||||
*/
|
||||
data class TangemSegmentedPickerUM(
|
||||
val items: ImmutableList<TangemSegmentUM>,
|
||||
val initialSelectedItem: TangemSegmentUM? = null,
|
||||
val hasSeparator: Boolean = false,
|
||||
val isFixed: Boolean = false,
|
||||
val isAltSurface: Boolean = false,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ fun TangemTopBar(
|
|||
endContent = endContent,
|
||||
content = {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
|
||||
) {
|
||||
|
|
@ -128,47 +129,56 @@ fun TangemTopBar(
|
|||
* A top bar composable that displays a title and optional start and end icons.
|
||||
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev)
|
||||
*
|
||||
* @param modifier Modifier to be applied to the top bar.
|
||||
* @param type Type of the top bar, which determines its size and padding.
|
||||
* @param content Composable content to be displayed in the center of the top bar
|
||||
* @param startContent Optional composable content to be displayed at the start (left) of the top bar.
|
||||
* @param endContent Optional composable content to be displayed at the end (right) of the top bar.
|
||||
* @param modifier Modifier to be applied to the top bar.
|
||||
* @param type Type of the top bar, which determines its size and padding.
|
||||
* @param content Composable content to be displayed in the center of the top bar
|
||||
* @param startContent Optional composable content to be displayed at the start (left) of the top bar.
|
||||
* @param endContent Optional composable content to be displayed at the end (right) of the top bar.
|
||||
* @param reserveSlotSpace If true (default), slot space is always reserved even when start/end content is null,
|
||||
* ensuring centered alignment of [content]. Set to false for edge-to-edge content like
|
||||
* search fields, where empty slots should not consume space.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemTopBar(
|
||||
modifier: Modifier = Modifier,
|
||||
type: TangemTopBarType = TangemTopBarType.Default,
|
||||
content: @Composable () -> Unit,
|
||||
reserveSlotSpace: Boolean = true,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
startContent: @Composable (() -> Unit)? = null,
|
||||
endContent: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = type.getSize())
|
||||
.padding(type.getPadding()),
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = startContent != null,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x11),
|
||||
label = "Start Content Visibility",
|
||||
) { isVisible ->
|
||||
if (isVisible) {
|
||||
startContent?.invoke()
|
||||
if (reserveSlotSpace || startContent != null) {
|
||||
AnimatedContent(
|
||||
targetState = startContent != null,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x11),
|
||||
label = "Start Content Visibility",
|
||||
) { isVisible ->
|
||||
if (isVisible) {
|
||||
startContent?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content()
|
||||
|
||||
AnimatedContent(
|
||||
targetState = endContent != null,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x11),
|
||||
label = "End Content Visibility",
|
||||
) { isVisible ->
|
||||
if (isVisible) {
|
||||
endContent?.invoke()
|
||||
if (reserveSlotSpace || endContent != null) {
|
||||
AnimatedContent(
|
||||
targetState = endContent != null,
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens2.x11)
|
||||
.widthIn(min = TangemTheme.dimens2.x11),
|
||||
label = "End Content Visibility",
|
||||
) { isVisible ->
|
||||
if (isVisible) {
|
||||
endContent?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import androidx.compose.ui.unit.Dp
|
|||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBapScrollDirection
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBarScrollDirection
|
||||
import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState
|
||||
import com.tangem.core.ui.utils.toPx
|
||||
import kotlin.math.abs
|
||||
|
|
@ -37,6 +37,7 @@ import kotlin.math.absoluteValue
|
|||
*/
|
||||
@Composable
|
||||
fun rememberTangemExitUntilCollapsedScrollBehavior(
|
||||
isTopOverscrollEnabled: Boolean = true,
|
||||
expandedHeight: Dp = -Int.MAX_VALUE.dp,
|
||||
partialCollapsedHeight: Dp = expandedHeight,
|
||||
snapAnimationSpec: AnimationSpec<Float>? = spring(),
|
||||
|
|
@ -45,6 +46,7 @@ fun rememberTangemExitUntilCollapsedScrollBehavior(
|
|||
val topBarState = rememberTangemCollapsingAppBarState(
|
||||
heightOffsetLimit = -expandedHeight.toPx(),
|
||||
partialHeightLimit = partialCollapsedHeight.toPx(),
|
||||
isTopOverscrollEnabled = isTopOverscrollEnabled,
|
||||
)
|
||||
return exitUntilCollapsedScrollBehavior(
|
||||
state = topBarState,
|
||||
|
|
@ -76,7 +78,7 @@ private fun exitUntilCollapsedScrollBehavior(
|
|||
val dy = available.y
|
||||
|
||||
val consume = if (dy < 0) {
|
||||
state.direction = TopBapScrollDirection.Collapsing
|
||||
state.direction = TopBarScrollDirection.Collapsing
|
||||
state.dispatchRawDelta(dy)
|
||||
} else {
|
||||
0f
|
||||
|
|
@ -89,16 +91,57 @@ private fun exitUntilCollapsedScrollBehavior(
|
|||
val dy = available.y
|
||||
|
||||
val consume = if (dy > 0) {
|
||||
state.direction = TopBapScrollDirection.Expanding
|
||||
state.direction = TopBarScrollDirection.Expanding
|
||||
state.dispatchRawDelta(dy)
|
||||
} else {
|
||||
state.direction = TopBapScrollDirection.Collapsing
|
||||
state.direction = TopBarScrollDirection.Collapsing
|
||||
0f
|
||||
}
|
||||
|
||||
return Offset(0f, consume)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
val vy = available.y
|
||||
// Only handle upward fling (collapsing)
|
||||
if (vy >= 0f) return Velocity.Zero
|
||||
|
||||
val effectiveLimit = if (state.isTopOverscrollEnabled) {
|
||||
state.heightOffsetLimit + state.partialHeightLimit
|
||||
} else {
|
||||
state.heightOffsetLimit
|
||||
}
|
||||
|
||||
// Already at the collapse limit — nothing to consume
|
||||
if (state.heightOffset <= effectiveLimit) return Velocity.Zero
|
||||
|
||||
state.direction = TopBarScrollDirection.Collapsing
|
||||
var remainingVelocity = vy
|
||||
|
||||
if (flingAnimationSpec != null) {
|
||||
var lastValue = 0f
|
||||
AnimationState(
|
||||
initialValue = 0f,
|
||||
initialVelocity = vy,
|
||||
).animateDecay(flingAnimationSpec) {
|
||||
val delta = value - lastValue
|
||||
val prevOffset = state.heightOffset
|
||||
state.heightOffset =
|
||||
(prevOffset + delta).coerceAtLeast(effectiveLimit)
|
||||
val consumed = abs(prevOffset - state.heightOffset)
|
||||
lastValue = value
|
||||
remainingVelocity = this.velocity
|
||||
// Stop when the bar can't collapse any further
|
||||
if (consumed < 0.5f && abs(delta) > 0.5f) {
|
||||
cancelAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Velocity(0f, available.y - remainingVelocity)
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
val superConsumed = super.onPostFling(consumed, available)
|
||||
return superConsumed + settleAppBar(
|
||||
|
|
@ -179,7 +222,7 @@ private suspend fun settleAppBar(
|
|||
|
||||
val availableDelta = partialLimit - initialHeightOffset
|
||||
|
||||
state.heightOffset = if (delta < 0f && initialHeightOffset > partialLimit) {
|
||||
state.heightOffset = if (delta < 0f && initialHeightOffset >= partialLimit) {
|
||||
(initialHeightOffset + delta).coerceAtLeast(partialLimit)
|
||||
} else {
|
||||
initialHeightOffset + delta
|
||||
|
|
@ -196,17 +239,17 @@ private suspend fun settleAppBar(
|
|||
if (snapAnimationSpec != null && state.heightOffset > partialLimit && state.heightOffset < 0f) {
|
||||
AnimationState(initialValue = state.heightOffset).animateTo(
|
||||
when (state.direction) {
|
||||
TopBapScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) {
|
||||
TopBarScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) {
|
||||
partialLimit
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
TopBapScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) {
|
||||
TopBarScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) {
|
||||
0f
|
||||
} else {
|
||||
partialLimit
|
||||
}
|
||||
TopBapScrollDirection.Idle -> 0f
|
||||
TopBarScrollDirection.Idle -> 0f
|
||||
},
|
||||
animationSpec = snapAnimationSpec,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class TangemCollapsingAppBarState(
|
|||
val initialHeightOffset: Float = 0f,
|
||||
val heightOffsetLimit: Float = 0f,
|
||||
val partialHeightLimit: Float = heightOffsetLimit,
|
||||
var isTopOverscrollEnabled: Boolean = true,
|
||||
) : ScrollableState {
|
||||
|
||||
private val _heightOffset = mutableFloatStateOf(initialHeightOffset)
|
||||
|
|
@ -42,8 +43,7 @@ class TangemCollapsingAppBarState(
|
|||
var heightOffset: Float
|
||||
get() = _heightOffset.floatValue
|
||||
set(newOffset) {
|
||||
_heightOffset.floatValue =
|
||||
newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f)
|
||||
_heightOffset.floatValue = newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -60,11 +60,13 @@ class TangemCollapsingAppBarState(
|
|||
/**
|
||||
* The current scroll direction of the app bar, which can be Collapsing, Expanding, or Idle.
|
||||
*/
|
||||
var direction: TopBapScrollDirection = TopBapScrollDirection.Idle
|
||||
var direction: TopBarScrollDirection = TopBarScrollDirection.Idle
|
||||
|
||||
private val scrollableState = ScrollableState { value ->
|
||||
val effectiveLimit = if (isTopOverscrollEnabled) heightOffsetLimit + partialHeightLimit else heightOffsetLimit
|
||||
val consume = if (value < 0) {
|
||||
max(heightOffsetLimit - heightOffset, value)
|
||||
// Already at or past the effective limit — don't consume collapsing scroll
|
||||
if (heightOffset <= effectiveLimit) 0f else max(effectiveLimit - heightOffset, value)
|
||||
} else {
|
||||
min(0f - heightOffset, value)
|
||||
}
|
||||
|
|
@ -104,12 +106,20 @@ class TangemCollapsingAppBarState(
|
|||
/** The default [Saver] implementation for [TangemCollapsingAppBarState]. */
|
||||
val Saver: Saver<TangemCollapsingAppBarState, *> =
|
||||
listSaver(
|
||||
save = { state -> listOf(state.heightOffsetLimit, state.heightOffset, state.partialHeightLimit) },
|
||||
save = { state ->
|
||||
listOf(
|
||||
state.heightOffsetLimit,
|
||||
state.heightOffset,
|
||||
state.partialHeightLimit,
|
||||
state.isTopOverscrollEnabled,
|
||||
)
|
||||
},
|
||||
restore = { state ->
|
||||
TangemCollapsingAppBarState(
|
||||
heightOffsetLimit = state[0],
|
||||
partialHeightLimit = state[2],
|
||||
initialHeightOffset = state[1],
|
||||
heightOffsetLimit = state[0] as Float,
|
||||
initialHeightOffset = state[1] as Float,
|
||||
partialHeightLimit = state[2] as Float,
|
||||
isTopOverscrollEnabled = state[3] as Boolean,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -121,6 +131,7 @@ class TangemCollapsingAppBarState(
|
|||
*/
|
||||
@Composable
|
||||
fun rememberTangemCollapsingAppBarState(
|
||||
isTopOverscrollEnabled: Boolean = true,
|
||||
heightOffsetLimit: Float = -Float.MAX_VALUE,
|
||||
partialHeightLimit: Float = -Float.MAX_VALUE,
|
||||
initialHeightOffset: Float = 0f,
|
||||
|
|
@ -130,13 +141,16 @@ fun rememberTangemCollapsingAppBarState(
|
|||
initialHeightOffset = initialHeightOffset,
|
||||
partialHeightLimit = partialHeightLimit,
|
||||
heightOffsetLimit = heightOffsetLimit,
|
||||
isTopOverscrollEnabled = isTopOverscrollEnabled,
|
||||
)
|
||||
}.also {
|
||||
it.isTopOverscrollEnabled = isTopOverscrollEnabled
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The scroll direction of the top app bar, which can be Collapsing, Expanding, or Idle.
|
||||
*/
|
||||
enum class TopBapScrollDirection {
|
||||
enum class TopBarScrollDirection {
|
||||
Collapsing, Expanding, Idle
|
||||
}
|
||||
|
|
@ -13,6 +13,22 @@ import com.tangem.core.ui.message.EventMessageAction
|
|||
*/
|
||||
object Dialogs {
|
||||
|
||||
/**
|
||||
* NFC feature is unavailable dialog
|
||||
*/
|
||||
fun nfcFeatureUnavailable(): DialogMessage = DialogMessage(
|
||||
title = resourceReference(id = R.string.common_error),
|
||||
message = resourceReference(R.string.nfc_error_unavailable),
|
||||
)
|
||||
|
||||
/**
|
||||
* Wrong wallet tapped dialog
|
||||
*/
|
||||
fun wrongWalletTapped(): DialogMessage = DialogMessage(
|
||||
title = resourceReference(id = R.string.common_warning),
|
||||
message = resourceReference(id = R.string.error_wrong_wallet_tapped),
|
||||
)
|
||||
|
||||
/**
|
||||
* Card verification failed dialog
|
||||
*
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ fun TangemThemeRedesign(content: @Composable () -> Unit) {
|
|||
LocalTangemColors provides themeColors,
|
||||
LocalTangemColors2 provides if (LocalIsInDarkTheme.current) darkThemeColors2() else lightThemeColors2(),
|
||||
LocalTangemTypography2 provides TangemTypography2(InterFamily),
|
||||
LocalTangemTypography provides TangemTypography(InterFamily),
|
||||
LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) },
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
|
|
@ -93,7 +94,7 @@ private fun lightThemeColors2(): TangemColors2 {
|
|||
neutral = TangemColors2.Border.Neutral(
|
||||
primary = TangemColorPalette.Light3,
|
||||
secondary = TangemColorPalette.Light5,
|
||||
tertiary = TangemColorPalette.Light_10,
|
||||
tertiary = TangemColorPalette.Dark_10,
|
||||
quaternary = TangemColorPalette.Dark_10,
|
||||
),
|
||||
status = TangemColors2.Border.Status(
|
||||
|
|
|
|||
|
|
@ -114,6 +114,18 @@ class TangemTypography2 internal constructor(
|
|||
),
|
||||
)
|
||||
|
||||
val headingSemibold22: TextStyle = TextStyle(
|
||||
fontFamily = fontFamily,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
letterSpacing = TextUnit(value = 0.38f, type = TextUnitType.Sp),
|
||||
lineHeight = TextUnit(value = 28f, type = TextUnitType.Sp),
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
)
|
||||
|
||||
val headingRegular20: TextStyle = TextStyle(
|
||||
fontFamily = fontFamily,
|
||||
fontSize = 20.sp,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object AccountDetailsScreenTestTags {
|
||||
const val MANAGE_TOKENS_BUTTON = "ACCOUNT_DETAILS_SCREEN_MANAGE_TOKENS_BUTTON"
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object ManageTokensScreenTestTags {
|
||||
const val TOKEN_ITEM = "MANAGE_TOKENS_SCREEN_TOKEN_ITEM"
|
||||
const val NETWORK_ICON = "MANAGE_TOKENS_SCREEN_NETWORK_ICON"
|
||||
const val NETWORK_NAME = "MANAGE_TOKENS_SCREEN_NETWORK_NAME"
|
||||
const val SWITCH = "MANAGE_TOKENS_SCREEN_SWITCH"
|
||||
}
|
||||
|
|
@ -3,6 +3,5 @@ package com.tangem.core.ui.test
|
|||
object MarketsTestTags {
|
||||
const val TOKENS_LIST = "MARKETS_TOKENS_LIST"
|
||||
const val TOKENS_LIST_ITEM = "MARKETS_TOKENS_LIST_ITEM"
|
||||
const val ADD_TO_PORTFOLIO_SWITCH = "MARKETS_ADD_TO_PORTFOLIO_SWITCH"
|
||||
const val LISTED_ON_EXCHANGES_COUNT = "MARKETS_LISTED_ON_EXCHANGES_COUNT"
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ object SwapTokenScreenTestTags {
|
|||
const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON"
|
||||
const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON"
|
||||
const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT"
|
||||
const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT"
|
||||
const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON"
|
||||
const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object SwitchTestTags {
|
||||
const val SWITCH = "SWITCH"
|
||||
}
|
||||
|
|
@ -9,4 +9,5 @@ object TokenElementsTestTags {
|
|||
const val TOKEN_CRYPTO_AMOUNT = "TOKEN_CRYPTO_AMOUNT"
|
||||
const val TOKEN_NON_FIAT_BLOCK = "TOKEN_NON_FIAT_BLOCK"
|
||||
const val TOKEN_YIELD_PROMO_BANNER = "TOKEN_YIELD_PROMO_BANNER"
|
||||
const val TOKEN_CUSTOM_DERIVATION_ICON = "TOKEN_CUSTOM_DERIVATION_ICON"
|
||||
}
|
||||
|
|
@ -3,4 +3,5 @@ package com.tangem.core.ui.test
|
|||
object WalletSettingsScreenTestTags {
|
||||
const val SCREEN_CONTAINER = "WALLET_SETTINGS_SCREEN_CONTAINER"
|
||||
const val SCREEN_ITEM = "WALLET_SETTINGS_SCREEN_ITEM"
|
||||
const val USER_ACCOUNT_ITEM = "WALLET_SETTINGS_USER_ACCOUNT_ITEM"
|
||||
}
|
||||
9
core/ui/src/main/res/drawable/ic_fixed.xml
Normal file
9
core/ui/src/main/res/drawable/ic_fixed.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="10dp"
|
||||
android:height="10dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="10">
|
||||
<path
|
||||
android:pathData="M7.834,5.222C7.834,4.946 7.61,4.722 7.334,4.722H2.667C2.391,4.722 2.167,4.946 2.167,5.222V7.834C2.167,8.11 2.391,8.334 2.667,8.334H7.334C7.61,8.333 7.834,8.11 7.834,7.834V5.222ZM6.444,3.111C6.444,2.313 5.798,1.667 5,1.667C4.202,1.667 3.556,2.313 3.556,3.111V3.722H6.444V3.111ZM7.444,3.727C8.221,3.784 8.834,4.431 8.834,5.222V7.834C8.834,8.662 8.162,9.333 7.334,9.334H2.667C1.839,9.334 1.167,8.662 1.167,7.834V5.222C1.167,4.431 1.779,3.784 2.556,3.727V3.111C2.556,1.761 3.65,0.667 5,0.667C6.35,0.667 7.444,1.761 7.444,3.111V3.727Z"
|
||||
android:fillColor="#919191"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_fixed_32.xml
Normal file
9
core/ui/src/main/res/drawable/ic_fixed_32.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M9.348,29.329H22.648C23.551,29.329 24.222,29.09 24.66,28.612C25.107,28.143 25.33,27.406 25.33,26.402V15.869C25.33,14.874 25.107,14.143 24.66,13.674C24.222,13.196 23.551,12.956 22.648,12.956H9.628C8.734,12.956 8.016,13.196 7.476,13.674C6.936,14.152 6.666,14.884 6.666,15.869V26.402C6.666,27.406 6.89,28.143 7.337,28.612C7.784,29.09 8.454,29.329 9.348,29.329ZM8.887,13.918H11.137V9.053C11.137,7.848 11.36,6.839 11.807,6.025C12.254,5.203 12.845,4.586 13.581,4.174C14.326,3.753 15.132,3.543 15.998,3.543C16.874,3.543 17.679,3.753 18.415,4.174C19.151,4.586 19.742,5.203 20.189,6.025C20.636,6.839 20.86,7.848 20.86,9.053V13.918H23.109V9.355C23.109,8.006 22.913,6.829 22.522,5.825C22.131,4.82 21.6,3.988 20.93,3.328C20.259,2.658 19.495,2.161 18.638,1.835C17.791,1.501 16.911,1.333 15.998,1.333C15.085,1.333 14.205,1.501 13.358,1.835C12.51,2.161 11.751,2.658 11.081,3.328C10.41,3.988 9.874,4.82 9.474,5.825C9.083,6.829 8.887,8.006 8.887,9.355V13.918Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
</vector>
|
||||
13
core/ui/src/main/res/drawable/ic_floating.xml
Normal file
13
core/ui/src/main/res/drawable/ic_floating.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="10dp"
|
||||
android:height="11dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="11">
|
||||
<path
|
||||
android:pathData="M3.639,4.008C4.256,3.907 4.849,4.154 5.411,4.716C5.825,5.129 6.112,5.177 6.314,5.144C6.562,5.103 6.848,4.92 7.198,4.569C7.393,4.374 7.71,4.374 7.905,4.569C8.1,4.765 8.1,5.081 7.905,5.276C7.522,5.659 7.048,6.037 6.476,6.131C5.859,6.231 5.266,5.984 4.704,5.423C4.29,5.009 4.003,4.962 3.8,4.995C3.552,5.036 3.267,5.219 2.916,5.569C2.721,5.764 2.404,5.764 2.209,5.569C2.014,5.374 2.014,5.057 2.209,4.862C2.592,4.479 3.066,4.101 3.639,4.008Z"
|
||||
android:fillColor="#919191"/>
|
||||
<path
|
||||
android:pathData="M5,0C7.761,0 10,2.239 10,5C10,7.761 7.761,10 5,10C2.239,10 0,7.761 0,5C0,2.239 2.239,0 5,0ZM5,1C2.791,1 1,2.791 1,5C1,7.209 2.791,9 5,9C7.209,9 9,7.209 9,5C9,2.791 7.209,1 5,1Z"
|
||||
android:fillColor="#919191"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_floating_32.xml
Normal file
9
core/ui/src/main/res/drawable/ic_floating_32.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M16,2C23.732,2 30,8.268 30,16C30,23.732 23.732,30 16,30C8.268,30 2,23.732 2,16C2,8.268 8.268,2 16,2ZM17.042,15.184C15.549,13.691 13.972,13.035 12.332,13.303C10.812,13.551 9.551,14.556 8.533,15.573C8.015,16.092 8.015,16.933 8.533,17.452C9.052,17.97 9.894,17.97 10.412,17.452C11.344,16.521 12.103,16.033 12.761,15.926C13.3,15.838 14.064,15.963 15.163,17.063C16.656,18.555 18.232,19.212 19.871,18.944C21.391,18.696 22.652,17.691 23.67,16.674C24.188,16.155 24.188,15.314 23.67,14.795C23.151,14.276 22.31,14.276 21.791,14.795C20.86,15.726 20.1,16.213 19.442,16.32C18.904,16.408 18.14,16.282 17.042,15.184Z"
|
||||
android:fillColor="#0099FF"/>
|
||||
</vector>
|
||||
9
core/ui/src/main/res/drawable/ic_return_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_return_24.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M20,5C20.552,5 21,5.448 21,6V9.596C21,12.374 18.735,14.62 15.956,14.596L7.29,14.519L10.445,17.808C10.827,18.206 10.814,18.839 10.416,19.222C10.017,19.604 9.384,19.591 9.002,19.192L4.33,14.323C4.134,14.118 4.028,13.863 4.005,13.608L4,13.5L4.005,13.392C4.028,13.137 4.134,12.882 4.33,12.677L9.002,7.808C9.384,7.409 10.017,7.396 10.416,7.778C10.814,8.161 10.827,8.794 10.445,9.192L7.252,12.519L15.974,12.596C17.641,12.61 19,11.263 19,9.596V6C19,5.448 19.448,5 20,5Z"
|
||||
android:fillColor="#919191"/>
|
||||
</vector>
|
||||
BIN
core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp
Normal file
BIN
core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
|
|
@ -23,6 +23,10 @@ dependencies {
|
|||
implementation(deps.jodatime)
|
||||
// endregion
|
||||
|
||||
// region Logging
|
||||
implementation(deps.kermit)
|
||||
// endregion
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.utils.coroutines
|
||||
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -16,11 +15,7 @@ object FeatureCoroutineExceptionHandler {
|
|||
val sw = StringWriter()
|
||||
throwable.printStackTrace(PrintWriter(sw))
|
||||
val exceptionAsString: String = sw.toString()
|
||||
// it delegates logging to android Logger, cause cant use timber in java module
|
||||
Logger.getLogger("CoroutineExceptHandler").log(
|
||||
Level.INFO,
|
||||
"CoroutineException: from: $from, exception: $exceptionAsString",
|
||||
)
|
||||
TangemLogger.i("CoroutineException: from: $from, exception: $exceptionAsString")
|
||||
throw throwable
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package com.tangem.utils.logging
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
/**
|
||||
* Application-level logger that wraps Kermit [Logger] with the same API.
|
||||
* All modules should use [TangemLogger] instead of importing Kermit directly.
|
||||
*/
|
||||
object TangemLogger {
|
||||
|
||||
fun v(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.v(messageString, throwable)
|
||||
}
|
||||
|
||||
fun d(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.d(messageString, throwable)
|
||||
}
|
||||
|
||||
fun i(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.i(messageString, throwable)
|
||||
}
|
||||
|
||||
fun w(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.w(messageString, throwable)
|
||||
}
|
||||
|
||||
fun e(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.e(messageString, throwable)
|
||||
}
|
||||
|
||||
fun a(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.a(messageString, throwable)
|
||||
}
|
||||
|
||||
fun withTag(tag: String): TaggedLogger = TaggedLogger(tag)
|
||||
|
||||
class TaggedLogger internal constructor(private val tag: String) {
|
||||
|
||||
fun v(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.withTag(tag).v(messageString, throwable)
|
||||
}
|
||||
|
||||
fun d(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.withTag(tag).d(messageString, throwable)
|
||||
}
|
||||
|
||||
fun i(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.withTag(tag).i(messageString, throwable)
|
||||
}
|
||||
|
||||
fun w(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.withTag(tag).w(messageString, throwable)
|
||||
}
|
||||
|
||||
fun e(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.withTag(tag).e(messageString, throwable)
|
||||
}
|
||||
|
||||
fun a(messageString: String, throwable: Throwable? = null) {
|
||||
Logger.withTag(tag).a(messageString, throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue