Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-06 17:16:32 +04:00
commit 19df6fefae
194 changed files with 5193 additions and 1945 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.datasource.api.ethpool
import com.tangem.datasource.api.common.response.ApiResponse
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolAccountsListRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest
import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolTransactionRequest
import com.tangem.datasource.api.ethpool.models.response.*
@ -94,4 +95,20 @@ interface P2PEthPoolApi {
@Path("delegatorAddress") delegatorAddress: String,
@Path("vaultAddress") vaultAddress: String,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolAccountResponse>>
/**
* Get account summaries for multiple delegators in a vault (batch).
*
* Designed to be called once per client to avoid rate-limit bursts.
*
* @param network Ethereum pool network: "mainnet" or "hoodi"
* @param vaultAddress Ethereum address of the vault
* @param body Delegator addresses to fetch (up to 255)
*/
@POST("api/v1/staking/pool/{network}/vaults/{vaultAddress}/accounts/list")
suspend fun getAccountsList(
@Path("network") network: String,
@Path("vaultAddress") vaultAddress: String,
@Body body: P2PEthPoolAccountsListRequest,
): ApiResponse<P2PEthPoolResponse<P2PEthPoolAccountsListResponse>>
}

View file

@ -0,0 +1,16 @@
package com.tangem.datasource.api.ethpool.models.request
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Request body for POST /api/v1/staking/pool/{network}/vaults/{vaultAddress}/accounts/list
*
* Batch fetch of staking balances for multiple delegator addresses within a single vault.
* Limit: up to 255 addresses per request. Addresses are deduplicated server-side.
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolAccountsListRequest(
@Json(name = "delegatorAddresses")
val delegatorAddresses: List<String>,
)

View file

@ -0,0 +1,26 @@
package com.tangem.datasource.api.ethpool.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Response for POST /api/v1/staking/pool/{network}/vaults/{vaultAddress}/accounts/list
*
* Each item is keyed by delegatorAddress and carries either a non-null [account]
* or a per-address [error] (e.g. code 127108 invalid delegator address).
*/
@JsonClass(generateAdapter = true)
data class P2PEthPoolAccountsListResponse(
@Json(name = "list")
val list: List<P2PEthPoolAccountListItem>,
)
@JsonClass(generateAdapter = true)
data class P2PEthPoolAccountListItem(
@Json(name = "delegatorAddress")
val delegatorAddress: String,
@Json(name = "account")
val account: P2PEthPoolAccountResponse?,
@Json(name = "error")
val error: P2PEthPoolErrorDetailsDTO?,
)

View file

@ -23,7 +23,7 @@ data class P2PEthPoolErrorDetailsDTO(
@Json(name = "message")
val message: String, // Human-readable error message
@Json(name = "name")
val name: String, // Error name/type
val name: String?, // Error name/type
@Json(name = "errors")
val errors: List<String>? = null, // Optional validation errors array
)

View file

