Updated on 2026-08-14

This commit is contained in:
Tangem 2022-11-23 13:49:15 +03:00
parent 4a89075d5a
commit e17ff1b4c2
19 changed files with 391 additions and 177 deletions

View file

@ -175,6 +175,7 @@
<string name="onboarding_twin_exit_warning">Процесс связывания карт частично завершен. Вы не можете выйти из него сейчас.</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Блокчейн недоступен. Попробуй позже.</string>
<string name="warning_button_ok">Хорошо, понял!</string>
<string name="onboarding_success">Успешно!</string>
<string name="onboarding_getting_started">Начнем</string>
<string name="onboarding_button_backup_now">Выполнить резервное копирование</string>
<string name="onboarding_button_skip_backup">Оставить на потом</string>

View file

@ -153,6 +153,7 @@
<string name="warning_button_ok" translatable="false">Ok, Got it!</string>
<string name="onboarding_success" translatable="false">Success!</string>
<string name="onboarding_getting_started" translatable="false">Getting started</string>
<string name="onboarding_button_backup_now" translatable="false">Backup now</string>
@ -222,7 +223,6 @@
<string name="wallet_connect_bnb_sign_message" translatable="false">Dapp %s, requesting to\n
sign BNB transaction
\n

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.components.appbar
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@ -18,22 +19,6 @@ import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
@Preview(widthDp = 360, heightDp = 56, showBackground = true)
@Composable
fun PreviewAppBarWithBackButtonInLightTheme() {
TangemTheme(isDark = false) {
AppBarWithBackButton(text = "Title", onBackClick = {})
}
}
@Preview(widthDp = 360, heightDp = 56, showBackground = true)
@Composable
fun PreviewAppBarWithBackButtonInDarkTheme() {
TangemTheme(isDark = true) {
AppBarWithBackButton(text = "Title", onBackClick = {})
}
}
/**
* App bar with back button and optional title
*
@ -47,8 +32,9 @@ fun PreviewAppBarWithBackButtonInDarkTheme() {
fun AppBarWithBackButton(text: String? = null, onBackClick: () -> Unit) {
Row(
modifier = Modifier
.padding(all = dimensionResource(R.dimen.spacing16))
.fillMaxWidth(),
.background(MaterialTheme.colors.primary)
.fillMaxWidth()
.padding(all = dimensionResource(R.dimen.spacing16)),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.spacing16)),
verticalAlignment = Alignment.CenterVertically,
) {
@ -69,4 +55,20 @@ fun AppBarWithBackButton(text: String? = null, onBackClick: () -> Unit) {
)
}
}
}
@Preview(widthDp = 360, heightDp = 56, showBackground = true)
@Composable
fun PreviewAppBarWithBackButtonInLightTheme() {
TangemTheme(isDark = false) {
AppBarWithBackButton(text = "Title", onBackClick = {})
}
}
@Preview(widthDp = 360, heightDp = 56, showBackground = true)
@Composable
fun PreviewAppBarWithBackButtonInDarkTheme() {
TangemTheme(isDark = true) {
AppBarWithBackButton(text = "Title", onBackClick = {})
}
}

View file

@ -2,4 +2,5 @@
<resources>
<string name="common_share">Поделиться</string>
<string name="common_copy">Копировать</string>
<string name="common_terms_and_conditions">условия участия</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="warning_button_ok">Хорошо, понял!</string>
</resources>

View file

@ -13,7 +13,9 @@
<!-- endregion Elevation -->
<!-- region Radius -->
<dimen name="radius2">2dp</dimen>
<dimen name="radius6">6dp</dimen>
<dimen name="radius8">8dp</dimen>
<dimen name="radius12">12dp</dimen>
<dimen name="radius16">16dp</dimen>
<!-- endregion Radius -->
@ -43,12 +45,14 @@
<dimen name="spacing8">8dp</dimen>
<dimen name="spacing10">10dp</dimen>
<dimen name="spacing12">12dp</dimen>
<dimen name="spacing14">14dp</dimen>
<dimen name="spacing16">16dp</dimen>
<dimen name="spacing24">24dp</dimen>
<dimen name="spacing32">32dp</dimen>
<dimen name="spacing44">44dp</dimen>
<dimen name="spacing50">50dp</dimen>
<dimen name="spacing54">54dp</dimen>
<dimen name="radius2">2dp</dimen>
<dimen name="spacing56">56dp</dimen>
<dimen name="spacing92">92dp</dimen>
<!-- endregion Spacing -->
</resources>

View file

@ -2,4 +2,5 @@
<resources>
<string name="common_share" translatable="false">Share</string>
<string name="common_copy" translatable="false">Copy</string>
<string name="common_terms_and_conditions">terms and conditions</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="warning_button_ok">Ok, Got it!</string>
</resources>

View file

@ -5,19 +5,22 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import com.tangem.feature.referral.router.ReferralRouter
import com.tangem.feature.referral.ui.ReferralScreen
import com.tangem.feature.referral.viewmodels.ReferralViewModel
import dagger.hilt.android.AndroidEntryPoint
import java.lang.ref.WeakReference
@AndroidEntryPoint
class ReferralFragment : Fragment() {
private val viewModel by viewModels<ReferralViewModel>().apply {
// TODO("[REDACTED_JIRA]") this.value.setOnBackClicked()
}
private val viewModel by viewModels<ReferralViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
viewModel.setRouter(ReferralRouter(fragmentManager = WeakReference(parentFragmentManager)))
return ComposeView(inflater.context).apply {
setContent {
ReferralScreen(stateHolder = viewModel.uiState)

View file

@ -1,15 +1,16 @@
package com.tangem.feature.referral.models
data class ReferralStateHolder(
internal data class ReferralStateHolder(
val headerState: HeaderState,
val referralInfoState: ReferralInfoState,
val errorToast: ErrorToast,
val errorSnackbar: ErrorSnackbar? = null,
) {
data class HeaderState(val onBackClicked: () -> Unit)
sealed interface ReferralInfoContentState {
val award: String
val networkName: String
val discount: String
val url: String
}
@ -17,15 +18,18 @@ data class ReferralStateHolder(
sealed interface ReferralInfoState {
data class ParticipantContent(
override val award: String,
override val networkName: String,
val address: String,
override val discount: String,
val purchasedWalletCount: Int,
val code: String,
val shareLink: String,
override val url: String,
) : ReferralInfoState, ReferralInfoContentState
data class NonParticipantContent(
override val award: String,
override val networkName: String,
override val discount: String,
override val url: String,
val onParticipateClicked: () -> Unit,
@ -34,8 +38,8 @@ data class ReferralStateHolder(
object Loading : ReferralInfoState
}
data class ErrorToast(
val visibility: Boolean,
val changeVisibility: () -> Unit
data class ErrorSnackbar(
val throwable: Throwable,
val onOkClicked: () -> Unit,
)
}

View file

@ -0,0 +1,11 @@
package com.tangem.feature.referral.router
import androidx.fragment.app.FragmentManager
import java.lang.ref.WeakReference
internal class ReferralRouter(private val fragmentManager: WeakReference<FragmentManager>) {
fun back() {
fragmentManager.get()?.popBackStack()
}
}

View file

@ -29,7 +29,7 @@ import com.tangem.feature.referral.presentation.R
* @param url link to the html page
*/
@Composable
fun AgreementBottomSheetContent(url: String) {
internal fun AgreementBottomSheetContent(url: String) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colors.secondary)

