Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-12 16:32:45 +05:00
parent c5e437586b
commit c5de0ebfdc
13 changed files with 452 additions and 2 deletions

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