Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-15 11:12:44 +05:00
commit adf64a5b99
21 changed files with 554 additions and 25 deletions

View file

@ -275,7 +275,7 @@ dependencies {
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.tokenRecieve.impl)
implementation(projects.features.yieldSupply.api)
implementation(projects.features.yieldSupply.api)
implementation(projects.features.yieldSupply.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)

View file

@ -34,9 +34,11 @@ import com.tangem.features.staking.api.StakingComponent
import com.tangem.features.swap.SwapComponent
import com.tangem.features.swap.v2.api.SendWithSwapComponent
import com.tangem.features.tangempay.components.TangemPayDetailsComponent
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.wallet.WalletEntryComponent
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent
import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
@ -45,7 +47,6 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
import com.tangem.tap.features.welcome.component.WelcomeComponent
import com.tangem.tap.routing.component.RoutingComponent.Child
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent
@ -105,6 +106,7 @@ internal class ChildFactory @Inject constructor(
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val tangemPayDetailsComponentFactory: TangemPayDetailsComponent.Factory,
private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory,
private val yieldSupplyPromoComponentFactory: YieldSupplyPromoComponent.Factory,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
@ -600,6 +602,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = tangemPayOnboardingComponentFactory,
)
}
is AppRoute.YieldSupplyPromo -> {
createComponentChild(
context = context,
params = YieldSupplyPromoComponent.Params(route.userWalletId, route.cryptoCurrency),
componentFactory = yieldSupplyPromoComponentFactory,
)
}
}
}
}

View file

@ -382,4 +382,10 @@ sealed class AppRoute(val path: String) : Route {
data class TangemPayOnboarding(
val deeplink: String,
) : AppRoute(path = "/tangem_pay_onboarding/$deeplink")
@Serializable
data class YieldSupplyPromo(
val userWalletId: UserWalletId,
val cryptoCurrency: CryptoCurrency,
) : AppRoute(path = "/yield_supply_promo/${userWalletId.stringValue}/${cryptoCurrency.symbol}")
}

View file

@ -1657,7 +1657,7 @@
<string name="yield_module_promo_screen_self_custodial_subtitle">Aaveは世界中で何百万人もの人々に信頼されています。総貸付額は104億ドルです。</string>
<string name="yield_module_promo_screen_self_custodial_title">分散型・自己管理型</string>
<string name="yield_module_promo_screen_terms_disclaimer">サービスを使用することにより、プロバイダーに同意したものとみなされます</string>
<string name="yield_module_promo_screen_title">年間%s%の収益</string>
<string name="yield_module_promo_screen_title">年間%s%%の収益</string>
<string name="yield_module_promo_screen_variable_rate_info">Aave • 変動金利</string>
<string name="yield_module_provider">Aave</string>
<string name="yield_module_rate_info_sheet_chart_average">平均%s</string>

View file