View file

@ -22,28 +22,8 @@ import com.tangem.core.ui.res.TextColorType
import com.tangem.core.ui.res.textColor
import com.tangem.feature.referral.presentation.R
@Preview(widthDp = 360, showBackground = true)
@Composable
fun Preview_AgreementText_InLightTheme() {
TangemTheme(isDark = false) {
Box(modifier = Modifier.background(MaterialTheme.colors.primary)) {
AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClicked = {})
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
fun Preview_AgreementText_InDarkTheme() {
TangemTheme(isDark = true) {
Box(modifier = Modifier.background(MaterialTheme.colors.primary)) {
AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClicked = {})
}
}
}
@Composable
fun AgreementText(@StringRes firstPartResId: Int, onClicked: () -> Unit) {
internal fun AgreementText(@StringRes firstPartResId: Int, onClicked: () -> Unit) {
val agreementText = annotatedAgreementString(firstPart = stringResource(firstPartResId))
ClickableText(
text = agreementText,
@ -74,4 +54,24 @@ private fun annotatedAgreementString(firstPart: String): AnnotatedString {
append(" ${stringResource(id = R.string.referral_tos_suffix)}")
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
fun Preview_AgreementText_InLightTheme() {
TangemTheme(isDark = false) {
Box(modifier = Modifier.background(MaterialTheme.colors.primary)) {
AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClicked = {})
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Composable
fun Preview_AgreementText_InDarkTheme() {
TangemTheme(isDark = true) {
Box(modifier = Modifier.background(MaterialTheme.colors.primary)) {
AgreementText(firstPartResId = R.string.referral_tos_not_enroled_prefix, onClicked = {})
}
}
}

View file

@ -14,7 +14,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.referral.presentation.R
@Composable
fun NonParticipateBottomBlock(onAgreementClicked: () -> Unit, onParticipateClicked: () -> Unit) {
internal fun NonParticipateBottomBlock(onAgreementClicked: () -> Unit, onParticipateClicked: () -> Unit) {
Column {
AgreementText(
firstPartResId = R.string.referral_tos_not_enroled_prefix,

View file

@ -1,5 +1,6 @@
package com.tangem.feature.referral.ui
import android.content.Context
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -14,8 +15,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@ -29,10 +32,12 @@ import com.tangem.core.ui.res.textColor
import com.tangem.feature.referral.presentation.R
@Composable
fun ParticipateBottomBlock(
internal fun ParticipateBottomBlock(
purchasedWalletCount: Int,
code: String,
shareLink: String,
onAgreementClicked: () -> Unit,
showCopySnackbar: () -> Unit,
) {
Column(
modifier = Modifier
@ -51,7 +56,7 @@ fun ParticipateBottomBlock(
),
)
PersonalCodeCard(code = code)
AdditionalButtons(code = code)
AdditionalButtons(shareLink = shareLink, showCopySnackbar = showCopySnackbar)
AgreementText(firstPartResId = R.string.referral_tos_enroled_prefix, onClicked = onAgreementClicked)
}
}
@ -89,9 +94,9 @@ private fun PersonalCodeCard(code: String) {
}
@Composable
private fun AdditionalButtons(code: String) {
private fun AdditionalButtons(shareLink: String, showCopySnackbar: () -> Unit) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
val hapticFeedback = LocalHapticFeedback.current
Row(
modifier = Modifier.fillMaxWidth(),
@ -101,25 +106,36 @@ private fun AdditionalButtons(code: String) {
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_copy),
iconResId = R.drawable.ic_copy,
onClicked = { clipboardManager.setText(AnnotatedString(code)) },
onClicked = {
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
clipboardManager.setText(AnnotatedString(shareLink))
showCopySnackbar()
},
)
val context = LocalContext.current
PrimaryStartIconButton(
modifier = Modifier.weight(1f),
text = stringResource(id = R.string.common_share),
iconResId = R.drawable.ic_share,
onClicked = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, code)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(context, shareIntent, null)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(shareLink)
},
)
}
}
private fun Context.shareText(text: String) {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(this, shareIntent, null)
}
@Preview(widthDp = 360, showBackground = true)
@Composable
fun Preview_ParticipateBottomBlock_InLightTheme() {
@ -128,7 +144,9 @@ fun Preview_ParticipateBottomBlock_InLightTheme() {
ParticipateBottomBlock(
purchasedWalletCount = 3,
code = "x4JdK",
shareLink = "",
onAgreementClicked = {},
showCopySnackbar = {},
)
}
}
@ -142,7 +160,9 @@ fun Preview_ParticipateBottomBlock_InDarkTheme() {
ParticipateBottomBlock(
purchasedWalletCount = 3,
code = "x4JdK",
shareLink = "",
onAgreementClicked = {},
showCopySnackbar = {},
)
}
}

View file

@ -1,38 +1,50 @@
package com.tangem.feature.referral.ui
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetState
import androidx.compose.material.BottomSheetValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarDuration
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.SnackbarResult
import androidx.compose.material.Text
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -41,8 +53,10 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.VerticalSpacer
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.Black
import com.tangem.core.ui.res.ButtonColorType
import com.tangem.core.ui.res.IconColorType
import com.tangem.core.ui.res.TangemColorPalette
@ -52,6 +66,7 @@ import com.tangem.core.ui.res.buttonColor
import com.tangem.core.ui.res.iconColor
import com.tangem.core.ui.res.textColor
import com.tangem.feature.referral.models.ReferralStateHolder
import com.tangem.feature.referral.models.ReferralStateHolder.ErrorSnackbar
import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoContentState
import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState
import com.tangem.feature.referral.presentation.R
@ -65,7 +80,7 @@ import kotlinx.coroutines.launch
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun ReferralScreen(stateHolder: ReferralStateHolder) {
internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed),
@ -75,7 +90,11 @@ fun ReferralScreen(stateHolder: ReferralStateHolder) {
BottomSheetScaffold(
sheetContent = {
AgreementBottomSheetContent(
url = (stateHolder.referralInfoState as? ReferralInfoContentState)?.url ?: "",
url = when (val state = stateHolder.referralInfoState) {
is ReferralInfoState.NonParticipantContent -> state.url
is ReferralInfoState.ParticipantContent -> state.url
is ReferralInfoState.Loading -> ""
},
)
},
scaffoldState = bottomSheetScaffoldState,
@ -100,66 +119,92 @@ fun ReferralScreen(stateHolder: ReferralStateHolder) {
)
},
)
}
}
val context = LocalContext.current
SideEffect {
if (stateHolder.errorToast.visibility) {
//TODO("[REDACTED_JIRA]")
Toast.makeText(context, "", Toast.LENGTH_LONG).show()
stateHolder.errorToast.changeVisibility()
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun ReferralContent(stateHolder: ReferralStateHolder, onAgreementClicked: () -> Unit) {
val isCopyButtonPressed = remember { mutableStateOf(false) }
Box {
CompositionLocalProvider(LocalOverscrollConfiguration provides null) {
LazyColumn(
modifier = Modifier
.background(color = MaterialTheme.colors.primary)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
stickyHeader {
AppBarWithBackButton(
text = stringResource(R.string.details_referral_title),
onBackClick = stateHolder.headerState.onBackClicked,
)
}
item { Header() }
item {
ReferralInfo(
stateHolder = stateHolder,
onAgreementClicked = onAgreementClicked,
showCopySnackbar = { isCopyButtonPressed.value = true },
)
}
}
}
ErrorSnackbarHost(errorSnackbar = stateHolder.errorSnackbar)
CopySnackbarHost(isCopyButtonPressed = isCopyButtonPressed)
}
}
@Composable
fun ReferralContent(stateHolder: ReferralStateHolder, onAgreementClicked: () -> Unit) {
Column(
modifier = Modifier
.background(color = MaterialTheme.colors.primary)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Header(stateHolder = stateHolder)
private fun Header() {
Column {
Image(
painter = painterResource(R.drawable.ill_businessman_3d),
contentDescription = null,
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.spacing32))
.fillMaxWidth()
.height(dimensionResource(R.dimen.size200)),
)
VerticalSpacer(spaceResId = R.dimen.spacing24)
Text(
text = stringResource(id = R.string.referral_title),
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing50)),
color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1),
textAlign = TextAlign.Center,
maxLines = 2,
style = MaterialTheme.typography.h2,
)
VerticalSpacer(spaceResId = R.dimen.spacing16)
ReferralInfo(stateHolder = stateHolder, onAgreementClicked = onAgreementClicked)
}
}
@Composable
private fun ColumnScope.Header(stateHolder: ReferralStateHolder) {
AppBar(stateHolder = stateHolder)
Image(
painter = painterResource(R.drawable.ill_businessman_3d),
contentDescription = null,
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.spacing32))
.fillMaxWidth()
.height(dimensionResource(R.dimen.size200)),
)
VerticalSpacer(spaceResId = R.dimen.spacing24)
Text(
text = stringResource(id = R.string.referral_title),
modifier = Modifier.padding(horizontal = dimensionResource(R.dimen.spacing50)),
color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY1),
textAlign = TextAlign.Center,
maxLines = 2,
style = MaterialTheme.typography.h2,
)
}
@Composable
fun ReferralInfo(stateHolder: ReferralStateHolder, onAgreementClicked: () -> Unit) {
private fun ReferralInfo(
stateHolder: ReferralStateHolder,
onAgreementClicked: () -> Unit,
showCopySnackbar: () -> Unit,
) {
when (val state = stateHolder.referralInfoState) {
is ReferralInfoState.ParticipantContent -> {
Conditions(state = state)
ParticipantBottomBlock(state = state, onAgreementClicked = onAgreementClicked)
ParticipateBottomBlock(
purchasedWalletCount = state.purchasedWalletCount,
code = state.code,
shareLink = state.shareLink,
onAgreementClicked = onAgreementClicked,
showCopySnackbar = showCopySnackbar,
)
}
is ReferralInfoState.NonParticipantContent -> {
Conditions(state = state)
VerticalSpacer(spaceResId = R.dimen.spacing44)
NonParticipantBottomBlock(state = state, onAgreementClicked = onAgreementClicked)
NonParticipateBottomBlock(
onAgreementClicked = onAgreementClicked,
onParticipateClicked = state.onParticipateClicked,
)
}
is ReferralInfoState.Loading -> {
LoadingCondition(iconResId = R.drawable.ic_tether)
@ -169,38 +214,34 @@ fun ReferralInfo(stateHolder: ReferralStateHolder, onAgreementClicked: () -> Uni
}
}
@Composable
private fun AppBar(stateHolder: ReferralStateHolder) {
AppBarWithBackButton(
text = stringResource(R.string.details_referral_title),
onBackClick = stateHolder.headerState.onBackClicked,
)
}
@Composable
private fun Conditions(state: ReferralInfoContentState) {
ConditionForYou(state = state)
VerticalSpacer(spaceResId = R.dimen.spacing32)
ConditionForYourFriend(state = state)
ConditionForYourFriend(discount = state.discount)
}
@Composable
private fun ConditionForYou(state: ReferralInfoContentState) {
Condition(iconResId = R.drawable.ic_tether) {
when (state) {
is ReferralInfoState.ParticipantContent -> InfoForYou(award = state.award, address = state.address)
is ReferralInfoState.NonParticipantContent -> InfoForYou(award = state.award)
is ReferralInfoState.ParticipantContent -> InfoForYou(
award = state.award,
networkName = state.networkName,
address = state.address,
)
is ReferralInfoState.NonParticipantContent -> InfoForYou(
award = state.award,
networkName = state.networkName,
)
}
}
}
@Composable
private fun ConditionForYourFriend(state: ReferralInfoContentState) {
private fun ConditionForYourFriend(discount: String) {
Condition(iconResId = R.drawable.ic_discount) {
when (state) {
is ReferralInfoState.ParticipantContent -> InfoForYourFriend(discount = state.discount)
is ReferralInfoState.NonParticipantContent -> InfoForYourFriend(discount = state.discount)
}
InfoForYourFriend(discount = discount)
}
}
@ -239,7 +280,7 @@ private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () ->
}
@Composable
private fun InfoForYou(award: String, address: String? = null) {
private fun InfoForYou(award: String, networkName: String, address: String? = null) {
ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) {
Text(
text = buildAnnotatedString {
@ -250,7 +291,8 @@ private fun InfoForYou(award: String, address: String? = null) {
append(
String.format(
stringResource(id = R.string.referral_point_currencies_description_suffix),
if (!address.isNullOrBlank()) address else "",
networkName,
if (!address.isNullOrBlank()) " $address" else "",
),
)
},
@ -319,21 +361,105 @@ private fun ShimmerInfo() {
}
}
// TODO() Replace component with component from ds
@Composable
private fun ParticipantBottomBlock(state: ReferralInfoState.ParticipantContent, onAgreementClicked: () -> Unit) {
ParticipateBottomBlock(
purchasedWalletCount = state.purchasedWalletCount,
code = state.code,
onAgreementClicked = onAgreementClicked,
)
private fun ErrorSnackbarHost(errorSnackbar: ErrorSnackbar?) {
if (errorSnackbar != null) {
val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) }
val coroutineScope = rememberCoroutineScope()
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.padding(top = dimensionResource(id = R.dimen.spacing56)),
snackbar = {
Snackbar(
snackbarData = it,
modifier = Modifier.fillMaxWidth(),
actionOnNewLine = true,
shape = RoundedCornerShape(size = dimensionResource(id = R.dimen.radius8)),
backgroundColor = Black,
contentColor = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2),
actionColor = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2),
elevation = dimensionResource(id = R.dimen.elevation3),
)
},
)
val message = if (errorSnackbar.throwable.cause != null) {
String.format(
format = stringResource(id = R.string.referral_error_failed_to_load_info_with_reason),
errorSnackbar.throwable.cause,
)
} else {
stringResource(id = R.string.referral_error_failed_to_load_info)
}
val actionLabel = stringResource(id = R.string.warning_button_ok)
SideEffect {
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = message,
actionLabel = actionLabel,
duration = SnackbarDuration.Indefinite,
)
if (result == SnackbarResult.ActionPerformed) {
errorSnackbar.onOkClicked()
}
}
}
}
}
// TODO() Replace component with component from ds
@Composable
private fun NonParticipantBottomBlock(state: ReferralInfoState.NonParticipantContent, onAgreementClicked: () -> Unit) {
NonParticipateBottomBlock(
onAgreementClicked = onAgreementClicked,
onParticipateClicked = state.onParticipateClicked,
)
private fun CopySnackbarHost(isCopyButtonPressed: MutableState<Boolean>) {
if (isCopyButtonPressed.value) {
val snackbarHostState by remember { mutableStateOf(SnackbarHostState()) }
val coroutineScope = rememberCoroutineScope()
var snackbarSize by remember { mutableStateOf(0) }
val width = LocalConfiguration.current.screenWidthDp.dp
val snackbarWidth = with(LocalDensity.current) { snackbarSize.toDp() }
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier
.padding(top = dimensionResource(id = R.dimen.spacing56), start = (width - snackbarWidth).div(2))
.fillMaxWidth(),
snackbar = {
Box(
modifier = Modifier
.onSizeChanged { snackbarSize = it.width }
.background(Black, RoundedCornerShape(size = dimensionResource(id = R.dimen.radius8)))
.shadow(
dimensionResource(id = R.dimen.elevation3),
RoundedCornerShape(size = dimensionResource(id = R.dimen.radius8)),
)
.padding(
horizontal = dimensionResource(id = R.dimen.spacing16),
vertical = dimensionResource(id = R.dimen.spacing14),
),
) {
Text(
text = it.message,
color = MaterialTheme.colors.textColor(type = TextColorType.PRIMARY2),
style = MaterialTheme.typography.body2,
)
}
},
)
val message = stringResource(id = R.string.referral_promo_code_copied)
SideEffect {
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = message,
duration = SnackbarDuration.Short,
)
isCopyButtonPressed.value = false
}
}
}
}
@Preview(widthDp = 360, showBackground = true)
@ -345,13 +471,15 @@ fun Preview_ReferralScreen_Participant_InLightTheme() {
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.ParticipantContent(
award = "10 USDT",
networkName = "Tron",
address = "ma80...zk8q2",
discount = "10%",
purchasedWalletCount = 3,
code = "x4JdK",
shareLink = "",
url = "",
),
errorToast = ReferralStateHolder.ErrorToast(visibility = false, changeVisibility = {}),
errorSnackbar = null,
),
)
}
@ -366,13 +494,15 @@ fun Preview_ReferralScreen_Participant_InDarkTheme() {
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.ParticipantContent(
award = "10 USDT",
networkName = "Tron",
address = "ma80...zk8q2",
discount = "10%",
purchasedWalletCount = 3,
code = "x4JdK",
shareLink = "",
url = "",
),
errorToast = ReferralStateHolder.ErrorToast(visibility = false, changeVisibility = {}),
errorSnackbar = null,
),
)
}
@ -387,11 +517,12 @@ fun Preview_ReferralScreen_NonParticipant_InLightTheme() {
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.NonParticipantContent(
award = "10 USDT",
networkName = "Tron",
discount = "10%",
url = "",
onParticipateClicked = {},
),
errorToast = ReferralStateHolder.ErrorToast(visibility = false, changeVisibility = {}),
errorSnackbar = null,
),
)
}
@ -406,11 +537,12 @@ fun Preview_ReferralScreen_NonParticipant_InDarkTheme() {
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.NonParticipantContent(
award = "10 USDT",
networkName = "Tron",
discount = "10%",
url = "",
onParticipateClicked = {},
),
errorToast = ReferralStateHolder.ErrorToast(visibility = false, changeVisibility = {}),
errorSnackbar = null,
),
)
}
@ -424,7 +556,7 @@ fun Preview_ReferralScreen_Loading_InLightTheme() {
stateHolder = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.Loading,
errorToast = ReferralStateHolder.ErrorToast(visibility = false, changeVisibility = {}),
errorSnackbar = null,
),
)
}
@ -438,7 +570,7 @@ fun Preview_ReferralScreen_Loading_InDarkTheme() {
stateHolder = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.Loading,
errorToast = ReferralStateHolder.ErrorToast(visibility = false, changeVisibility = {}),
errorSnackbar = null,
),
)
}

