Updated on 2026-08-14

This commit is contained in:
Tangem 2023-07-24 13:45:56 +03:00
commit ea661f55ab
250 changed files with 6169 additions and 949 deletions

View file

@ -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
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,4 @@
plugins {
alias(deps.plugins.kotlin.jvm)
id("configuration")
}

View file

@ -1,4 +1,4 @@
package com.tangem.core.analytics
package com.tangem.core.analytics.models
/**
[REDACTED_AUTHOR]

View file

@ -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
/**

View file

@ -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]

View file

@ -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]

View file

@ -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]

View file

@ -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)

View file

@ -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()
}

View file

@ -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()
}
}

View file

@ -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)

View 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()
}

View 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()
}
}

View 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,
)

View file

@ -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) }
}
}

View file

@ -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
}
}

View file

@ -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",

View file

@ -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",
),
)

View file

@ -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 = {},
),
),
)

View file

@ -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,

View file

@ -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"),
),
)

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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 hasnt 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 hasnt 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 hasnt been backed up",
title = TextReference.Str("Your wallet hasnt been backed up"),
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
),
NotificationState.Action(
title = "Your wallet hasnt 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 hasnt 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 hasnt been backed up",
NotificationState.Clickable(
title = TextReference.Str(value = "Your wallet hasnt been backed up"),
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
onClick = {},
),
NotificationState.Closable(
title = TextReference.Str(value = "Your wallet hasnt 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 hasnt been backed up"),
subtitle = null,
iconResId = R.drawable.ic_alert_circle_24,
tint = TangemColorPalette.Amaranth,
onCloseClick = {},
),
),
)

View file

@ -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)
}

View file

@ -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,

View file

@ -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),

View file

@ -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
}
}
}
}

View file

@ -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