From 164b6e3c71c249520a47b0434c0f832f7ea5f89c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 Sep 2023 13:04:47 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../datasource/api/common/LocalDateAdapter.kt | 29 ++ .../api/tangemTech/models/ReferralResponse.kt | 18 +- .../com/tangem/datasource/di/MoshiModule.kt | 2 + .../com/tangem/datasource/di/NetworkModule.kt | 2 +- core/res/src/main/res/values-ru/strings.xml | 2 - .../src/main/res/values-zh-rTW/strings.xml | 2 - core/res/src/main/res/values/strings.xml | 2 - .../referral/converters/ReferralConverter.kt | 57 +++- .../referral/data/ReferralRepositoryImpl.kt | 2 +- .../referral/domain/ReferralInteractorImpl.kt | 12 +- .../referral/domain/ReferralRepository.kt | 2 +- .../referral/domain/models/ReferralData.kt | 11 + .../referral/models/ReferralStateHolder.kt | 3 + .../ui/AgreementBottomSheetContent.kt | 10 +- .../tangem/feature/referral/ui/AwardItems.kt | 71 +++++ .../feature/referral/ui/CornersToRound.kt | 27 ++ .../referral/ui/ParticipateBottomBlock.kt | 282 ++++++++++++++++-- .../feature/referral/ui/ReferralScreen.kt | 99 +++++- .../referral/viewmodels/ReferralViewModel.kt | 16 +- 19 files changed, 584 insertions(+), 65 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt create mode 100644 features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt new file mode 100644 index 0000000000..b11c76fccd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/LocalDateAdapter.kt @@ -0,0 +1,29 @@ +package com.tangem.datasource.api.common + +import com.squareup.moshi.FromJson +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.ToJson +import org.joda.time.LocalDate +import org.joda.time.format.DateTimeFormat + +class LocalDateAdapter : JsonAdapter() { + + private val formatter = DateTimeFormat.forPattern("yyyy-MM-dd") + + @FromJson + override fun fromJson(reader: JsonReader): LocalDate? { + val dateString = reader.nextString() + return LocalDate.parse(dateString, formatter) + } + + @ToJson + override fun toJson(writer: JsonWriter, value: LocalDate?) { + if (value != null) { + writer.value(formatter.print(value)) + } else { + writer.nullValue() + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt index 712c98c40a..2abb6e2a57 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/ReferralResponse.kt @@ -1,14 +1,12 @@ package com.tangem.datasource.api.tangemTech.models import com.squareup.moshi.Json +import org.joda.time.LocalDate -/** - * Main response class for referral API - * contains all necessary info about users program status - */ data class ReferralResponse( @Json(name = "conditions") val conditions: Conditions, @Json(name = "referral") val referral: Referral?, + @Json(name = "expectedAwards") val expectedAwards: ExpectedAwards?, ) { data class Conditions( @@ -45,4 +43,16 @@ data class ReferralResponse( @Json(name = "walletsPurchased") val walletsPurchased: Int, @Json(name = "termsAcceptedAt") val termsAcceptedAt: String?, ) + + data class ExpectedAwards( + @Json(name = "numberOfWallets") val numberOfWallets: Int, + @Json(name = "list") val list: List, + ) { + + data class AwardItem( + @Json(name = "currency") val currency: String, + @Json(name = "paymentDate") val paymentDate: LocalDate, + @Json(name = "amount") val amount: Int, + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 65668982d4..bf3d94095f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.BigDecimalAdapter +import com.tangem.datasource.api.common.LocalDateAdapter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -20,6 +21,7 @@ class MoshiModule { fun provideNetworkMoshi(): Moshi { return Moshi.Builder() .add(BigDecimalAdapter()) + .add(LocalDateAdapter()) .add(KotlinJsonAdapterFactory()) .build() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 9c11afc771..10abba599f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -28,7 +28,7 @@ class NetworkModule { fun provideTangemTechApi(@NetworkMoshi moshi: Moshi): TangemTechApi { return Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) - .baseUrl(PROD_TANGEM_TECH_BASE_URL) + .baseUrl(if (BuildConfig.DEBUG) DEV_TANGEM_TECH_BASE_URL else PROD_TANGEM_TECH_BASE_URL) .client( OkHttpClient.Builder() .addHeaders( diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index c7cf8aeddc..743f1f19ad 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -325,8 +325,6 @@ за %d кошельков Получите ^^%1$s^^ на ваш адрес в сети %2$s %3$s ^^спустя 30 дней^^ за каждый кошелек, который купит ваш друг - Получите - на ваш адрес в сети %1$s%2$s за каждый кошелек, который купит ваш друг Вы Получит при покупке кошелька на сайте tangem.com diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 294f3413b7..4af7ef5160 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -259,8 +259,6 @@ 無法加載有關推薦計劃的消息。請稍後再試 無法加載有關推薦計劃的消息。原因:%s。請稍後再試 您的朋友買 - 會得到 - 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包 得到 %s 折扣 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 02f330b04b..6e8c21c2a4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -320,8 +320,6 @@ for %d wallets Will get ^^%1$s^^ for each wallet bought by your friend on your %2$s network address %3$s ^^30 days after^^ that - Will get - for each wallet bought by your friend on your %1$s network address%2$s You Will get a when buying a wallet on tangem.com diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt index 11ee37cff4..921f57b107 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/converters/ReferralConverter.kt @@ -1,13 +1,17 @@ package com.tangem.feature.referral.converters +import android.text.format.DateUtils import com.tangem.datasource.api.tangemTech.models.ReferralResponse -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.domain.models.TokenData +import com.tangem.feature.referral.domain.models.* import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.isToday +import com.tangem.utils.extensions.isYesterday import com.tangem.utils.safeValueOf +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.format.DateTimeFormatterBuilder import org.joda.time.format.ISODateTimeFormat +import java.util.Locale import javax.inject.Inject class ReferralConverter @Inject constructor() : Converter { @@ -16,6 +20,7 @@ class ReferralConverter @Inject constructor() : Converter { + + /** Example, 2 Aug, 2023 */ + private val dateFormatter by lazy { + DateTimeFormatterBuilder() + .appendDayOfMonth(1) + .appendLiteral(' ') + .appendMonthOfYearShortText() + .appendLiteral(", ") + .appendYear(4, 4) + .toFormatter() + .withLocale(Locale.getDefault()) + } + + override fun convert(value: ReferralResponse.ExpectedAwards): ExpectedAwards { + return ExpectedAwards( + numberOfWallets = value.numberOfWallets, + expectedAwards = value.list.map { + ExpectedAward( + paymentDate = it.paymentDate.toDateTimeAtStartOfDay().millis.toDateFormat(), + amount = "${it.amount} ${it.currency}", + ) + }, + ) + } + + private fun Long.toDateFormat(): String { + val localDate = DateTime(this, DateTimeZone.getDefault()) + return if (localDate.isToday() || localDate.isYesterday()) { + DateUtils.getRelativeTimeSpanString( + this, + DateTime.now().millis, + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE, + ).toString() + } else { + dateFormatter.print(localDate) + } + } +} + private class TokenConverter : Converter { override fun convert(value: ReferralResponse.Conditions.Award.Token): TokenData { diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 7f8d7470e7..8f8d3a5c42 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -22,7 +22,7 @@ internal class ReferralRepositoryImpl @Inject constructor( override val isDemoMode: Boolean get() = demoModeDatasource.isDemoModeActive - override suspend fun getReferralStatus(walletId: String): ReferralData { + override suspend fun getReferralData(walletId: String): ReferralData { return withContext(coroutineDispatcher.io) { referralConverter.convert( referralApi.getReferralStatus( diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 6280321c45..a62fbcca35 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -20,9 +20,11 @@ internal class ReferralInteractorImpl( get() = repository.isDemoMode override suspend fun getReferralStatus(): ReferralData { - val refStatus = repository.getReferralStatus(userWalletManager.getWalletId()) - saveRefTokens(refStatus.tokens) - return refStatus + val referralData = repository.getReferralData(userWalletManager.getWalletId()) + + saveReferralTokens(referralData.tokens) + + return referralData } override suspend fun startReferral(): ReferralData { @@ -37,7 +39,7 @@ internal class ReferralInteractorImpl( address = publicAddress, ) } else { - error("tokens for ref is empty") + error("Tokens for ref is empty") } } @@ -53,7 +55,7 @@ internal class ReferralInteractorImpl( return derivationPath } - private fun saveRefTokens(tokens: List) { + private fun saveReferralTokens(tokens: List) { tokensForReferral.clear() tokensForReferral.addAll(tokens) } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt index ad770f8c2e..c3b5eaa9ca 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralRepository.kt @@ -7,7 +7,7 @@ interface ReferralRepository { val isDemoMode: Boolean /** Returns data object of [ReferralData] depends on user program status */ - suspend fun getReferralStatus(walletId: String): ReferralData + suspend fun getReferralData(walletId: String): ReferralData /** Starts user referral program */ suspend fun startReferral(walletId: String, networkId: String, tokenId: String, address: String): ReferralData diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt index f5cc061cb0..751af6a116 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/models/ReferralData.kt @@ -18,6 +18,7 @@ sealed interface ReferralData { override val tosLink: String, override val tokens: List, val referral: ReferralInfo, + val expectedAwards: ExpectedAwards?, ) : ReferralData /** Data class that used if user is not participant of program */ @@ -47,6 +48,16 @@ data class ReferralInfo( val termsAcceptedAt: DateTime?, ) +data class ExpectedAwards( + val numberOfWallets: Int, + val expectedAwards: List, +) + +data class ExpectedAward( + val paymentDate: String, + val amount: String, +) + enum class DiscountType { PERCENTAGE, VALUE } \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt index 90e8def3d6..dc7b06e5ef 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/models/ReferralStateHolder.kt @@ -1,5 +1,7 @@ package com.tangem.feature.referral.models +import com.tangem.feature.referral.domain.models.ExpectedAwards + internal data class ReferralStateHolder( val headerState: HeaderState, val referralInfoState: ReferralInfoState, @@ -26,6 +28,7 @@ internal data class ReferralStateHolder( val code: String, val shareLink: String, override val url: String, + val expectedAwards: ExpectedAwards?, ) : ReferralInfoState, ReferralInfoContentState data class NonParticipantContent( diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index 2a1c5b1e86..c803919829 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration +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 @@ -43,13 +44,16 @@ internal fun AgreementBottomSheetContent(url: String) { @Composable private fun AgreementHtmlView(url: String) { val state = rememberWebViewState(url) + val isInPreviewMode = LocalInspectionMode.current WebView( state = state, modifier = Modifier.background(TangemTheme.colors.background.secondary), onCreated = { - it.settings.apply { - javaScriptEnabled = false - allowFileAccess = false + if (!isInPreviewMode) { + it.settings.apply { + javaScriptEnabled = false + allowFileAccess = false + } } }, ) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt new file mode 100644 index 0000000000..7085ca0a34 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AwardItems.kt @@ -0,0 +1,71 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme + +@Suppress("LongParameterList") +@Composable +internal fun AwardText( + startText: String, + startTextColor: Color, + startTextStyle: TextStyle, + endText: String, + endTextColor: Color, + endTextStyle: TextStyle, + cornersToRound: CornersToRound, +) { + Surface( + shape = cornersToRound.getShape(), + color = TangemTheme.colors.background.primary, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = startText, + color = startTextColor, + maxLines = 1, + style = startTextStyle, + ) + + Text( + text = endText, + color = endTextColor, + maxLines = 1, + style = endTextStyle, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_AwardItem_Light() { + TangemTheme { + AwardText( + startText = "startText", + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = "endText", + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = CornersToRound.TOP_2, + ) + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt new file mode 100644 index 0000000000..b5070cdb28 --- /dev/null +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/CornersToRound.kt @@ -0,0 +1,27 @@ +package com.tangem.feature.referral.ui + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme + +internal enum class CornersToRound { + + ALL_4, + TOP_2, + BOTTOM_2, + ZERO, + ; + + @Suppress("TopLevelComposableFunctions") + @Composable + fun getShape(): RoundedCornerShape { + val radius = TangemTheme.dimens.radius12 + return when (this) { + ALL_4 -> RoundedCornerShape(radius) + TOP_2 -> RoundedCornerShape(topStart = radius, topEnd = radius) + BOTTOM_2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + ZERO -> RoundedCornerShape(0.dp) + } + } +} \ No newline at end of file diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt index 169fb81271..911aa890dd 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ParticipateBottomBlock.kt @@ -2,15 +2,19 @@ package com.tangem.feature.referral.ui import android.content.Context import android.content.Intent +import androidx.compose.animation.* import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -19,14 +23,16 @@ 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.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.core.content.ContextCompat.startActivity -import com.tangem.core.ui.components.PrimaryStartIconButton -import com.tangem.core.ui.components.SmallInfoCard +import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.domain.models.ExpectedAward +import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.presentation.R @Suppress("LongParameterList") @@ -36,6 +42,7 @@ internal fun ParticipateBottomBlock( purchasedWalletCount: Int, code: String, shareLink: String, + expectedAwards: ExpectedAwards?, onAgreementClick: () -> Unit, onShowCopySnackbar: () -> Unit, onCopyClick: () -> Unit, @@ -50,14 +57,6 @@ internal fun ParticipateBottomBlock( .padding(horizontal = TangemTheme.dimens.spacing16), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), ) { - SmallInfoCard( - startText = stringResource(id = R.string.referral_friends_bought_title), - endText = pluralStringResource( - id = R.plurals.referral_wallets_purchased_count, - count = purchasedWalletCount, - purchasedWalletCount, - ), - ) PersonalCodeCard(code = code) AdditionalButtons( code = code, @@ -66,10 +65,187 @@ internal fun ParticipateBottomBlock( onCopyClick = onCopyClick, onShareClick = onShareClick, ) + CounterAndAwards(purchasedWalletCount = purchasedWalletCount, expectedAwards = expectedAwards) AgreementText(firstPartResId = R.string.referral_tos_enroled_prefix, onClick = onAgreementClick) } } +@Composable +private fun CounterAndAwards(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { + Column { + Counter(purchasedWalletCount, expectedAwards) + + if (expectedAwards != null) { + Awards(expectedAwards) + } else if (purchasedWalletCount != 0) { + EmptyUpcomingPayments() + } + } +} + +@Composable +private fun Counter(purchasedWalletCount: Int, expectedAwards: ExpectedAwards?) { + val isExpectedAwardsPresent = expectedAwards != null + + AwardText( + startText = stringResource(id = R.string.referral_friends_bought_title), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = pluralStringResource( + id = R.plurals.referral_wallets_purchased_count, + count = purchasedWalletCount, + purchasedWalletCount, + ), + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = if (isExpectedAwardsPresent || purchasedWalletCount != 0) { + CornersToRound.TOP_2 + } else { + CornersToRound.ALL_4 + }, + ) +} + +@Suppress("MagicNumber") +@Composable +private fun Awards(expectedAwards: ExpectedAwards) { + val elementsCountToShowInLessMode = 3 + val isExpanded = remember { mutableStateOf(false) } + + Divider( + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + ) + AwardText( + startText = stringResource(id = R.string.referral_expected_awards), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = pluralStringResource( + id = R.plurals.referral_number_of_wallets, + count = expectedAwards.numberOfWallets, + expectedAwards.numberOfWallets, + ), + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = CornersToRound.ZERO, + ) + + val initialItems = expectedAwards.expectedAwards.take(elementsCountToShowInLessMode) + val extraItems = expectedAwards.expectedAwards.drop(elementsCountToShowInLessMode) + + initialItems.forEachIndexed { index, expectedAward -> + AwardText( + startText = expectedAward.paymentDate, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.subtitle2, + endText = expectedAward.amount, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = if (index == initialItems.size - 1 && extraItems.isEmpty()) { + CornersToRound.BOTTOM_2 + } else { + CornersToRound.ZERO + }, + ) + } + + AnimatedVisibility( + visible = isExpanded.value, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut(), + ) { + ExtraItems(extraItems = extraItems) + } + + if (expectedAwards.expectedAwards.size > elementsCountToShowInLessMode) { + LessMoreButton(isExpanded = isExpanded) + } +} + +@Composable +private fun EmptyUpcomingPayments() { + Divider( + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + ) + AwardText( + startText = stringResource(id = R.string.referral_expected_awards), + startTextColor = TangemTheme.colors.text.tertiary, + startTextStyle = TangemTheme.typography.subtitle2, + endText = "", + endTextColor = TangemTheme.colors.text.tertiary, + endTextStyle = TangemTheme.typography.body2, + cornersToRound = CornersToRound.BOTTOM_2, + ) +} + +@Composable +private fun LessMoreButton(isExpanded: MutableState) { + Surface( + shape = RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius12, + bottomEnd = TangemTheme.dimens.radius12, + ), + ) { + Column( + modifier = Modifier.background(TangemTheme.colors.background.primary), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(TangemTheme.dimens.size48) + .clickable { isExpanded.value = !isExpanded.value } + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = if (isExpanded.value) { + stringResource(id = R.string.referral_less) + } else { + stringResource(id = R.string.referral_more) + }, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + + val chevronIcon = if (isExpanded.value) { + painterResource(id = com.tangem.core.ui.R.drawable.ic_chevron_up_24) + } else { + painterResource(id = com.tangem.core.ui.R.drawable.ic_chevron_24) + } + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + painter = chevronIcon, + tint = TangemTheme.colors.text.tertiary, + contentDescription = null, + ) + } + } + } +} + +@Composable +private fun ExtraItems(extraItems: List) { + Column { + extraItems.forEach { expectedAward -> + AwardText( + startText = expectedAward.paymentDate, + startTextColor = TangemTheme.colors.text.primary1, + startTextStyle = TangemTheme.typography.subtitle2, + endText = expectedAward.amount, + endTextColor = TangemTheme.colors.text.primary1, + endTextStyle = TangemTheme.typography.subtitle2, + cornersToRound = CornersToRound.ZERO, + + ) + } + } +} + @Composable private fun PersonalCodeCard(code: String) { Column( @@ -157,11 +333,28 @@ private fun Context.shareText(text: String) { @Composable private fun Preview_ParticipateBottomBlock_InLightTheme() { TangemTheme(isDark = false) { - Column(Modifier.background(TangemTheme.colors.background.primary)) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( purchasedWalletCount = 3, code = "x4JdK", shareLink = "", + expectedAwards = ExpectedAwards( + numberOfWallets = 3, + expectedAwards = listOf( + ExpectedAward( + amount = "10 USDT", + paymentDate = "Today", + ), + ExpectedAward( + amount = "20 USDT", + paymentDate = "6 Aug 2023", + ), + ExpectedAward( + amount = "30 USDT", + paymentDate = "10 Aug 2023", + ), + ), + ), onAgreementClick = {}, onShowCopySnackbar = {}, onCopyClick = {}, @@ -173,13 +366,64 @@ private fun Preview_ParticipateBottomBlock_InLightTheme() { @Preview(widthDp = 360, showBackground = true) @Composable -private fun Preview_ParticipateBottomBlock_InDarkTheme() { - TangemTheme(isDark = true) { - Column(Modifier.background(TangemTheme.colors.background.primary)) { +private fun Preview_ParticipateBottomBlock_Without_Awards_InLightTheme() { + TangemTheme(isDark = false) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { ParticipateBottomBlock( purchasedWalletCount = 3, code = "x4JdK", shareLink = "", + expectedAwards = null, + onAgreementClick = {}, + onShowCopySnackbar = {}, + onCopyClick = {}, + onShareClick = {}, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ParticipateBottomBlock_Without_Awards_And_Purchased_Wallets_InLightTheme() { + TangemTheme(isDark = false) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { + ParticipateBottomBlock( + purchasedWalletCount = 0, + code = "x4JdK", + shareLink = "", + expectedAwards = null, + onAgreementClick = {}, + onShowCopySnackbar = {}, + onCopyClick = {}, + onShareClick = {}, + ) + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun LessMoreButton_White() { + TangemTheme(isDark = false) { + LessMoreButton( + isExpanded = remember { + mutableStateOf(false) + }, + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ParticipateBottomBlock_Without_Awards_InDarkTheme() { + TangemTheme(isDark = true) { + Column(Modifier.background(TangemTheme.colors.background.secondary)) { + ParticipateBottomBlock( + purchasedWalletCount = 3, + code = "x4JdK", + shareLink = "", + expectedAwards = null, onAgreementClick = {}, onShowCopySnackbar = {}, onCopyClick = {}, diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 7c3e8b2ddf..6dcec255cf 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity 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 @@ -31,6 +32,8 @@ import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.referral.domain.models.ExpectedAward +import com.tangem.feature.referral.domain.models.ExpectedAwards import com.tangem.feature.referral.models.DemoModeException import com.tangem.feature.referral.models.ReferralStateHolder import com.tangem.feature.referral.models.ReferralStateHolder.* @@ -159,6 +162,7 @@ private fun ReferralInfo( purchasedWalletCount = state.purchasedWalletCount, code = state.code, shareLink = state.shareLink, + expectedAwards = state.expectedAwards, onAgreementClick = onAgreementClick, onShowCopySnackbar = onShowCopySnackbar, onCopyClick = stateHolder.analytics.onCopyClicked, @@ -253,25 +257,47 @@ private fun Condition(@DrawableRes iconResId: Int, infoBlock: @Composable () -> private fun InfoForYou(award: String, networkName: String, address: String? = null) { ConditionInfo(title = stringResource(id = R.string.referral_point_currencies_title)) { Text( - text = buildAnnotatedString { - append(stringResource(id = R.string.referral_point_currencies_description_prefix)) - withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) { - append(" $award ") - } - append( - String.format( - stringResource(id = R.string.referral_point_currencies_description_suffix), - networkName, - if (!address.isNullOrBlank()) " $address" else "", - ), - ) - }, + formatAwardConditionsString( + quantity = award, + network = networkName, + address = if (!address.isNullOrBlank()) " $address" else "", + ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, ) } } +@Composable +private fun formatAwardConditionsString(quantity: String, network: String, address: String): AnnotatedString { + val rawString = stringResource(R.string.referral_point_currencies_description, quantity, network, address) + + val pattern = Regex("\\^\\^(.*?)\\^\\^") + var startIndex = 0 + val annotatedString = buildAnnotatedString { + pattern.findAll(rawString).forEach { matchResult -> + val index = matchResult.range.first + val matchedValue = matchResult.groups[1]?.value ?: "" + + // appends unformatted part + append(rawString.substring(startIndex, index)) + + // applies style on ^^-wrapped parts + withStyle(SpanStyle(color = TangemTheme.colors.text.primary1)) { + append(matchedValue) + } + + // goes to next part + startIndex = matchResult.range.last + 1 + } + + // appends remaining ending if exists + append(rawString.substring(startIndex)) + } + + return annotatedString +} + @Composable private fun InfoForYourFriend(discount: String) { ConditionInfo(title = stringResource(id = R.string.referral_point_discount_title)) { @@ -464,6 +490,7 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() { code = "x4JdK", shareLink = "", url = "", + expectedAwards = null, ), errorSnackbar = null, analytics = Analytics( @@ -492,6 +519,52 @@ private fun Preview_ReferralScreen_Participant_InDarkTheme() { code = "x4JdK", shareLink = "", url = "", + expectedAwards = null, + ), + errorSnackbar = null, + analytics = Analytics( + onAgreementClicked = {}, + onCopyClicked = {}, + onShareClicked = {}, + ), + ), + ) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Composable +private fun Preview_ReferralScreen_Participant_With_Referrals_InLightTheme() { + TangemTheme(isDark = false) { + ReferralScreen( + stateHolder = ReferralStateHolder( + headerState = HeaderState(onBackClicked = {}), + referralInfoState = ReferralInfoState.ParticipantContent( + award = "10 USDT", + networkName = "Tron", + address = "ma80...zk8q2", + discount = "10%", + purchasedWalletCount = 3, + code = "x4JdK", + shareLink = "", + url = "", + expectedAwards = ExpectedAwards( + numberOfWallets = 5, + expectedAwards = listOf( + ExpectedAward( + amount = "10 USDT", + paymentDate = "Today", + ), + ExpectedAward( + amount = "20 USDT", + paymentDate = "6 Aug 2023", + ), + ExpectedAward( + amount = "30 USDT", + paymentDate = "10 Aug 2023", + ), + ), + ), ), errorSnackbar = null, analytics = Analytics( diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt index b2f802e181..82c0d08bb8 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/viewmodels/ReferralViewModel.kt @@ -36,7 +36,7 @@ internal class ReferralViewModel @Inject constructor( private var referralRouter: ReferralRouter by Delegates.notNull() - private val lastReferralData = mutableStateOf(null) + private var lastReferralData: ReferralData? = null init { loadReferralData() @@ -67,7 +67,7 @@ internal class ReferralViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { referralInteractor.getReferralStatus().apply { - lastReferralData.value = this + lastReferralData = this } } .onSuccess(::showContent) @@ -84,14 +84,13 @@ internal class ReferralViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { referralInteractor.startReferral() } .onSuccess(::showContent) - .onFailure { - if (it is UserCancelledException) { - val lastRefData = lastReferralData.value - if (lastRefData != null) { - showContent(lastRefData) + .onFailure { throwable -> + if (throwable is UserCancelledException) { + lastReferralData?.let { referralData -> + showContent(referralData) } } else { - showErrorSnackbar(it) + showErrorSnackbar(throwable) } } } @@ -130,6 +129,7 @@ internal class ReferralViewModel @Inject constructor( code = referral.promocode, shareLink = referral.shareLink, url = tosLink, + expectedAwards = expectedAwards, ) is ReferralData.NonParticipantData -> ReferralInfoState.NonParticipantContent( award = getAwardValue(),