View file

@ -8,16 +8,20 @@ import androidx.lifecycle.viewModelScope
import com.tangem.feature.referral.domain.ReferralInteractor
import com.tangem.feature.referral.domain.models.DiscountType
import com.tangem.feature.referral.domain.models.ReferralData
import com.tangem.feature.referral.domain.models.ReferralInfo
import com.tangem.feature.referral.models.ReferralStateHolder
import com.tangem.feature.referral.models.ReferralStateHolder.ErrorSnackbar
import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState
import com.tangem.feature.referral.router.ReferralRouter
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
@HiltViewModel
class ReferralViewModel @Inject constructor(
internal class ReferralViewModel @Inject constructor(
private val referralInteractor: ReferralInteractor,
private val dispatchers: CoroutineDispatcherProvider,
) : ViewModel() {
@ -25,57 +29,80 @@ class ReferralViewModel @Inject constructor(
var uiState: ReferralStateHolder by mutableStateOf(createInitiallyUiState())
private set
private var referralRouter: ReferralRouter by Delegates.notNull()
init {
loadReferralData()
}
fun setOnBackClicked(onBackClicked: () -> Unit) {
uiState = uiState.copy(headerState = ReferralStateHolder.HeaderState(onBackClicked = onBackClicked))
fun setRouter(router: ReferralRouter) {
referralRouter = router
uiState = uiState.copy(headerState = ReferralStateHolder.HeaderState(onBackClicked = router::back))
}
private fun createInitiallyUiState() = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = { }),
referralInfoState = ReferralInfoState.Loading,
errorToast = ReferralStateHolder.ErrorToast(
visibility = false,
changeVisibility = {
uiState = uiState.copy(
referralInfoState = uiState.referralInfoState,
errorToast = uiState.errorToast.copy(visibility = false),
)
},
),
errorSnackbar = null,
)
private fun loadReferralData() {
val lastUiState = uiState
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) { referralInteractor.getReferralStatus() }
.onSuccess { uiState = uiState.copy(referralInfoState = it.convertToReferralInfoState()) }
.onFailure { uiState = lastUiState.copy(errorToast = lastUiState.errorToast.copy(visibility = true)) }
.onSuccess(::showContent)
.onFailure(::showErrorSnackbar)
}
}
private fun participate() {
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) { referralInteractor.startReferral() }
.onSuccess(::showContent)
.onFailure(::showErrorSnackbar)
}
}
private fun showContent(referralData: ReferralData) {
uiState = uiState.copy(referralInfoState = referralData.convertToReferralInfoState())
}
private fun showErrorSnackbar(throwable: Throwable) {
uiState = uiState.copy(
errorSnackbar = ErrorSnackbar(throwable = throwable, onOkClicked = referralRouter::back),
)
}
private fun ReferralData.convertToReferralInfoState(): ReferralInfoState = when (this) {
is ReferralData.ParticipantData -> ReferralInfoState.ParticipantContent(
award = getAwardValue(),
address = referral.address,
networkName = getNetworkName(),
address = referral.getAddressValue(),
discount = getDiscountValue(),
purchasedWalletCount = referral.walletsPurchased,
code = referral.promocode,
shareLink = referral.shareLink,
url = tosLink,
)
is ReferralData.NonParticipantData -> ReferralInfoState.NonParticipantContent(
award = getAwardValue(),
networkName = getNetworkName(),
discount = getDiscountValue(),
url = tosLink,
onParticipateClicked = ::onParticipateClicked,
onParticipateClicked = ::participate,
)
}
private fun ReferralData.getAwardValue(): String =
"$award ${requireNotNull(tokens.firstOrNull()?.symbol) { "Token list is empty" }}"
private fun ReferralData.getAwardValue(): String = "$award ${getToken().symbol}"
private fun ReferralData.getNetworkName(): String = getToken().networkId.replaceFirstChar(Char::uppercase)
private fun ReferralInfo.getAddressValue(): String {
check(address.length > 5) { "Invalid address" }
return address.substring(startIndex = 0, endIndex = 4) + "..." +
address.substring(startIndex = address.length - 5, endIndex = address.length)
}
private fun ReferralData.getDiscountValue(): String {
val discountSymbol = when (discountType) {
@ -85,13 +112,5 @@ class ReferralViewModel @Inject constructor(
return "$discount $discountSymbol"
}
private fun onParticipateClicked() {
val lastUiState = uiState
uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading)
viewModelScope.launch(dispatchers.main) {
runCatching(dispatchers.io) { referralInteractor.startReferral() }
.onSuccess { uiState = uiState.copy(referralInfoState = it.convertToReferralInfoState()) }
.onFailure { uiState = lastUiState.copy(errorToast = lastUiState.errorToast.copy(visibility = true)) }
}
}
private fun ReferralData.getToken() = requireNotNull(tokens.firstOrNull()) { "Token list is empty" }
}