@ -200,6 +200,7 @@
<string name="common_camera_denied_alert_message">You have not given access to your camera, please adjust your privacy settings</string>
<string name="common_cancel">Cancel</string>
<string name="common_change">Change</string>
<string name="common_choose_account">Choose account</string>
<string name="common_choose_action">Choose action</string>
<string name="common_choose_network">Choose network</string>
<string name="common_choose_token">Choose token</string>
@ -1266,6 +1267,14 @@
<string name="swapping_to_title">You receive</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_not_available">not available</string>
<string name="tangempay_onboarding_get_card_button_text">Get card</string>
<string name="tangempay_onboarding_pay_description">Add to your wallet and pay with your phone anywhere.</string>
<string name="tangempay_onboarding_pay_title">Apple Pay &amp; Google Pay</string>
<string name="tangempay_onboarding_purchases_description">Use your USDC balance to pay for everyday purchases seamlessly.</string>
<string name="tangempay_onboarding_purchases_title">Everyday purchases</string>
<string name="tangempay_onboarding_security_description">Your card details are protected — full control in the app.</string>
<string name="tangempay_onboarding_security_title">Built-in security</string>
<string name="tangempay_onboarding_title">Get your free Crypto Card\nin minutes</string>
<string name="this_is_my_wallet_title">This is my wallet</string>
<string name="toast_balances_hidden">Balances hidden</string>
<string name="toast_balances_shown">Balances shown</string>
@ -1414,14 +1423,6 @@
<string name="visa_unlock_notification_button">Unlock</string>
<string name="visa_unlock_notification_subtitle">Scan your card to unlock access</string>
<string name="visa_unlock_notification_title">Needed unlock</string>
<string name="tangempay_onboarding_title">Get your free Crypto Card\nin minutes</string>
<string name="tangempay_onboarding_security_title">Built-in security</string>
<string name="tangempay_onboarding_security_description">Your card details are protected — full control in the app.</string>
<string name="tangempay_onboarding_purchases_title">Everyday purchases</string>
<string name="tangempay_onboarding_purchases_description">Use your USDC balance to pay for everyday purchases seamlessly.</string>
<string name="tangempay_onboarding_pay_title">Apple Pay &amp; Google Pay</string>
<string name="tangempay_onboarding_pay_description">Add to your wallet and pay with your phone anywhere.</string>
<string name="tangempay_onboarding_get_card_button_text">Get card</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain is unreachable. Try later</string>
<string name="wallet_balance_missing_derivation">Scan card or ring</string>
<string name="wallet_been_activated_message">This wallet has already been activated earlier.\nIf it was not done by you, please contact support.\nTangem never sells wallets along with pre-generated access codes.</string>
@ -1732,8 +1733,8 @@
<string name="yield_module_promo_screen_how_it_works_button_title">How it works?</string>
<string name="yield_module_promo_screen_self_custodial_subtitle">Aave is trusted by millions worldwide. Total lended value is $10.4B.</string>
<string name="yield_module_promo_screen_self_custodial_title">Decentralized and self-custodial</string>
<string name="yield_module_promo_screen_terms_disclaimer">By using service, you agree with provider</string>
<string name="yield_module_promo_screen_title">Earn %s% yearly</string>
<string name="yield_module_promo_screen_terms_disclaimer">By using service, you agree with provider %1$s and %2$s</string>
<string name="yield_module_promo_screen_title">Earn %s%% yearly</string>
<string name="yield_module_promo_screen_variable_rate_info">Aave • Variable Interest Rate</string>
<string name="yield_module_provider">Aave</string>
<string name="yield_module_rate_info_sheet_chart_average">Avg %s</string>

View file

@ -2,19 +2,22 @@ package com.tangem.core.ui.components.label
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.TextReference
@ -48,8 +51,18 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
},
)
val iconColor by animateColorAsState(
targetValue = when (state.style) {
LabelStyle.ACCENT -> TangemTheme.colors.icon.accent
LabelStyle.REGULAR -> TangemTheme.colors.icon.informative
LabelStyle.WARNING -> TangemTheme.colors.icon.warning
},
)
AnimatedContent(targetState = state.text) { text ->
Box(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier
.padding(horizontal = 4.dp)
.background(
@ -63,6 +76,15 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
style = TangemTheme.typography.caption1,
color = textColor,
)
AnimatedVisibility(state.icon != null) {
val wrappedIcon = remember(this) { requireNotNull(state.icon) }
Icon(
imageVector = ImageVector.vectorResource(wrappedIcon),
tint = iconColor,
contentDescription = null,
modifier = Modifier.size(16.dp),
)
}
}
}
}
@ -73,6 +95,7 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
private fun LabelPreview() {
TangemThemePreview {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp),
) {
Label(
@ -81,24 +104,39 @@ private fun LabelPreview() {
style = LabelStyle.REGULAR,
),
)
Spacer(modifier = Modifier.height(8.dp))
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
),
)
Spacer(modifier = Modifier.height(8.dp))
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Regular Label"),
style = LabelStyle.REGULAR,
icon = R.drawable.ic_information_24,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Accent Label"),
style = LabelStyle.ACCENT,
icon = R.drawable.ic_information_24,
),
)
Label(
state = LabelUM(
text = TextReference.Str("Warning Label"),
style = LabelStyle.WARNING,
icon = R.drawable.ic_information_24,
),
)
}
}
}

View file