@ -23,9 +23,11 @@ interface AppsFlyerStore {
enum class AppsFlyerDeeplinkSource {
TangemPayHotWalletOnboarding,
Referral,
;
fun toStoreKey() = when (this) {
TangemPayHotWalletOnboarding -> "tangem_pay_hot_wallet_onboarding"
Referral -> "referral"
}
}

View file

@ -0,0 +1,68 @@
package com.tangem.datasource.api.ethpool
import com.google.common.truth.Truth.assertThat
import com.squareup.moshi.Types
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountsListResponse
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolResponse
import org.junit.jupiter.api.Test
import java.math.BigDecimal
internal class P2PEthPoolAccountsListResponseTest {
private val adapter = MoshiConverter.networkMoshi.adapter<P2PEthPoolResponse<P2PEthPoolAccountsListResponse>>(
Types.newParameterizedType(
P2PEthPoolResponse::class.java,
P2PEthPoolAccountsListResponse::class.java,
),
)
@Test
fun `decode batch payload with valid account and per-address error`() {
val response = requireNotNull(adapter.fromJson(SAMPLE_JSON))
val list = requireNotNull(response.result).list
assertThat(list).hasSize(2)
val good = list.first { it.account != null }
val account = requireNotNull(good.account)
assertThat(account.stake.assets.compareTo(BigDecimal("1.2345"))).isEqualTo(0)
assertThat(account.availableToWithdraw).isGreaterThan(BigDecimal(15049))
assertThat(account.exitQueue.requests).isEmpty()
val bad = list.first { it.account == null }
assertThat(requireNotNull(bad.error).code).isEqualTo(127108)
}
private companion object {
private val SAMPLE_JSON = """
{
"error": null,
"result": {
"list": [
{
"delegatorAddress": "0x008d3cd3e349Cd3D5F7c287b3BaF9e4f3E4ba99b",
"account": {
"delegatorAddress": "0x008d3cd3e349Cd3D5F7c287b3BaF9e4f3E4ba99b",
"vaultAddress": "0x4c09BC47db288F998b33CD63BCc1b6ddCCe13F33",
"stake": { "assets": "1.234500000000000000", "totalEarnedAssets": 0.0191 },
"availableToUnstake": "0.000000000000000005",
"availableToWithdraw": 15049.547647281135,
"exitQueue": { "total": 0, "requests": [] }
},
"error": null
},
{
"delegatorAddress": "0xBADADDRESS",
"account": null,
"error": {
"code": 127108,
"message": "The provided delegator address is invalid or not properly formatted."
}
}
]
}
}
""".trimIndent()
}
}

View file

@ -83,12 +83,16 @@ fun TangemMessage(
trailingContent = if (isIconLeading) null else icon,
contentColor = contentColor,
onCloseClick = messageUM.onCloseClick,
buttons = {
messageUM.buttonsUM.fastForEach { buttonUM ->
TangemButton(
buttonUM = buttonUM.tangemButtonUM,
modifier = Modifier.weight(1f),
)
buttons = if (messageUM.buttonsUM.isEmpty()) {
null
} else {
{
messageUM.buttonsUM.fastForEach { buttonUM ->
TangemButton(
buttonUM = buttonUM.tangemButtonUM,
modifier = Modifier.weight(1f),
)
}
}
},
)

View file

@ -27,19 +27,20 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenElementsTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
/**
* UI model for header row component
*
* @param headerRowUM UI model for the header row
* @param modifier Modifier for the composable
* @param headerRowUM UI model for the header row
* @param modifier Modifier for the composable
* @param dragHandleModifier Modifier applied to the drag handle in the row's tail (e.g. a reorderable
* drag-handle modifier). Defaults to [Modifier] for non-reorderable rows.
*/
@Composable
fun TangemHeaderRow(
headerRowUM: TangemHeaderRowUM,
modifier: Modifier = Modifier,
reorderableState: ReorderableLazyListState? = null,
dragHandleModifier: Modifier = Modifier,
isBalanceHidden: Boolean = false,
) {
TangemHeaderRow(
@ -48,7 +49,7 @@ fun TangemHeaderRow(
title = headerRowUM.title,
subtitle = headerRowUM.subtitle,
isBalanceHidden = isBalanceHidden,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = modifier,
)
}
@ -129,7 +130,7 @@ fun TangemHeaderRow(
subtitle: TextReference? = null,
headTangemIconUM: TangemIconUM? = null,
tailUM: TangemRowTailUM = TangemRowTailUM.Empty,
reorderableState: ReorderableLazyListState? = null,
dragHandleModifier: Modifier = Modifier,
isEnabled: Boolean = false,
onItemClick: (() -> Unit)? = null,
) {
@ -181,7 +182,7 @@ fun TangemHeaderRow(
SpacerWMax()
TangemRowTail(
tangemRowTailUM = tailUM,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
)
}
}

View file

@ -19,14 +19,12 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
fun TangemRowTail(
tangemRowTailUM: TangemRowTailUM,
modifier: Modifier = Modifier,
reorderableState: ReorderableLazyListState? = null,
dragHandleModifier: Modifier = Modifier,
) {
AnimatedContent(
targetState = tangemRowTailUM,
@ -39,7 +37,7 @@ fun TangemRowTail(
TangemRowTailUM.Empty -> Unit
is TangemRowTailUM.Draggable -> DraggableImage(
iconRes = animatedState.iconRes,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = innerModifier,
)
is TangemRowTailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier)
@ -54,21 +52,11 @@ fun TangemRowTail(
}
@Composable
private fun DraggableImage(
@DrawableRes iconRes: Int,
reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
private fun DraggableImage(@DrawableRes iconRes: Int, dragHandleModifier: Modifier, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(size = TangemTheme.dimens2.x6)
.then(
other = if (reorderableState != null) {
Modifier.detectReorder(reorderableState)
} else {
Modifier
},
)
.then(dragHandleModifier)
.testTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE),
contentAlignment = Alignment.Center,
) {

View file

@ -21,24 +21,24 @@ import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenElementsTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
/**
* Composable function that represents a Tangem token row in a list.
*
* [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4)
*
* @param tokenRowUM The user model containing the data for the token row.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param reorderableState The state of the reorderable lazy list, if applicable.
* @param modifier The modifier to be applied to the row.
* @param tokenRowUM The user model containing the data for the token row.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param dragHandleModifier Modifier applied to the drag handle in the row's tail (e.g. a reorderable
* drag-handle modifier). Defaults to [Modifier] for non-reorderable rows.
* @param modifier The modifier to be applied to the row.
*/
@Composable
fun TangemTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
dragHandleModifier: Modifier = Modifier,
) {
TangemRowContainer(
content = {
@ -91,7 +91,7 @@ fun TangemTokenRow(
TangemRowTail(
tangemRowTailUM = tokenRowUM.tailUM,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
@ -116,18 +116,19 @@ fun TangemTokenRow(
* [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4)
*
* @param tokenRowUM The user model containing the data for the token row.
* @param headComponent The composable function representing the head component.
* @param titleComponent The composable function representing the title component.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param reorderableState The state of the reorderable lazy list, if applicable.
* @param modifier The modifier to be applied to the row.
* @param headComponent The composable function representing the head component.
* @param titleComponent The composable function representing the title component.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param dragHandleModifier Modifier applied to the drag handle in the row's tail (e.g. a reorderable
* drag-handle modifier). Defaults to [Modifier] for non-reorderable rows.
* @param modifier The modifier to be applied to the row.
*/
@Composable
fun TangemTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
dragHandleModifier: Modifier = Modifier,
headComponent: @Composable (Modifier) -> Unit,
titleComponent: @Composable (Modifier) -> Unit,
) {
@ -190,7 +191,7 @@ fun TangemTokenRow(
TangemRowTail(
tangemRowTailUM = tokenRowUM.tailUM,
reorderableState = reorderableState,
dragHandleModifier = dragHandleModifier,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
@ -210,7 +211,6 @@ private fun TangemTokenRow_Preview(
TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = false,
reorderableState = null,
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
)
}

View file

@ -28,6 +28,8 @@ import kotlinx.coroutines.flow.drop
import kotlin.math.abs
import kotlin.math.absoluteValue
private const val LIST_FLING_DAMPING = 0.1f
/**
* A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down.
* When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state
@ -176,7 +178,8 @@ private fun exitUntilCollapsedScrollBehavior(
}
}
return Velocity(0f, available.y - remainingVelocity)
val passedVelocity = remainingVelocity * LIST_FLING_DAMPING
return Velocity(0f, available.y - passedVelocity)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {

View file

@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.graphics.vector.ImageVector
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_arrow_left_20
import com.tangem.core.ui.res.generated.icons.ic_chevron_left_20
import com.tangem.core.ui.res.generated.icons.ic_cross_20
import com.tangem.core.ui.test.TopNavigationTestTags
@ -16,7 +17,8 @@ fun TangemButton.Back(modifier: Modifier = Modifier, onClick: () -> Unit) {
TangemButton(
modifier = modifier.testTag(TopNavigationTestTags.BACK_BUTTON),
variant = TangemButton.Variant.Material,
iconStart = TangemIconUM.Icon(Icons.ic_arrow_left_20),
size = TangemButton.Size.X11,
iconStart = TangemIconUM.Icon(Icons.ic_chevron_left_20),
onClick = onClick,
)
}
@ -27,7 +29,26 @@ fun TangemButton.Close(modifier: Modifier = Modifier, onClick: () -> Unit) {
TangemButton(
modifier = modifier,
variant = TangemButton.Variant.Material,
size = TangemButton.Size.X11,
iconStart = TangemIconUM.Icon(Icons.ic_cross_20),
onClick = onClick,
)
}
@Composable
@NonRestartableComposable
fun TangemButton.GroupEntry(iconUM: TangemIconUM, modifier: Modifier = Modifier, onClick: () -> Unit) {
TangemButton(
modifier = modifier,
variant = TangemButton.Variant.Ghost,
size = TangemButton.Size.X9,
iconStart = iconUM,
onClick = onClick,
)
}
@Composable
@NonRestartableComposable
fun TangemButton.GroupEntry(imageVector: ImageVector, modifier: Modifier = Modifier, onClick: () -> Unit) {
GroupEntry(iconUM = TangemIconUM.Icon(imageVector), modifier = modifier, onClick = onClick)
}

View file

@ -1,27 +1,13 @@
package com.tangem.core.ui.ds2.button
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.*
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -38,11 +24,7 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.loader.TangemLoader
import com.tangem.core.ui.extensions.ColorReference2
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.rememberLastNonNull
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
/**
@ -168,6 +150,10 @@ private fun ContentRow(
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = TangemTheme.typography3.caption.medium.fontSize,
maxFontSize = TangemTheme.typography3.body.medium.fontSize,
),
)
}
}

View file

@ -140,7 +140,11 @@ private fun SearchField(state: TangemSearch.State, focusRequester: FocusRequeste
Icon(
modifier = Modifier.padding(end = 8.dp),
imageVector = Icons.ic_search_20,
tint = TangemTheme.colors3.icon.primary,
tint = if (state.isActive) {
TangemTheme.colors3.icon.secondary
} else {
TangemTheme.colors3.icon.primary
},
contentDescription = null,
)
QueryTextField(state = state, focusRequester = focusRequester)

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
@ -14,7 +15,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -24,25 +25,17 @@ import kotlin.math.cos
import kotlin.math.sin
/**
* Design-system rectangle shimmer placeholder.
* Design-system v2 shimmer placeholder a rounded rectangle with a sweeping highlight.
*
* A rounded rectangle painted with `bg.opaque.secondary`. A tilted band sweeps across it where
* the base color's alpha is gradually dimmed toward the center of the band and restored at the
* edges, producing a soft "blade" highlight passing through the placeholder. The alpha profile
* matches [com.tangem.core.ui.components.text.BladeAnimation].
*
* Cycle: 1.5s hold 0.8s linear sweep restart.
*
* Version 1.0
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=3398-625&p=f&m=dev)
*
* Sizing is the caller's responsibility set width and height via [modifier].
* For a placeholder sized after a typography line, use the [TangemShimmer] text overload instead.
*
* @param modifier Modifier applied to the shimmer's root.
* @param modifier Modifier applied to the shimmer's root. Set the width and height here.
* @param radius Corner radius of the rectangle.
*/
@Composable
fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = 6.dp) {
fun TangemShimmer(modifier: Modifier = Modifier, radius: Dp = TangemShimmer.DefaultRadius) {
val baseColor = TangemTheme.colors3.bg.opaque.secondary
val progress = LocalTangemShimmerProgress.current ?: rememberShimmerProgressInstance()
val colorStops = remember(baseColor) { buildColorStops(baseColor) }
@ -77,63 +70,76 @@ fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = 6.dp) {
}
/**
* Text-sized shimmer placeholder. Sizes itself to the bounding box of the [text] measured in the
* typography preset selected by [style], plus the preset's vertical padding (top + bottom).
* Text-line shimmer placeholder, sized and styled after the typography line described by [style].
*
* @param text Text used to determine the shimmer's size. Not drawn.
* @param style Typography preset drives both the measurement style and the vertical padding.
* @param radius Corner radius of the rectangle.
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=3398-625&p=f&m=dev)
*
* @param style A [TangemTheme.typography3] style (e.g. `TangemTheme.typography3.body.medium`) the
* placeholder is sized after. Unrecognized styles fall back to `body.medium`.
* @param modifier Modifier applied to the shimmer's root.
* @param textAlign Horizontal position of the block within the parent width.
*/
@Composable
fun TextShimmer(text: String, style: TextShimmerStyle, radius: Dp, modifier: Modifier = Modifier) {
val textStyle = style.toTextStyle()
val measurer = rememberTextMeasurer()
val density = LocalDensity.current
val (widthDp, heightDp) = remember(text, textStyle, measurer, density) {
val measured = measurer.measure(text = text, style = textStyle)
with(density) { measured.size.width.toDp() to measured.size.height.toDp() }
fun TangemShimmer(style: TextStyle, modifier: Modifier = Modifier, textAlign: TextAlign = TextAlign.Start) {
val preset = TangemShimmer.TextPreset.forStyle(style)
val lineHeightDp = with(LocalDensity.current) { style.lineHeight.toDp() }
val alignment = when (textAlign) {
TextAlign.Center -> Alignment.Center
TextAlign.End -> Alignment.CenterEnd
else -> Alignment.CenterStart
}
RectangleShimmer(
modifier = modifier.size(
width = widthDp,
height = heightDp + style.verticalPadding * 2,
),
radius = radius,
)
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = alignment,
) {
TangemShimmer(
modifier = Modifier
.fillMaxWidth(preset.widthFraction)
.height(lineHeightDp)
.padding(vertical = preset.verticalPadding),
radius = preset.radius,
)
}
}
/** Public API namespace for [TangemShimmer]. */
object TangemShimmer {
/** Default corner radius of the rectangle shimmer. */
val DefaultRadius: Dp = 6.dp
/** Per-typography sizing for the [TangemShimmer] text overload. */
internal enum class TextPreset(val widthFraction: Float, val verticalPadding: Dp, val radius: Dp) {
Display(widthFraction = 0.5f, verticalPadding = 4.dp, radius = 12.dp),
HeadingMedium(widthFraction = 0.7f, verticalPadding = 2.dp, radius = 8.dp),
HeadingSmall(widthFraction = 0.6f, verticalPadding = 2.dp, radius = 16.dp),
Body(widthFraction = 0.5f, verticalPadding = 2.dp, radius = 16.dp),
Subheading(widthFraction = 0.4f, verticalPadding = 2.dp, radius = 16.dp),
Caption(widthFraction = 0.3f, verticalPadding = 2.dp, radius = 16.dp),
;
companion object {
@Composable
@ReadOnlyComposable
fun forStyle(style: TextStyle): TextPreset {
val typography = TangemTheme.typography3
return when (style) {
typography.display.medium -> Display
typography.heading.medium -> HeadingMedium
typography.heading.small -> HeadingSmall
typography.subheading.medium -> Subheading
typography.caption.medium -> Caption
else -> Body
}
}
}
}
}
/**
* Typography preset for [TextShimmer]. Each preset maps to a [TangemTheme.typography3] style
* and contributes additional [verticalPadding] applied to both top and bottom the shimmer
* block ends up `2 * verticalPadding` taller than the raw measured text.
*/
enum class TextShimmerStyle(val verticalPadding: Dp) {
DISPLAY(verticalPadding = 4.dp),
HEADING_MEDIUM(verticalPadding = 2.dp),
HEADING_SMALL(verticalPadding = 2.dp),
BODY(verticalPadding = 2.dp),
SUBHEADING(verticalPadding = 2.dp),
CAPTION(verticalPadding = 2.dp),
}
@Composable
@ReadOnlyComposable
private fun TextShimmerStyle.toTextStyle(): TextStyle = when (this) {
TextShimmerStyle.DISPLAY -> TangemTheme.typography3.display.medium
TextShimmerStyle.HEADING_MEDIUM -> TangemTheme.typography3.heading.medium
TextShimmerStyle.HEADING_SMALL -> TangemTheme.typography3.heading.small
TextShimmerStyle.BODY -> TangemTheme.typography3.body.medium
TextShimmerStyle.SUBHEADING -> TangemTheme.typography3.subheading.medium
TextShimmerStyle.CAPTION -> TangemTheme.typography3.caption.medium
}
/**
* Wraps [content] so every [RectangleShimmer] / [TextShimmer] inside reuses a single shimmer
* animation driver. Without this provider each shimmer creates its own
* [rememberInfiniteTransition] that scales poorly in lists and lets sweeps drift out of phase.
* Safe to nest; safe to omit (each shimmer falls back to its own driver).
* Wraps [content] so every [TangemShimmer] inside shares a single, in-phase animation driver
* use it around lists of shimmers. Safe to nest; safe to omit.
*/
@Composable
fun ProvideTangemShimmer(content: @Composable () -> Unit) {
@ -198,24 +204,16 @@ private fun TangemShimmerPreview() {
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
RectangleShimmer(
TangemShimmer(
modifier = Modifier.size(width = 200.dp, height = 24.dp),
radius = 6.dp,
)
RectangleShimmer(
TangemShimmer(
modifier = Modifier.size(width = 120.dp, height = 16.dp),
radius = 4.dp,
)
TextShimmer(
text = "Account balance",
style = TextShimmerStyle.BODY,
radius = 4.dp,
)
TextShimmer(
text = "$12,345.67",
style = TextShimmerStyle.HEADING_MEDIUM,
radius = 6.dp,
)
TangemShimmer(style = TangemTheme.typography3.body.medium)
TangemShimmer(style = TangemTheme.typography3.heading.medium)
}
}
}

View file

@ -100,7 +100,7 @@ fun TangemSurface(
}
if (onClick != null) {
CompositionLocalProvider(LocalRippleConfiguration provides tangemSurfaceRipple()) {
CompositionLocalProvider(LocalRippleConfiguration provides tangemSurfaceRipple(color)) {
surface()
}
} else {
@ -117,14 +117,17 @@ fun TangemSurface(
* `isAlphaContentClip`) to avoid the dark blur bleeding through the surface.
*/
@Composable
private fun Modifier.materialShadow(shape: Shape, radius: Dp): Modifier = softLayerShadow(
radius = radius,
color = Color.Black.copy(alpha = 0.12f),
shape = shape,
spread = 0.dp,
offset = DpOffset(x = 0.dp, y = 8.dp),
isAlphaContentClip = true,
)
private fun Modifier.materialShadow(shape: Shape, radius: Dp): Modifier {
val isBlurEnabled = LocalHazeState.current.blurEnabled
return softLayerShadow(
radius = radius,
color = Color.Black.copy(alpha = 0.12f),
shape = shape,
spread = 0.dp,
offset = DpOffset(x = 0.dp, y = 8.dp),
isAlphaContentClip = isBlurEnabled,
)
}
/** Diagonal gradient stroke that wraps the material variant. */
@Composable
@ -193,8 +196,12 @@ private fun materialBorderBrush(): Brush {
@Composable
@ReadOnlyComposable
private fun tangemSurfaceRipple(): RippleConfiguration = RippleConfiguration(
color = TangemTheme.colors3.interaction.press.default,
private fun tangemSurfaceRipple(backgroundColor: Color): RippleConfiguration = RippleConfiguration(
color = if (backgroundColor == TangemTheme.colors3.bg.inverse) {
TangemTheme.colors3.interaction.press.inverse
} else {
TangemTheme.colors3.interaction.press.default
},
rippleAlpha = RippleAlpha(
draggedAlpha = 0f,
focusedAlpha = 0f,

View file

@ -48,7 +48,7 @@ fun TangemNavigationText(
modifier = modifier,
color = navigationTextColor(role),
style = navigationTextStyle(role),
textAlign = TextAlign.Center,
textAlign = TextAlign.Start,
maxLines = maxLines,
overflow = overflow,
)
@ -73,7 +73,7 @@ fun TangemNavigationText(
modifier = modifier,
color = navigationTextColor(role),
style = navigationTextStyle(role),
textAlign = TextAlign.Center,
textAlign = TextAlign.Start,
maxLines = maxLines,
overflow = overflow,
)

View file

@ -53,7 +53,7 @@ private enum class SlotId { Start, Content, Group, End }
* @param blurBackground Whether the fade behind the row should blur the content below.
* @param startButton Leading slot. Typically a back button (see [TangemButton.Back]).
* @param endButtonsGroup Optional pill-grouped secondary actions placed just before [endButton].
* @param endButton Trailing slot. Typically a close button (see [TangemButton.Close]).
* @param endButton Trailing slot. Typically, a close button (see [TangemButton.Close]).
* @param contentColumn Center slot. Place title/subtitle children here.
*/
@Suppress("LongMethod")
@ -88,7 +88,6 @@ fun TangemTopNavigation(
blur = blurBackground,
)
val groupSpacing = 8.dp
Layout(
modifier = Modifier
.fillMaxWidth()
@ -108,7 +107,7 @@ fun TangemTopNavigation(
Column(
modifier = Modifier
.padding(horizontal = 12.dp)
.padding(start = if (startButton != null) 12.dp else 0.dp, end = 12.dp)
.layoutId(SlotId.Content),
horizontalAlignment = when (contentAlign) {
TangemTopNavigation.ContentAlign.Start -> Alignment.Start
@ -148,7 +147,7 @@ fun TangemTopNavigation(
}
},
) { measurables, constraints ->
val groupSpacingPx = groupSpacing.roundToPx()
val groupSpacingPx = 8.dp.roundToPx()
val totalWidth = constraints.maxWidth
val startM = measurables.first { it.layoutId == SlotId.Start }
@ -161,13 +160,15 @@ fun TangemTopNavigation(
val endP = endM.measure(slotConstraints)
val groupP = groupM.measure(slotConstraints)
val endGap = if (endP.width > 0) groupSpacingPx else 0
val groupOccupiedWidth = if (groupP.width > 0) endGap + groupP.width else 0
val trailingWidth = endP.width + groupOccupiedWidth
val contentMaxWidth = when (contentAlign) {
// Symmetric band so the content can be visually centered within `totalWidth`
// without colliding with the start/end slots.
TangemTopNavigation.ContentAlign.Center ->
(totalWidth - 2 * maxOf(startP.width, endP.width)).coerceAtLeast(0)
(totalWidth - 2 * maxOf(startP.width, trailingWidth)).coerceAtLeast(0)
TangemTopNavigation.ContentAlign.Start ->
(totalWidth - startP.width - endP.width).coerceAtLeast(0)
(totalWidth - startP.width - trailingWidth).coerceAtLeast(0)
}
val contentP = contentM.measure(slotConstraints.copy(maxWidth = contentMaxWidth))
@ -183,15 +184,14 @@ fun TangemTopNavigation(
((totalWidth - contentP.width) / 2)
.coerceIn(
startP.width,
(totalWidth - endP.width - contentP.width).coerceAtLeast(startP.width),
(totalWidth - trailingWidth - contentP.width).coerceAtLeast(startP.width),
)
}
contentP.placeRelative(x = contentX, y = centerY(contentP.height))
endP.placeRelative(x = totalWidth - endP.width, y = centerY(endP.height))
// Group floats to the left of endButton with `groupSpacing` gap, overlaying the
// tail of the content band if necessary.
val groupX = (totalWidth - endP.width - groupSpacingPx - groupP.width)
val groupX = (totalWidth - endP.width - endGap - groupP.width)
.coerceAtLeast(0)
groupP.placeRelative(x = groupX, y = centerY(groupP.height))
}

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import com.tangem.core.ui.components.haze.ProvideHaze
import com.tangem.core.ui.ds2.shimmers.ProvideTangemShimmer
import com.tangem.core.ui.res.generated.TangemTypography3
import com.tangem.core.ui.res.generated.darkColors3
import com.tangem.core.ui.res.generated.lightColors3
@ -48,8 +49,10 @@ fun TangemThemeRedesign(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalTextSelectionColors provides TangemTextSelectionColors2,
) {
ProvideHaze {
content()
ProvideTangemShimmer {
ProvideHaze {
content()
}
}
}
}

View file

@ -3,4 +3,5 @@ package com.tangem.core.ui.test
object BuyTokenScreenTestTags {
const val LAZY_LIST = "BUY_TOKEN_SCREEN_LAZY_LIST"
const val LAZY_LIST_ITEM = "BUY_TOKEN_SCREEN_LAZY_LIST_ITEM"
const val WALLET_TAB = "BUY_TOKEN_SCREEN_WALLET_TAB"
}

View file

@ -5,4 +5,5 @@ object DetailsScreenTestTags {
const val SCREEN_ITEM = "DETAILS_SCREEN_ITEM"
const val VERSION_NAME = "DETAILS_SCREEN_VERSION_NAME"
const val USER_WALLET_ITEM = "DETAILS_SCREEN_USER_WALLET_ITEM"
const val ADD_WALLET_BUTTON = "DETAILS_SCREEN_ADD_WALLET_BUTTON"
}

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:pathData="M22.0779,0.0009C22.2017,6.0786 22.4682,12.1564 22.8218,18.234C23.2874,26.2385 26.7009,34.2436 29.0827,42.2481C29.1354,42.4251 29.185,42.6029 29.2367,42.7799C26.9701,43.5691 24.5355,44 22,44C19.4649,44 17.0305,43.5698 14.7642,42.7808C14.816,42.6034 14.8663,42.4255 14.9191,42.2481C17.3009,34.2436 20.7144,26.2385 21.18,18.234C21.5336,12.1564 21.7992,6.0786 21.923,0.0009C21.9487,0.0008 21.9743,0 22,0C22.026,0 22.0519,0.0008 22.0779,0.0009Z"
android:fillColor="#000000" />
<path
android:pathData="M20.7262,21.2364C19.4639,28.1647 15.6505,35.0935 12.8718,42.0216C12.0526,41.6475 11.2603,41.2248 10.4987,40.7568C14.3095,34.2502 19.1923,27.7431 20.7262,21.2364Z"
android:fillColor="#000000" />
<path
android:pathData="M23.2747,21.2364C24.8085,27.7426 29.6898,34.2498 33.5004,40.7559C32.739,41.2237 31.9472,41.6467 31.1282,42.0208C28.3495,35.093 24.5369,28.1642 23.2747,21.2364Z"
android:fillColor="#000000" />
<path
android:pathData="M20.6062,0.0448C20.5973,6.1451 20.5901,12.2455 20.5641,18.3459C20.5354,25.059 13.5075,31.7743 7.4363,38.4875C2.8764,34.4566 0,28.5647 0,22C0,10.3179 9.1054,0.7638 20.6062,0.0448Z"
android:fillColor="#000000" />
<path
android:pathData="M23.3938,0.0448C34.8946,0.7638 44,10.3179 44,22C44,28.5649 41.123,34.4565 36.5628,38.4875C30.4919,31.7747 23.4655,25.0585 23.4368,18.3459C23.4108,12.2455 23.4027,6.1451 23.3938,0.0448Z"
android:fillColor="#000000" />
</vector>

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="32" android:viewportWidth="32" android:width="24dp">
<path android:fillColor="#EBEBEB" android:pathData="M16,23.697L22.663,27.802C23.715,28.45 25.011,27.488 24.733,26.268L22.964,18.528L28.852,13.321C29.78,12.5 29.284,10.944 28.061,10.838L20.31,10.167L17.278,2.864C16.799,1.712 15.201,1.712 14.722,2.864L11.69,10.167L3.939,10.838C2.716,10.944 2.22,12.5 3.148,13.321L9.036,18.528L7.267,26.268C6.988,27.488 8.285,28.45 9.336,27.802L16,23.697Z"/>

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="128dp"

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="8dp"

View file

@ -1,18 +1,3 @@
<!--
~ Copyright (C) 2026 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="44"
android:viewportHeight="44">
<path
android:pathData="M22,0C34.15,0 44,9.85 44,22C44,34.15 34.15,44 22,44C9.85,44 0,34.15 0,22C0,9.85 9.85,0 22,0Z"
android:fillColor="#242424" />
<path
android:pathData="M22,0C34.15,0 44,9.85 44,22C44,34.15 34.15,44 22,44C9.85,44 0,34.15 0,22C0,9.85 9.85,0 22,0ZM22.0779,0.0009C22.2017,6.0786 22.4682,12.1564 22.8218,18.234C23.2874,26.2385 26.7009,34.2436 29.0827,42.2481C29.1354,42.4251 29.185,42.6029 29.2367,42.7799C26.9701,43.5691 24.5355,44 22,44C19.4649,44 17.0305,43.5698 14.7642,42.7808C14.816,42.6034 14.8663,42.4255 14.9191,42.2481C17.3009,34.2436 20.7144,26.2385 21.18,18.234C21.5336,12.1564 21.7992,6.0786 21.923,0.0009C21.9487,0.0008 21.9743,0 22,0C22.026,0 22.0519,0.0008 22.0779,0.0009ZM20.7262,21.2364C19.4639,28.1647 15.6505,35.0935 12.8718,42.0216C12.0526,41.6475 11.2603,41.2248 10.4987,40.7568C14.3095,34.2502 19.1923,27.7431 20.7262,21.2364ZM23.2747,21.2364C24.8085,27.7426 29.6898,34.2498 33.5004,40.7559C32.739,41.2237 31.9472,41.6467 31.1282,42.0208C28.3495,35.093 24.5369,28.1642 23.2747,21.2364ZM20.6062,0.0448C20.5973,6.1451 20.5901,12.2455 20.5641,18.3459C20.5354,25.059 13.5075,31.7743 7.4363,38.4875C2.8764,34.4566 0,28.5647 0,22C0,10.3179 9.1054,0.7638 20.6062,0.0448ZM23.3938,0.0448C34.8946,0.7638 44,10.3179 44,22C44,28.5649 41.123,34.4565 36.5628,38.4875C30.4919,31.7747 23.4655,25.0585 23.4368,18.3459C23.4108,12.2455 23.4027,6.1451 23.3938,0.0448Z"
android:fillColor="#FFFFFF"
android:fillType="evenOdd" />
</vector>