View file

@ -4,7 +4,7 @@
<string name="referral_title">Приведи друга в Tangem</string>
<string name="referral_point_currencies_title">Вы</string>
<string name="referral_point_currencies_description_prefix">Получите</string>
<string name="referral_point_currencies_description_suffix">за каждый кошелек, который купит ваш друг на ваш адрес в сети Tron %s</string>
<string name="referral_point_currencies_description_suffix">на ваш адрес в сети %s%s за каждый кошелек, который купит ваш друг</string>
<string name="referral_point_discount_title">Ваш друг</string>
<string name="referral_point_discount_description_prefix">Получит</string>
<string name="referral_point_discount_description_value">%s скидку</string>
@ -17,4 +17,8 @@
<string name="referral_promo_code_title">Ваш персональный код</string>
<string name="referral_wallets_purchased_count">%d кошельков</string>
<string name="referral_friends_bought_title">Ваши друзья купили</string>
<string name="referral_error_failed_to_load_info">Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже.</string>
<string name="referral_error_failed_to_load_info_with_reason">Не удалось загрузить информацию по реферальной программе. Причина: %s. Пожалуйста, попробуйте позже.</string>
<string name="referral_error_failed_to_participate">Не удалось обработать вашу заявку на участие. Причина: %s. Пожалуйста, попробуйте позже. Если проблема сохранится, вы можете обратиться в техподдержку.</string>
<string name="referral_promo_code_copied">Персональный код скопирован!</string>
</resources>

