Updated on 2026-08-14
This commit is contained in:
commit
ea661f55ab
250 changed files with 6169 additions and 949 deletions
|
|
@ -13,5 +13,7 @@ dependencies {
|
|||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core shouldn't depends on core, but in case with utils and logging its necessary */
|
||||
implementation(project(":core:utils"))
|
||||
implementation(projects.core.utils)
|
||||
|
||||
implementation(projects.core.analytics.models)
|
||||
}
|
||||
1
core/analytics/models/.gitignore
vendored
Normal file
1
core/analytics/models/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
4
core/analytics/models/build.gradle.kts
Normal file
4
core/analytics/models/build.gradle.kts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
plugins {
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
id("configuration")
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.core.analytics
|
||||
package com.tangem.core.analytics.models
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -1,18 +1,9 @@
|
|||
package com.tangem.core.analytics
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventFilter
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsFilterHolder
|
||||
import com.tangem.core.analytics.api.AnalyticsHandler
|
||||
import com.tangem.core.analytics.api.AnalyticsHandlerHolder
|
||||
import com.tangem.core.analytics.api.ParamsInterceptor
|
||||
import com.tangem.core.analytics.api.ParamsInterceptorHolder
|
||||
import com.tangem.core.analytics.api.*
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.core.analytics.api
|
||||
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.core.analytics.api
|
||||
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.core.analytics.api
|
||||
|
||||
import com.tangem.core.analytics.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ dependencies {
|
|||
implementation(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.okHttp)
|
||||
implementation(deps.okHttp.logging)
|
||||
implementation(deps.okHttp.prettyLogging)
|
||||
implementation(deps.retrofit)
|
||||
implementation(deps.retrofit.moshi)
|
||||
implementation(deps.reactive.network)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.datasource.api.common
|
||||
|
||||
import android.util.Log
|
||||
import com.ihsanbal.logging.Level
|
||||
import com.ihsanbal.logging.LoggingInterceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
|
@ -22,7 +24,7 @@ fun createRetrofitInstance(
|
|||
}
|
||||
interceptors.forEach { okHttpBuilder.addInterceptor(it) }
|
||||
|
||||
if (logEnabled) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor())
|
||||
if (logEnabled) okHttpBuilder.addInterceptor(createNetworkLoggingInterceptor())
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
|
|
@ -31,6 +33,9 @@ fun createRetrofitInstance(
|
|||
.build()
|
||||
}
|
||||
|
||||
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
fun createNetworkLoggingInterceptor(): Interceptor {
|
||||
return LoggingInterceptor.Builder()
|
||||
.setLevel(Level.BODY)
|
||||
.log(Log.VERBOSE)
|
||||
.build()
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.cache.CacheKeysStore
|
||||
import com.tangem.datasource.local.cache.RuntimeCacheKeysStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CacheKeysStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCacheKeysStore(): CacheKeysStore {
|
||||
return RuntimeCacheKeysStore()
|
||||
}
|
||||
}
|
||||
|
|
@ -63,9 +63,7 @@ class NetworkModule {
|
|||
@PromotionOneInch
|
||||
fun providePromotionOneInchApi(authProvider: AuthProvider, @NetworkMoshi moshi: Moshi): PromotionApi {
|
||||
val okClient = OkHttpClient.Builder()
|
||||
.addHeaders(
|
||||
AuthenticationHeader(authProvider),
|
||||
)
|
||||
.addHeaders(AuthenticationHeader(authProvider))
|
||||
.allowLogging()
|
||||
.callTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
.connectTimeout(API_ONE_INCH_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
||||
|
|
|
|||
14
core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt
vendored
Normal file
14
core/datasource/src/main/java/com/tangem/datasource/local/cache/CacheKeysStore.kt
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.datasource.local.cache
|
||||
|
||||
import com.tangem.datasource.local.cache.model.CacheKey
|
||||
|
||||
interface CacheKeysStore {
|
||||
|
||||
fun get(id: String): CacheKey?
|
||||
|
||||
fun addOrReplace(key: CacheKey)
|
||||
|
||||
fun remove(id: String)
|
||||
|
||||
fun clear()
|
||||
}
|
||||
25
core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt
vendored
Normal file
25
core/datasource/src/main/java/com/tangem/datasource/local/cache/RuntimeCacheKeysStore.kt
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.local.cache
|
||||
|
||||
import com.tangem.datasource.local.cache.model.CacheKey
|
||||
import com.tangem.datasource.local.store.RuntimeStore
|
||||
|
||||
internal class RuntimeCacheKeysStore : CacheKeysStore {
|
||||
|
||||
private val store = RuntimeStore(keyProvider = CacheKey::id)
|
||||
|
||||
override fun get(id: String): CacheKey? {
|
||||
return store.getSync { it.id == id }.firstOrNull()
|
||||
}
|
||||
|
||||
override fun addOrReplace(key: CacheKey) {
|
||||
store.addOrReplace(key)
|
||||
}
|
||||
|
||||
override fun remove(id: String) {
|
||||
store.remove { it.id == id }
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
store.clear()
|
||||
}
|
||||
}
|
||||
10
core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt
vendored
Normal file
10
core/datasource/src/main/java/com/tangem/datasource/local/cache/model/CacheKey.kt
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.local.cache.model
|
||||
|
||||
import org.joda.time.Duration
|
||||
import org.joda.time.LocalDateTime
|
||||
|
||||
data class CacheKey(
|
||||
val id: String,
|
||||
val updatedAt: LocalDateTime,
|
||||
val expiresIn: Duration,
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.datasource.local.store
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
internal class RuntimeStore<Key, Data>(private val keyProvider: (Data) -> Key) {
|
||||
|
||||
private val store = MutableStateFlow<HashMap<Key, Data>>(hashMapOf())
|
||||
|
||||
fun get(selector: (Data) -> Boolean = { true }): Flow<List<Data>> {
|
||||
return store.map { getInternal(it, selector) }
|
||||
}
|
||||
|
||||
fun getSync(selector: (Data) -> Boolean = { true }): List<Data> {
|
||||
return getInternal(store.value, selector)
|
||||
}
|
||||
|
||||
// TODO: Uncomment if needed
|
||||
// fun addOrReplace(items: Collection<Data>) {
|
||||
// if (items.isEmpty()) return
|
||||
//
|
||||
// val storeValue = store.value
|
||||
//
|
||||
// items.forEach { item ->
|
||||
// storeValue[keyProvider(item)] = item
|
||||
// }
|
||||
//
|
||||
// store.value = storeValue
|
||||
// }
|
||||
|
||||
fun addOrReplace(item: Data) {
|
||||
val storeValue = store.value
|
||||
storeValue[keyProvider(item)] = item
|
||||
|
||||
store.value = storeValue
|
||||
}
|
||||
|
||||
fun remove(selector: (Data) -> Boolean) {
|
||||
val storeValue = store.value
|
||||
|
||||
storeValue.forEach { (key, item) ->
|
||||
if (selector(item)) {
|
||||
storeValue.remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
store.value = storeValue
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
store.update { hashMapOf() }
|
||||
}
|
||||
|
||||
private fun getInternal(store: HashMap<Key, Data>, selector: (Data) -> Boolean): List<Data> {
|
||||
return store.values.filter { selector(it) }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
package com.tangem.datasource.utils
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import okhttp3.logging.HttpLoggingInterceptor.Level
|
||||
|
||||
/** Extension for adding headers [requestHeaders] to every [OkHttpClient] request */
|
||||
internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeader): OkHttpClient.Builder {
|
||||
|
|
@ -26,5 +25,10 @@ internal fun OkHttpClient.Builder.addHeaders(vararg requestHeaders: RequestHeade
|
|||
*
|
||||
* @param level logging level. By default, only the request body.
|
||||
*/
|
||||
internal fun OkHttpClient.Builder.allowLogging(level: Level = Level.BODY): OkHttpClient.Builder =
|
||||
if (BuildConfig.DEBUG) addInterceptor(interceptor = HttpLoggingInterceptor().setLevel(level)) else this
|
||||
internal fun OkHttpClient.Builder.allowLogging(): OkHttpClient.Builder {
|
||||
return if (BuildConfig.DEBUG) {
|
||||
addInterceptor(interceptor = createNetworkLoggingInterceptor())
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
|
@ -230,7 +230,12 @@ private fun PrimaryButtonSample() {
|
|||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
PrimaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { })
|
||||
PrimaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { })
|
||||
PrimaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showProgress = true,
|
||||
text = "Manage tokens",
|
||||
onClick = { },
|
||||
)
|
||||
PrimaryButtonIconEnd(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
|
|
@ -289,7 +294,12 @@ private fun SecondaryButtonSample() {
|
|||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
SecondaryButton(modifier = Modifier.fillMaxWidth(), text = "Manage tokens", onClick = { })
|
||||
SecondaryButton(modifier = Modifier.fillMaxWidth(), showProgress = true, text = "Manage tokens", onClick = { })
|
||||
SecondaryButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
showProgress = true,
|
||||
text = "Manage tokens",
|
||||
onClick = { },
|
||||
)
|
||||
SecondaryButtonIconEnd(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = "Manage tokens",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* [Show in Figma](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=68%3A20&mode=design&t=WSV3AxC6zV1y0CHF-1)
|
||||
* */
|
||||
@Composable
|
||||
fun Notifier(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
textColor: Color = TangemTheme.colors.text.primary1,
|
||||
backgroundColor: Color = TangemTheme.colors.icon.inactive,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.heightIn(TangemTheme.dimens.size32)
|
||||
.background(
|
||||
color = backgroundColor,
|
||||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.padding(horizontal = TangemTheme.dimens.size10),
|
||||
text = text,
|
||||
color = textColor,
|
||||
style = TangemTheme.typography.button,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TangemNotifierPreview_Light(@PreviewParameter(NotifierProvider::class) text: String) {
|
||||
TangemTheme(isDark = false) {
|
||||
Notifier(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TangemNotifierPreview_Dark(@PreviewParameter(NotifierProvider::class) text: String) {
|
||||
TangemTheme(isDark = true) {
|
||||
Notifier(text = text)
|
||||
}
|
||||
}
|
||||
|
||||
private class NotifierProvider : CollectionPreviewParameterProvider<String>(
|
||||
collection = listOf(
|
||||
"a",
|
||||
"Card 1 of 2",
|
||||
"Card 1 of 2 or many other",
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
package com.tangem.core.ui.components.buttons
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButton
|
||||
import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Composable
|
||||
fun HorizontalActionChips(
|
||||
buttons: ImmutableList<ActionButtonConfig>,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens.spacing0),
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
items(
|
||||
items = buttons,
|
||||
key = { it.text },
|
||||
itemContent = { ActionButton(config = it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_HorizontalActionChips_Light(
|
||||
@PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList<ActionButtonConfig>,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
HorizontalActionChips(buttons = buttons)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_HorizontalActionChips_Dark(
|
||||
@PreviewParameter(ActionButtonConfigProvider::class) buttons: ImmutableList<ActionButtonConfig>,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
HorizontalActionChips(buttons = buttons)
|
||||
}
|
||||
}
|
||||
|
||||
private class ActionButtonConfigProvider : CollectionPreviewParameterProvider<ActionButtonConfig>(
|
||||
collection = persistentListOf(
|
||||
ActionButtonConfig(
|
||||
text = "Buy",
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = "Send",
|
||||
iconResId = R.drawable.ic_arrow_up_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = "Receive",
|
||||
iconResId = R.drawable.ic_arrow_down_24,
|
||||
onClick = {},
|
||||
),
|
||||
ActionButtonConfig(
|
||||
text = "Exchange",
|
||||
iconResId = R.drawable.ic_exchange_vertical_24,
|
||||
onClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -30,7 +30,7 @@ internal fun TangemButton(
|
|||
elevation = elevation,
|
||||
shape = size.toShape(),
|
||||
colors = colors,
|
||||
contentPadding = size.toContentPadding(icon = icon),
|
||||
contentPadding = if (showProgress) ButtonDefaults.ContentPadding else size.toContentPadding(icon = icon),
|
||||
) {
|
||||
ButtonContent(
|
||||
text = text,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
package com.tangem.core.ui.components.marketprice
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.RectangleShimmer
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* @see <a href =
|
||||
* "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=1217-922&mode=design&t=t1PxyisyGJSwzQwg-4"
|
||||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun MarketPriceBlock(state: MarketPriceBlockState, modifier: Modifier = Modifier) {
|
||||
var rootWidth by remember { mutableStateOf(value = 0) }
|
||||
Column(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.primary,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
|
||||
)
|
||||
.heightIn(min = TangemTheme.dimens.size70)
|
||||
.fillMaxWidth()
|
||||
.padding(all = TangemTheme.dimens.spacing14)
|
||||
.onSizeChanged { rootWidth = it.width },
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_marketplace_block_title, state.currencyName),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
)
|
||||
|
||||
when (state) {
|
||||
is MarketPriceBlockState.Loading -> {
|
||||
RectangleShimmer(
|
||||
modifier = Modifier.size(width = TangemTheme.dimens.size158, height = TangemTheme.dimens.size20),
|
||||
)
|
||||
}
|
||||
is MarketPriceBlockState.Content -> {
|
||||
Price(
|
||||
config = state,
|
||||
priceWidthDp = with(LocalDensity.current) { rootWidth.div(other = 2).toDp() },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Price(config: MarketPriceBlockState.Content, priceWidthDp: Dp) {
|
||||
Row(
|
||||
modifier = Modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
Text(
|
||||
text = config.price,
|
||||
modifier = Modifier.widthIn(max = priceWidthDp),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
|
||||
PriceChangeInPercent(config.priceChangeConfig)
|
||||
|
||||
Text(
|
||||
text = stringResource(id = R.string.wallet_marketprice_block_update_time),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PriceChangeInPercent(config: PriceChangeConfig) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4),
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(
|
||||
id = when (config.type) {
|
||||
PriceChangeConfig.Type.UP -> R.drawable.img_arrow_up_8
|
||||
PriceChangeConfig.Type.DOWN -> R.drawable.img_arrow_down_8
|
||||
},
|
||||
),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = config.valueInPercent,
|
||||
color = when (config.type) {
|
||||
PriceChangeConfig.Type.UP -> TangemTheme.colors.text.accent
|
||||
PriceChangeConfig.Type.DOWN -> TangemTheme.colors.text.warning
|
||||
},
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_MarketPriceBlock_Light(
|
||||
@PreviewParameter(WalletMarketPriceBlockStateProvider::class)
|
||||
state: MarketPriceBlockState,
|
||||
) {
|
||||
TangemTheme(isDark = false) {
|
||||
MarketPriceBlock(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_MarketPriceBlock_Dark(
|
||||
@PreviewParameter(WalletMarketPriceBlockStateProvider::class)
|
||||
state: MarketPriceBlockState,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
MarketPriceBlock(state = state)
|
||||
}
|
||||
}
|
||||
|
||||
private class WalletMarketPriceBlockStateProvider : CollectionPreviewParameterProvider<MarketPriceBlockState>(
|
||||
collection = listOf(
|
||||
MarketPriceBlockState.Content(
|
||||
currencyName = "BTC",
|
||||
price = "98900",
|
||||
priceChangeConfig = PriceChangeConfig(
|
||||
valueInPercent = "5.16%",
|
||||
type = PriceChangeConfig.Type.DOWN,
|
||||
),
|
||||
),
|
||||
MarketPriceBlockState.Content(
|
||||
currencyName = "BTC",
|
||||
price = "98900",
|
||||
priceChangeConfig = PriceChangeConfig(
|
||||
valueInPercent = "10.89%",
|
||||
type = PriceChangeConfig.Type.UP,
|
||||
),
|
||||
),
|
||||
MarketPriceBlockState.Loading(currencyName = "BTC"),
|
||||
),
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.core.ui.components.marketprice
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed interface MarketPriceBlockState {
|
||||
|
||||
val currencyName: String
|
||||
|
||||
data class Loading(override val currencyName: String) : MarketPriceBlockState
|
||||
|
||||
data class Content(
|
||||
override val currencyName: String,
|
||||
val price: String,
|
||||
val priceChangeConfig: PriceChangeConfig,
|
||||
) : MarketPriceBlockState
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.tangem.core.ui.components.marketprice
|
||||
|
||||
data class PriceChangeConfig(val valueInPercent: String, val type: Type) {
|
||||
|
||||
/** Price changing type */
|
||||
enum class Type {
|
||||
UP, DOWN
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH2
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
|
|
@ -43,10 +45,10 @@ fun Notification(state: NotificationState, modifier: Modifier = Modifier) {
|
|||
)
|
||||
.clickable(
|
||||
enabled = when (state) {
|
||||
is NotificationState.Simple -> false
|
||||
is NotificationState.Action -> true
|
||||
is NotificationState.Clickable -> true
|
||||
is NotificationState.Simple, is NotificationState.Closable -> false
|
||||
},
|
||||
onClick = if (state is NotificationState.Action) {
|
||||
onClick = if (state is NotificationState.Clickable) {
|
||||
state.onClick
|
||||
} else {
|
||||
{}
|
||||
|
|
@ -67,12 +69,23 @@ fun Notification(state: NotificationState, modifier: Modifier = Modifier) {
|
|||
)
|
||||
|
||||
NotificationInfoBlock(
|
||||
title = state.title,
|
||||
subtitle = state.subtitle,
|
||||
title = state.title.resolveReference(),
|
||||
subtitle = state.subtitle?.resolveReference(),
|
||||
modifier = Modifier.align(alignment = Alignment.CenterStart),
|
||||
)
|
||||
|
||||
if (state is NotificationState.Action) {
|
||||
if (state is NotificationState.Closable) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size20)
|
||||
.align(alignment = Alignment.TopEnd),
|
||||
painter = painterResource(id = R.drawable.ic_close_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
|
||||
if (state is NotificationState.Clickable) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size20)
|
||||
|
|
@ -149,30 +162,50 @@ private fun Preview_WarningNotification_Dark(
|
|||
private class NotificationStateProvider : CollectionPreviewParameterProvider<NotificationState>(
|
||||
collection = listOf(
|
||||
NotificationState.Simple(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
|
||||
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
|
||||
title = TextReference.Str(value = "Your wallet hasn’t been backed up"),
|
||||
subtitle = TextReference.Str(
|
||||
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
|
||||
"ut labore et...",
|
||||
),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
),
|
||||
NotificationState.Simple(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
title = TextReference.Str("Your wallet hasn’t been backed up"),
|
||||
subtitle = null,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
tint = TangemColorPalette.Amaranth,
|
||||
),
|
||||
NotificationState.Action(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
subtitle = "Lorem ipsum dolor sit amet, consectetur " +
|
||||
"adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
|
||||
NotificationState.Clickable(
|
||||
title = TextReference.Str(value = "Your wallet hasn’t been backed up"),
|
||||
subtitle = TextReference.Str(
|
||||
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
|
||||
"ut labore et...",
|
||||
),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
onClick = {},
|
||||
),
|
||||
NotificationState.Action(
|
||||
title = "Your wallet hasn’t been backed up",
|
||||
NotificationState.Clickable(
|
||||
title = TextReference.Str(value = "Your wallet hasn’t been backed up"),
|
||||
subtitle = null,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
tint = TangemColorPalette.Amaranth,
|
||||
onClick = {},
|
||||
),
|
||||
NotificationState.Closable(
|
||||
title = TextReference.Str(value = "Your wallet hasn’t been backed up"),
|
||||
subtitle = TextReference.Str(
|
||||
value = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt " +
|
||||
"ut labore et...",
|
||||
),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
onCloseClick = {},
|
||||
),
|
||||
NotificationState.Closable(
|
||||
title = TextReference.Str(value = "Your wallet hasn’t been backed up"),
|
||||
subtitle = null,
|
||||
iconResId = R.drawable.ic_alert_circle_24,
|
||||
tint = TangemColorPalette.Amaranth,
|
||||
onCloseClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.core.ui.components.notifications
|
|||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
* Notification component state
|
||||
|
|
@ -14,8 +15,8 @@ import androidx.compose.ui.graphics.Color
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class NotificationState(
|
||||
open val title: String,
|
||||
open val subtitle: String? = null,
|
||||
open val title: TextReference,
|
||||
open val subtitle: TextReference? = null,
|
||||
@DrawableRes open val iconResId: Int,
|
||||
open val tint: Color? = null,
|
||||
) {
|
||||
|
|
@ -29,8 +30,8 @@ sealed class NotificationState(
|
|||
* @property tint icon tint
|
||||
*/
|
||||
data class Simple(
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
override val title: TextReference,
|
||||
override val subtitle: TextReference? = null,
|
||||
@DrawableRes override val iconResId: Int,
|
||||
override val tint: Color? = null,
|
||||
) : NotificationState(title, subtitle, iconResId, tint)
|
||||
|
|
@ -42,13 +43,30 @@ sealed class NotificationState(
|
|||
* @property subtitle subtitle
|
||||
* @property iconResId icon resource id
|
||||
* @property tint icon tint
|
||||
* @param onClick lambda be invoked when notification component is clicked
|
||||
* @property onClick lambda be invoked when notification component is clicked
|
||||
*/
|
||||
data class Action(
|
||||
override val title: String,
|
||||
override val subtitle: String? = null,
|
||||
data class Clickable(
|
||||
override val title: TextReference,
|
||||
override val subtitle: TextReference? = null,
|
||||
@DrawableRes override val iconResId: Int,
|
||||
override val tint: Color? = null,
|
||||
val onClick: () -> Unit,
|
||||
) : NotificationState(title, subtitle, iconResId, tint)
|
||||
|
||||
/**
|
||||
* Closable notification state
|
||||
*
|
||||
* @property title title
|
||||
* @property subtitle subtitle
|
||||
* @property iconResId icon resource id
|
||||
* @property tint icon tint
|
||||
* @property onCloseClick lambda be invoked when close button is clicked
|
||||
*/
|
||||
data class Closable(
|
||||
override val title: TextReference,
|
||||
override val subtitle: TextReference? = null,
|
||||
@DrawableRes override val iconResId: Int,
|
||||
override val tint: Color? = null,
|
||||
val onCloseClick: (() -> Unit)? = null,
|
||||
) : NotificationState(title, subtitle, iconResId, tint)
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ data class TangemDimens internal constructor(
|
|||
val size5: Dp = 5.dp,
|
||||
val size7: Dp = 7.dp,
|
||||
val size8: Dp = 8.dp,
|
||||
val size10: Dp = 10.dp,
|
||||
val size11: Dp = 11.dp,
|
||||
val size12: Dp = 12.dp,
|
||||
val size16: Dp = 16.dp,
|
||||
|
|
@ -101,6 +102,7 @@ data class TangemDimens internal constructor(
|
|||
val spacing34: Dp = 34.dp,
|
||||
val spacing36: Dp = 34.dp,
|
||||
val spacing38: Dp = 38.dp,
|
||||
val spacing40: Dp = 40.dp,
|
||||
val spacing44: Dp = 44.dp,
|
||||
val spacing50: Dp = 50.dp,
|
||||
val spacing52: Dp = 52.dp,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.Shape
|
|||
data class TangemShapes internal constructor(
|
||||
val roundedCornersSmall: Shape,
|
||||
val roundedCornersSmall2: Shape,
|
||||
val roundedCorners8: Shape,
|
||||
val roundedCornersMedium: Shape,
|
||||
val roundedCornersXMedium: Shape,
|
||||
val roundedCornersLarge: Shape,
|
||||
|
|
@ -16,6 +17,7 @@ data class TangemShapes internal constructor(
|
|||
constructor(dimens: TangemDimens) : this(
|
||||
roundedCornersSmall = RoundedCornerShape(size = dimens.radius2),
|
||||
roundedCornersSmall2 = RoundedCornerShape(size = dimens.radius4),
|
||||
roundedCorners8 = RoundedCornerShape(size = dimens.radius8),
|
||||
roundedCornersMedium = RoundedCornerShape(size = dimens.radius12),
|
||||
roundedCornersXMedium = RoundedCornerShape(size = dimens.radius16),
|
||||
roundedCornersLarge = RoundedCornerShape(size = dimens.radius28),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
import java.util.Currency
|
||||
import java.util.Locale
|
||||
|
||||
object BigDecimalFormatter {
|
||||
|
||||
const val EMPTY_BALANCE_SIGN = "—"
|
||||
|
||||
private const val TEMP_CURRENCY_CODE = "USD"
|
||||
|
||||
fun formatCryptoAmount(
|
||||
cryptoAmount: BigDecimal,
|
||||
cryptoCurrency: String,
|
||||
decimals: Int,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val formatterCurrency = getCurrency(cryptoCurrency)
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8)
|
||||
minimumFractionDigits = 2
|
||||
roundingMode = RoundingMode.DOWN
|
||||
}
|
||||
|
||||
return formatter.format(cryptoAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), cryptoCurrency)
|
||||
}
|
||||
|
||||
fun formatFiatAmount(
|
||||
fiatAmount: BigDecimal,
|
||||
fiatCurrencyCode: String,
|
||||
fiatCurrencySymbol: String,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): String {
|
||||
val formatterCurrency = getCurrency(fiatCurrencyCode)
|
||||
val formatter = NumberFormat.getCurrencyInstance(locale).apply {
|
||||
currency = formatterCurrency
|
||||
maximumFractionDigits = 2
|
||||
minimumFractionDigits = 2
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
|
||||
return formatter.format(fiatAmount)
|
||||
.replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol)
|
||||
}
|
||||
|
||||
fun formatPercent(percent: BigDecimal, useAbsoluteValue: Boolean, locale: Locale = Locale.getDefault()): String {
|
||||
val formatter = NumberFormat.getPercentInstance(locale).apply {
|
||||
maximumFractionDigits = 2
|
||||
minimumFractionDigits = 2
|
||||
roundingMode = RoundingMode.HALF_UP
|
||||
}
|
||||
val value = if (useAbsoluteValue) percent.abs() else percent
|
||||
|
||||
return formatter.format(value)
|
||||
}
|
||||
|
||||
private fun getCurrency(code: String): Currency {
|
||||
return runCatching { Currency.getInstance(code) }
|
||||
.getOrElse { e ->
|
||||
// Currency code is not valid ISO 4217 code
|
||||
if (e is IllegalArgumentException) {
|
||||
Currency.getInstance(TEMP_CURRENCY_CODE)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,18 +9,21 @@ import javax.inject.Inject
|
|||
interface CoroutineDispatcherProvider {
|
||||
val main: CoroutineDispatcher
|
||||
val io: CoroutineDispatcher
|
||||
val default: CoroutineDispatcher
|
||||
val single: CoroutineDispatcher
|
||||
}
|
||||
|
||||
class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider {
|
||||
override val main: CoroutineDispatcher = Dispatchers.Main
|
||||
override val io: CoroutineDispatcher = Dispatchers.IO
|
||||
override val default: CoroutineDispatcher = Dispatchers.Default
|
||||
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
|
||||
}
|
||||
|
||||
class TestingCoroutineDispatcherProvider(
|
||||
override val main: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val io: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val default: CoroutineDispatcher = Dispatchers.Unconfined,
|
||||
override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
|
||||
) : CoroutineDispatcherProvider
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue