Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-24 17:36:59 +03:00
commit fbb99bcda0
317 changed files with 3800 additions and 2894 deletions

View file

@ -14,6 +14,7 @@ dependencies {
/** Core modules */
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.res)
implementation(projects.core.utils)
implementation(projects.core.ui)
@ -37,7 +38,6 @@ dependencies {
/** Other libraries */
implementation(deps.compose.shimmer)
implementation(deps.compose.accompanist.webView)
implementation(deps.compose.accompanist.systemUiController)
/** DI */

View file

@ -50,6 +50,6 @@ internal data class ReferralStateHolder(
data class Analytics(
val onAgreementClicked: () -> Unit,
val onCopyClicked: () -> Unit,
val onShareClicked: () -> Unit,
val onShareClicked: (String) -> Unit,
)
}

View file

@ -1,66 +0,0 @@
package com.tangem.feature.referral.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.webview.applySafeSettings
import com.tangem.feature.referral.presentation.R
/**
* Bottom sheet content with referral program terms of use
*
* @param url link to the html page
* @param bottomBarHeight bottom insets
*/
@Composable
internal fun AgreementBottomSheetContent(url: String, bottomBarHeight: Dp) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
TangemTopAppBar(title = stringResource(id = R.string.details_referral_title))
AgreementHtmlView(url = url)
Spacer(modifier = Modifier.height(bottomBarHeight))
}
}
@Composable
private fun AgreementHtmlView(url: String) {
val state = rememberWebViewState(url)
val isInPreviewMode = LocalInspectionMode.current
WebView(
state = state,
modifier = Modifier.background(TangemTheme.colors.background.primary),
captureBackPresses = false,
onCreated = {
if (!isInPreviewMode) it.applySafeSettings()
},
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AgreementBottomSheet() {
TangemThemePreview {
AgreementBottomSheetContent(url = "https://tangem.com/en/", 50.dp)
}
}

View file

@ -9,20 +9,20 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.ClickableText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
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 com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.presentation.R
@Composable
internal fun AgreementText(@StringRes firstPartResId: Int, onClick: () -> Unit) {
val agreementText = annotatedAgreementString(firstPart = stringResource(firstPartResId))
val agreementText = annotatedAgreementString(firstPart = stringResourceSafe(firstPartResId))
ClickableText(
text = agreementText,
modifier = Modifier
@ -46,10 +46,10 @@ private fun annotatedAgreementString(firstPart: String): AnnotatedString {
append("$firstPart ")
}
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
append(stringResource(id = R.string.common_terms_and_conditions))
append(stringResourceSafe(id = R.string.common_terms_and_conditions))
}
withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) {
append(" ${stringResource(id = R.string.referral_tos_suffix)}")
append(" ${stringResourceSafe(id = R.string.referral_tos_suffix)}")
}
}
}

View file