View file

@ -4,7 +4,7 @@
<string name="referral_title">Refer your friends to Tangem</string>
<string name="referral_point_currencies_title">You</string>
<string name="referral_point_currencies_description_prefix">Will get</string>
<string name="referral_point_currencies_description_suffix">for each wallet bought by your friend on your Tron network address %s</string>
<string name="referral_point_currencies_description_suffix">for each wallet bought by your friend on your %s network address%s</string>
<string name="referral_point_discount_title">Your friend</string>
<string name="referral_point_discount_description_prefix">Will get a</string>
<string name="referral_point_discount_description_value">%s discount</string>
@ -17,4 +17,8 @@
<string name="referral_promo_code_title">Your personal code</string>
<string name="referral_wallets_purchased_count">%d wallets</string>
<string name="referral_friends_bought_title">Your friends bought</string>
<string name="referral_error_failed_to_load_info">Failed to load the information about the referral program. Please try again later.</string>
<string name="referral_error_failed_to_load_info_with_reason">Failed to load the information about the referral program. Reason: %s. Please try again later.</string>
<string name="referral_error_failed_to_participate">Your participation request could not be processed. Reason: %s. Please try again later. If the problem persists — feel free to contact our support.</string>
<string name="referral_promo_code_copied">Personal code copied!</string>
</resources>