@ -1,10 +1,13 @@
package com.tangem.core.ui.components.label.entity
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
data class LabelUM(
val text: TextReference,
val style: LabelStyle,
@DrawableRes val icon: Int? = null,
val onIconClick: (() -> Unit)? = null,
)
enum class LabelStyle {

View file

@ -10,4 +10,13 @@ interface ComposableBottomSheetComponent {
@Composable
fun BottomSheet()
}
fun getEmptyComposableBottomSheetComponent() = EmptyComposableBottomSheetComponent
object EmptyComposableBottomSheetComponent : ComposableBottomSheetComponent {
override fun dismiss() {}
@Composable
override fun BottomSheet() {}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.058,1.75C14.248,1.75 15.968,1.75 17.312,1.931C18.688,2.116 19.781,2.503 20.639,3.361C21.497,4.22 21.884,5.312 22.069,6.688C22.25,8.032 22.25,9.752 22.25,11.942V12.058C22.25,14.248 22.25,15.968 22.069,17.312C21.884,18.688 21.497,19.781 20.639,20.639C19.781,21.497 18.688,21.884 17.312,22.069C15.968,22.25 14.248,22.25 12.058,22.25H11.942C9.752,22.25 8.032,22.25 6.688,22.069C5.312,21.884 4.22,21.497 3.361,20.639C2.503,19.781 2.116,18.688 1.931,17.312C1.75,15.968 1.75,14.248 1.75,12.058V11.942C1.75,9.752 1.75,8.032 1.931,6.688C2.116,5.312 2.503,4.22 3.361,3.361C4.22,2.503 5.312,2.116 6.688,1.931C8.032,1.75 9.752,1.75 11.942,1.75H12.058ZM7,15.25C6.586,15.25 6.25,15.586 6.25,16V18C6.25,18.414 6.586,18.75 7,18.75C7.414,18.75 7.75,18.414 7.75,18V16C7.75,15.586 7.414,15.25 7,15.25ZM12,14.25C11.586,14.25 11.25,14.586 11.25,15V18C11.25,18.414 11.586,18.75 12,18.75C12.414,18.75 12.75,18.414 12.75,18V15C12.75,14.586 12.414,14.25 12,14.25ZM17,12.25C16.586,12.25 16.25,12.586 16.25,13V18C16.25,18.414 16.586,18.75 17,18.75C17.414,18.75 17.75,18.414 17.75,18V13C17.75,12.586 17.414,12.25 17,12.25ZM15.858,5.252C15.654,5.261 15.416,5.301 15.179,5.34C15.168,5.342 15.157,5.344 15.146,5.346L13.878,5.555C13.469,5.622 13.192,6.008 13.26,6.417C13.327,6.826 13.713,7.103 14.122,7.035L14.846,6.915C12.963,9.557 9.953,10.75 6,10.75C5.586,10.75 5.25,11.086 5.25,11.5C5.25,11.914 5.586,12.25 6,12.25C10.322,12.25 13.898,10.906 16.132,7.694L16.279,8.207C16.394,8.605 16.809,8.835 17.207,8.721C17.605,8.606 17.835,8.191 17.721,7.793L17.403,6.683C17.4,6.671 17.396,6.66 17.393,6.648C17.335,6.446 17.272,6.223 17.19,6.041C17.089,5.819 16.93,5.582 16.645,5.42C16.369,5.263 16.09,5.241 15.858,5.252Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13.608,1.147C14.314,1.448 14.797,2.163 14.797,3.017L14.798,9.785C14.798,9.895 14.887,9.985 14.998,9.985H18.099C18.985,9.985 19.595,10.583 19.847,11.21C20.097,11.837 20.064,12.642 19.563,13.285L12.564,22.268C12.003,22.988 11.12,23.163 10.392,22.852C9.685,22.551 9.202,21.837 9.202,20.983L9.202,14.215C9.202,14.104 9.112,14.015 9.002,14.015H5.9C5.014,14.015 4.404,13.417 4.153,12.789C3.902,12.163 3.936,11.358 4.437,10.715L11.435,1.732C11.996,1.011 12.879,0.837 13.608,1.147Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,0.59C18.302,0.59 23.41,5.698 23.41,12C23.41,18.302 18.302,23.41 12,23.41C5.698,23.41 0.59,18.302 0.59,12C0.59,5.698 5.698,0.59 12,0.59ZM18.057,10.834C17.616,10.857 17.277,11.233 17.299,11.674C17.375,13.168 16.842,14.685 15.702,15.825C13.794,17.733 10.829,17.937 8.693,16.438L9.295,16.381C9.734,16.339 10.057,15.948 10.016,15.509C9.973,15.069 9.581,14.746 9.142,14.788L6.631,15.028C6.191,15.071 5.868,15.462 5.91,15.901L6.15,18.413C6.192,18.853 6.584,19.175 7.023,19.133C7.463,19.09 7.785,18.701 7.743,18.262L7.688,17.688C10.454,19.691 14.342,19.448 16.834,16.957C18.307,15.484 18.995,13.52 18.896,11.593C18.874,11.152 18.498,10.811 18.057,10.834ZM16.77,4.914C16.33,4.956 16.008,5.347 16.05,5.786L16.104,6.361C13.338,4.357 9.451,4.599 6.959,7.091C5.486,8.564 4.797,10.528 4.896,12.455C4.918,12.896 5.294,13.235 5.735,13.213C6.176,13.19 6.516,12.815 6.494,12.374C6.418,10.88 6.951,9.362 8.091,8.222C9.999,6.314 12.965,6.112 15.1,7.61L14.497,7.667C14.058,7.709 13.736,8.099 13.777,8.539C13.82,8.979 14.211,9.302 14.65,9.26L17.161,9.019C17.601,8.977 17.924,8.586 17.882,8.146L17.643,5.635C17.601,5.195 17.209,4.872 16.77,4.914Z"
android:fillColor="#0099FF"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11.999,1.25C13.68,1.25 15.048,1.818 16.24,2.436C16.601,2.622 16.936,2.807 17.256,2.983C18.039,3.415 18.733,3.797 19.495,4.04C19.942,4.183 20.338,4.31 20.619,4.42C20.883,4.523 21.198,4.665 21.421,4.913C21.621,5.136 21.724,5.385 21.792,5.624C21.854,5.841 21.907,6.135 21.958,6.417C23.196,13.282 20.492,19.787 13.882,22.318C13.232,22.567 12.753,22.75 12.002,22.75C11.251,22.75 10.773,22.567 10.123,22.318C3.513,19.788 0.806,13.283 2.044,6.417C2.095,6.135 2.147,5.841 2.209,5.624C2.277,5.385 2.381,5.136 2.581,4.913C2.804,4.666 3.118,4.523 3.382,4.42C3.663,4.31 4.06,4.183 4.507,4.04C5.268,3.797 5.962,3.415 6.744,2.983L7.236,2.713C7.405,2.621 7.579,2.529 7.759,2.436C8.951,1.818 10.318,1.25 11.999,1.25ZM17.942,8.666C17.758,8.146 17.187,7.873 16.666,8.058C15.788,8.369 14.948,8.961 14.212,9.607C13.465,10.262 12.767,11.023 12.174,11.734C11.735,12.261 11.345,12.769 11.03,13.199C10.742,12.854 10.454,12.601 10.18,12.418C9.905,12.235 9.552,12 9,12C8.448,12 8,12.448 8,13C8,13.521 8.398,13.949 8.906,13.996C9.11,14.015 9.665,14.566 10.106,15.447C10.267,15.77 10.589,15.981 10.949,15.999C11.309,16.017 11.651,15.84 11.844,15.536C11.849,15.529 12.193,15.013 12.357,14.78C12.689,14.311 13.157,13.677 13.709,13.016C14.263,12.352 14.888,11.675 15.53,11.111C16.183,10.539 16.8,10.132 17.334,9.943C17.855,9.759 18.127,9.187 17.942,8.666Z"
android:fillColor="#0099FF"/>
</vector>

View file

@ -0,0 +1,16 @@
package com.tangem.features.yield.supply.api
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
interface YieldSupplyPromoComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
val currency: CryptoCurrency,
)
interface Factory : ComponentFactory<Params, YieldSupplyPromoComponent>
}

View file

@ -21,6 +21,7 @@ dependencies {
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.navigation)
/** Common */
implementation(projects.common.ui)

View file

@ -0,0 +1,56 @@
package com.tangem.features.yield.supply.impl.promo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.getEmptyComposableBottomSheetComponent
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import com.tangem.features.yield.supply.impl.promo.model.YieldSupplyPromoModel
import com.tangem.features.yield.supply.impl.promo.ui.YieldSupplyPromoContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultYieldSupplyPromoComponent @AssistedInject constructor(
@Assisted private val appComponentContext: AppComponentContext,
@Assisted private val params: YieldSupplyPromoComponent.Params,
) : YieldSupplyPromoComponent, AppComponentContext by appComponentContext {
private val model: YieldSupplyPromoModel = getOrCreateModel(params = params)
private val bottomSheetSlot = childSlot(
source = model.bottomSheetNavigation,
serializer = null,
handleBackButton = false,
childFactory = { _, _ -> bottomSheetChild() },
)
@Composable
override fun Content(modifier: Modifier) {
val state = model.uiState
val bottomSheet by bottomSheetSlot.subscribeAsState()
YieldSupplyPromoContent(
yieldSupplyPromoUM = state,
clickIntents = model,
modifier = modifier,
)
bottomSheet.child?.instance?.BottomSheet()
}
private fun bottomSheetChild(): ComposableBottomSheetComponent = getEmptyComposableBottomSheetComponent()
@AssistedFactory
interface Factory : YieldSupplyPromoComponent.Factory {
override fun create(
context: AppComponentContext,
params: YieldSupplyPromoComponent.Params,
): DefaultYieldSupplyPromoComponent
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.yield.supply.impl.promo
internal enum class YieldSupplyPromoConfig {
Apy,
Action,
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.yield.supply.impl.promo.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.yield.supply.impl.promo.DefaultYieldSupplyPromoComponent
import com.tangem.features.yield.supply.impl.promo.model.YieldSupplyPromoModel
import com.tangem.features.yield.supply.api.YieldSupplyPromoComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface YieldSupplyPromoBindsModule {
@Binds
@Singleton
fun provideYieldSupplyPromoComponentFactory(
impl: DefaultYieldSupplyPromoComponent.Factory,
): YieldSupplyPromoComponent.Factory
}
@Module
@InstallIn(ModelComponent::class)
internal interface YieldSupplyPromoModelModule {
@Binds
@IntoMap
@ClassKey(YieldSupplyPromoModel::class)
fun provideYieldSupplyPromoModel(impl: YieldSupplyPromoModel): Model
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.yield.supply.impl.promo.entity
import com.tangem.core.ui.extensions.TextReference
data class YieldSupplyPromoUM(
val tosLink: String,
val policyLink: String,
val title: TextReference,
)

View file

@ -0,0 +1,14 @@
package com.tangem.features.yield.supply.impl.promo.model
internal interface YieldSupplyPromoClickIntents {
fun onBackClick()
fun onApyInfoClick()
fun onHowItWorksClick()
fun onStartEarningClick()
fun onUrlClick(url: String)
}

View file

@ -0,0 +1,51 @@
package com.tangem.features.yield.supply.impl.promo.model
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.promo.YieldSupplyPromoConfig
import com.tangem.features.yield.supply.impl.promo.entity.YieldSupplyPromoUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
@ModelScoped
internal class YieldSupplyPromoModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val urlOpener: UrlOpener,
private val appRouter: AppRouter,
) : Model(), YieldSupplyPromoClickIntents {
val uiState: YieldSupplyPromoUM = YieldSupplyPromoUM(
tosLink = "https://tangem.com/terms-of-service/", // TODO replace with real link
policyLink = "https://tangem.com/privacy-policy/", // TODO replace with real link
title = resourceReference(R.string.yield_module_promo_screen_title, wrappedList("5.3")),
)
val bottomSheetNavigation: SlotNavigation<YieldSupplyPromoConfig> = SlotNavigation()
override fun onBackClick() {
appRouter.pop()
}
override fun onApyInfoClick() {
bottomSheetNavigation.activate(YieldSupplyPromoConfig.Apy)
}
override fun onUrlClick(url: String) {
urlOpener.openUrl(url)
}
override fun onHowItWorksClick() {
urlOpener.openUrl("https://tangem.com/") // TODO replace with real link
}
override fun onStartEarningClick() {
bottomSheetNavigation.activate(YieldSupplyPromoConfig.Action)
}
}

View file

@ -0,0 +1,239 @@
package com.tangem.features.yield.supply.impl.promo.ui
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withLink
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.label.Label
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.yield.supply.impl.R
import com.tangem.features.yield.supply.impl.promo.entity.YieldSupplyPromoUM
import com.tangem.features.yield.supply.impl.promo.model.YieldSupplyPromoClickIntents
import com.tangem.utils.StringsSigns
@Suppress("MagicNumber")
@Composable
internal fun YieldSupplyPromoContent(
yieldSupplyPromoUM: YieldSupplyPromoUM,
clickIntents: YieldSupplyPromoClickIntents,
modifier: Modifier = Modifier,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.background(color = TangemTheme.colors.background.tertiary)
.fillMaxWidth()
.imePadding()
.systemBarsPadding()
.padding(horizontal = 16.dp),
) {
YieldStatusAppBar(
onBackClick = clickIntents::onBackClick,
onHowItWorksClick = clickIntents::onHowItWorksClick,
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 20.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_analytics_up_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
modifier = Modifier
.background(TangemTheme.colors.icon.accent.copy(0.1f), CircleShape)
.padding(12.dp)
.size(32.dp),
)
SpacerH(20.dp)
Text(
text = yieldSupplyPromoUM.title.resolveReference(),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
SpacerH8()
Label(
state = LabelUM(
text = resourceReference(R.string.yield_module_promo_screen_variable_rate_info),
style = LabelStyle.REGULAR,
icon = R.drawable.ic_information_24,
onIconClick = clickIntents::onApyInfoClick,
),
)
SpacerH32()
PromoItems()
}
SpacerHMax()
YieldSupplyTosText(
tosLink = yieldSupplyPromoUM.tosLink,
policyLink = yieldSupplyPromoUM.policyLink,
onClick = clickIntents::onUrlClick,
)
PrimaryButton(
text = stringResourceSafe(R.string.yield_module_start_earning),
onClick = clickIntents::onStartEarningClick,
modifier = Modifier.fillMaxWidth(),
)
SpacerH8()
}
}
@Composable
private fun YieldStatusAppBar(onBackClick: () -> Unit, onHowItWorksClick: () -> Unit) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_back_24),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
modifier = Modifier.clickable(onClick = onBackClick),
)
SpacerWMax()
Text(
text = stringResourceSafe(R.string.yield_module_promo_screen_how_it_works_button_title),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier.clickable(onClick = onHowItWorksClick),
)
}
}
@Composable
private fun PromoItems() {
PromoItem(
icon = R.drawable.ic_flash_new_24,
title = resourceReference(R.string.yield_module_promo_screen_cash_out_title),
subtitle = resourceReference(R.string.yield_module_promo_screen_cash_out_subtitle),
)
SpacerH24()
PromoItem(
icon = R.drawable.ic_repeat_24,
title = resourceReference(R.string.yield_module_promo_screen_auto_balance_title),
subtitle = resourceReference(R.string.yield_module_promo_screen_auto_balance_subtitle),
)
SpacerH24()
PromoItem(
icon = R.drawable.ic_security_check_24,
title = resourceReference(R.string.yield_module_promo_screen_self_custodial_title),
subtitle = resourceReference(R.string.yield_module_promo_screen_self_custodial_subtitle),
)
}
@Composable
private fun PromoItem(@DrawableRes icon: Int, title: TextReference, subtitle: TextReference) {
Row(
horizontalArrangement = Arrangement.spacedBy(20.dp),
) {
Icon(
imageVector = ImageVector.vectorResource(icon),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
Column(verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Composable
private fun YieldSupplyTosText(tosLink: String, policyLink: String, onClick: (String) -> Unit) {
val tosTitle = stringResourceSafe(R.string.common_terms_of_use)
val policyTitle = stringResourceSafe(R.string.common_privacy_policy)
val fullString = stringResourceSafe(id = R.string.yield_module_promo_screen_terms_disclaimer, tosTitle, policyTitle)
val tosIndex = fullString.indexOf(tosTitle)
val policyIndex = fullString.indexOf(policyTitle)
Text(
text = buildAnnotatedString {
append(StringsSigns.POINT_SIGN)
appendSpace()
append(fullString.substring(0, tosIndex))
withLink(
link = LinkAnnotation.Clickable(
tag = "TOS_TAG",
linkInteractionListener = { onClick(tosLink) },
),
block = {
appendColored(
text = fullString.substring(tosIndex, tosIndex + tosTitle.length),
color = TangemTheme.colors.text.accent,
)
},
)
append(fullString.substring(tosIndex + tosTitle.length, policyIndex))
withLink(
link = LinkAnnotation.Clickable(
tag = "POLICY_TAG",
linkInteractionListener = { onClick(policyLink) },
),
block = {
appendColored(
text = fullString.substring(policyIndex, policyIndex + policyTitle.length),
color = TangemTheme.colors.text.accent,
)
},
)
},
textAlign = TextAlign.Center,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360, heightDp = 724)
@Preview(showBackground = true, widthDp = 360, heightDp = 724, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun YieldSupplyPromoContent_Preview() {
TangemThemePreview {
YieldSupplyPromoContent(
yieldSupplyPromoUM = YieldSupplyPromoUM(
tosLink = "https://tangem.com/terms-of-service/",
policyLink = "https://tangem.com/privacy-policy/",
title = resourceReference(R.string.yield_module_promo_screen_title, wrappedList("5.3")),
),
clickIntents = object : YieldSupplyPromoClickIntents {
override fun onBackClick() {}
override fun onApyInfoClick() {}
override fun onHowItWorksClick() {}
override fun onStartEarningClick() {}
override fun onUrlClick(url: String) {}
},
)
}
}
// endregion