@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.presentation.R
@ -23,7 +23,7 @@ internal fun NonParticipateBottomBlock(onAgreementClick: () -> Unit, onParticipa
)
PrimaryButtonIconEnd(
text = stringResource(id = R.string.referral_button_participate),
text = stringResourceSafe(id = R.string.referral_button_participate),
iconResId = R.drawable.ic_tangem_24,
onClick = onParticipateClick,
modifier = Modifier

View file

@ -1,7 +1,5 @@
package com.tangem.feature.referral.ui
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.background
@ -18,15 +16,15 @@ import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.core.content.ContextCompat.startActivity
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.components.PrimaryButtonIconStart
import com.tangem.core.ui.components.rows.RoundableCornersRow
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
@ -44,7 +42,7 @@ internal fun ParticipateBottomBlock(
snackbarHostState: SnackbarHostState,
onAgreementClick: () -> Unit,
onCopyClick: () -> Unit,
onShareClick: () -> Unit,
onShareClick: (String) -> Unit,
) {
Column(
modifier = Modifier
@ -98,10 +96,10 @@ private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: Expected
@Composable
private fun Counter(purchasedWalletCount: Int, overallItemsLastIndex: Int) {
RoundableCornersRow(
startText = stringResource(id = R.string.referral_friends_bought_title),
startText = stringResourceSafe(id = R.string.referral_friends_bought_title),
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
endText = pluralStringResource(
endText = pluralStringResourceSafe(
id = R.plurals.referral_wallets_purchased_count,
count = purchasedWalletCount,
purchasedWalletCount,
@ -122,14 +120,14 @@ private fun Awards(expectedAwards: ExpectedAwards, overallItemsLastIndex: Int, i
)
RoundableCornersRow(
startText = if (expectedAwards.expectedAwards.isNotEmpty()) {
stringResource(id = R.string.referral_expected_awards)
stringResourceSafe(id = R.string.referral_expected_awards)
} else {
stringResource(id = R.string.referral_no_expected_awards)
stringResourceSafe(id = R.string.referral_no_expected_awards)
},
startTextColor = TangemTheme.colors.text.tertiary,
startTextStyle = TangemTheme.typography.subtitle2,
endText = if (expectedAwards.expectedAwards.isNotEmpty()) {
pluralStringResource(
pluralStringResourceSafe(
id = R.plurals.referral_number_of_wallets,
count = expectedAwards.numberOfWallets,
expectedAwards.numberOfWallets,
@ -201,9 +199,9 @@ private fun LessMoreButton(isExpanded: MutableState<Boolean>) {
) {
Text(
text = if (isExpanded.value) {
stringResource(id = R.string.referral_less)
stringResourceSafe(id = R.string.referral_less)
} else {
stringResource(id = R.string.referral_more)
stringResourceSafe(id = R.string.referral_more)
},
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
@ -261,7 +259,7 @@ private fun PersonalCodeCard(code: String) {
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Text(
text = stringResource(id = R.string.referral_promo_code_title),
text = stringResourceSafe(id = R.string.referral_promo_code_title),
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
style = TangemTheme.typography.subtitle2,
@ -281,7 +279,7 @@ private fun AdditionalButtons(
shareLink: String,
snackbarHostState: SnackbarHostState,
onCopyClick: () -> Unit,
onShareClick: () -> Unit,
onShareClick: (String) -> Unit,
) {
val clipboardManager = LocalClipboardManager.current
val hapticFeedback = LocalHapticFeedback.current
@ -294,7 +292,7 @@ private fun AdditionalButtons(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
PrimaryButtonIconStart(
text = stringResource(id = R.string.common_copy),
text = stringResourceSafe(id = R.string.common_copy),
iconResId = R.drawable.ic_copy_24,
onClick = {
onCopyClick.invoke()
@ -303,7 +301,7 @@ private fun AdditionalButtons(
coroutineScope.launch {
snackbarHostState.showSnackbar(
message = resources.getString(R.string.referral_promo_code_copied),
message = resources.getStringSafe(R.string.referral_promo_code_copied),
duration = SnackbarDuration.Short,
)
}
@ -313,12 +311,11 @@ private fun AdditionalButtons(
val context = LocalContext.current
PrimaryButtonIconStart(
text = stringResource(id = R.string.common_share),
text = stringResourceSafe(id = R.string.common_share),
iconResId = R.drawable.ic_share_24,
onClick = {
onShareClick.invoke()
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
context.shareText(context.getString(R.string.referral_share_link, shareLink))
onShareClick(context.getString(R.string.referral_share_link, shareLink))
},
modifier = Modifier.weight(1f),
)
@ -341,16 +338,6 @@ private fun calculateOverallItemsCount(expectedAwards: ExpectedAwards?, isExpand
return counterItems + awardItems
}
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)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
@ -434,5 +421,5 @@ private data class ParticipateBottomBlockData(
val onAgreementClick: () -> Unit = {},
val onShowCopySnackbar: () -> Unit = {},
val onCopyClick: () -> Unit = {},
val onShareClick: () -> Unit = {},
val onShareClick: (String) -> Unit = {},
)

View file

@ -1,47 +0,0 @@
package com.tangem.feature.referral.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.feature.referral.models.ReferralStateHolder
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ReferralBottomSheet(
sheetState: SheetState,
isVisible: Boolean,
onDismissRequest: () -> Unit,
config: ReferralStateHolder.ReferralInfoState,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
if (isVisible) {
ModalBottomSheet(
modifier = Modifier.statusBarsPadding(),
onDismissRequest = onDismissRequest,
sheetState = sheetState,
shape = RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
contentWindowInsets = { WindowInsetsZero },
containerColor = TangemTheme.colors.background.primary,
) {
AgreementBottomSheetContent(
url = when (config) {
is ReferralStateHolder.ReferralInfoState.NonParticipantContent -> config.url
is ReferralStateHolder.ReferralInfoState.ParticipantContent -> config.url
is ReferralStateHolder.ReferralInfoState.Loading -> ""
},
bottomBarHeight = bottomBarHeight,
)
}
}
}

View file

@ -10,18 +10,21 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
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 com.tangem.core.res.getStringSafe
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
@ -29,6 +32,7 @@ import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar
import com.tangem.core.ui.components.snackbar.TangemSnackbar
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.referral.domain.models.ExpectedAward
@ -43,14 +47,9 @@ import kotlinx.coroutines.launch
* Referral program screen for participant and non-participant
*
* @param stateHolder state holder
* @param modifier modifier
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
var isBottomSheetVisible by remember { mutableStateOf(value = false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val snackbarHostState = remember(::SnackbarHostState)
BackHandler(onBack = stateHolder.headerState.onBackClicked)
@ -59,7 +58,7 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
topBar = {
AppBarWithBackButton(
modifier = Modifier.statusBarsPadding(),
text = stringResource(R.string.details_referral_title),
text = stringResourceSafe(R.string.details_referral_title),
onBackClick = stateHolder.headerState.onBackClicked,
)
},
@ -83,10 +82,7 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
ReferralContent(
stateHolder = stateHolder,
snackbarHostState = snackbarHostState,
onAgreementClick = {
stateHolder.analytics.onAgreementClicked.invoke()
isBottomSheetVisible = true
},
onAgreementClick = stateHolder.analytics.onAgreementClicked,
modifier = Modifier.padding(it),
)
}
@ -100,7 +96,7 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
coroutineScope.launch {
val result = snackbarHostState.showSnackbar(
message = resources.getMessageForErrorSnackbar(errorSnackbar.throwable),
actionLabel = resources.getString(R.string.warning_button_ok),
actionLabel = resources.getStringSafe(R.string.warning_button_ok),
duration = SnackbarDuration.Indefinite,
)
@ -110,13 +106,6 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
}
}
}
ReferralBottomSheet(
sheetState = sheetState,
isVisible = isBottomSheetVisible,
onDismissRequest = { isBottomSheetVisible = false },
config = stateHolder.referralInfoState,
)
}
@Composable
@ -156,7 +145,7 @@ private fun Header() {
)
SpacerH24()
Text(
text = stringResource(id = R.string.referral_title),
text = stringResourceSafe(id = R.string.referral_title),
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing50),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
@ -273,7 +262,7 @@ private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () ->
@Composable
private fun InfoForYou(award: String, networkName: String, address: String? = null) {
ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) {
ConditionInfo(title = stringResourceSafe(id = R.string.referral_point_currencies_title)) {
Text(
formatAwardConditionsString(
quantity = award,
@ -288,7 +277,7 @@ private fun InfoForYou(award: String, networkName: String, address: String? = nu
@Composable
private fun formatAwardConditionsString(quantity: String, network: String, address: String): AnnotatedString {
val rawString = stringResource(R.string.referral_point_currencies_description, quantity, network, address)
val rawString = stringResourceSafe(R.string.referral_point_currencies_description, quantity, network, address)
val pattern = Regex("\\^\\^(.*?)\\^\\^")
var startIndex = 0
@ -318,20 +307,20 @@ private fun formatAwardConditionsString(quantity: String, network: String, addre
@Composable
private fun InfoForYourFriend(discount: String) {
ConditionInfo(title = stringResource(id = R.string.referral_point_discount_title)) {
ConditionInfo(title = stringResourceSafe(id = R.string.referral_point_discount_title)) {
Text(
text = buildAnnotatedString {
append(stringResource(id = R.string.referral_point_discount_description_prefix))
append(stringResourceSafe(id = R.string.referral_point_discount_description_prefix))
withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) {
append(
String.format(
stringResource(id = R.string.referral_point_discount_description_value),
stringResourceSafe(id = R.string.referral_point_discount_description_value),
" $discount",
),
)
}
append(" ")
append(stringResource(id = R.string.referral_point_discount_description_suffix))
append(stringResourceSafe(id = R.string.referral_point_discount_description_suffix))
},
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
@ -374,9 +363,11 @@ private fun ShimmerInfo() {
private fun Resources.getMessageForErrorSnackbar(throwable: Throwable): String {
return when {
throwable is DemoModeException -> getString(R.string.alert_demo_feature_disabled)
throwable.cause != null -> getString(R.string.referral_error_failed_to_load_info_with_reason, throwable.cause)
else -> getString(R.string.referral_error_failed_to_load_info)
throwable is DemoModeException -> getStringSafe(R.string.alert_demo_feature_disabled)
throwable.cause != null -> {
getStringSafe(R.string.referral_error_failed_to_load_info_with_reason, requireNotNull(throwable.cause))
}
else -> getStringSafe(R.string.referral_error_failed_to_load_info)
}
}

View file

@ -10,6 +10,8 @@ import androidx.lifecycle.viewModelScope
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.referral.analytics.ReferralEvents
import com.tangem.feature.referral.domain.ReferralInteractor
@ -34,6 +36,8 @@ internal class ReferralViewModel @Inject constructor(
private val referralInteractor: ReferralInteractor,
private val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
private val shareManager: ShareManager,
private val urlOpener: UrlOpener,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
@ -119,14 +123,18 @@ internal class ReferralViewModel @Inject constructor(
private fun onAgreementClicked() {
analyticsEventHandler.send(ReferralEvents.ClickTaC)
lastReferralData?.tosLink?.let(urlOpener::openUrl)
}
private fun onCopyClicked() {
analyticsEventHandler.send(ReferralEvents.ClickCopy)
}
private fun onShareClicked() {
private fun onShareClicked(text: String) {
analyticsEventHandler.send(ReferralEvents.ClickShare)
shareManager.shareText(text = text)
}
private fun ReferralData.convertToReferralInfoState(): ReferralInfoState = when (this) {