Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-25 17:34:25 +03:00
commit 949658d756
32 changed files with 258 additions and 72 deletions

View file

@ -142,6 +142,17 @@
android:scheme="tangem" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="*"
android:scheme="tangem" />
</intent-filter>
</activity>
<!-- Disable android.startup completely. Used for Worker according doc -->

View file

@ -11,6 +11,7 @@ import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.net.toUri
import coil.executeBlocking
import coil.request.ImageRequest
import com.google.firebase.messaging.FirebaseMessagingService
@ -36,10 +37,11 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
// TODO refactoring: [REDACTED_JIRA]
val intent = Intent(applicationContext, MainActivity::class.java)
intent.putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val intent = Intent(applicationContext, MainActivity::class.java).apply {
data = message.data[DEEPLINK_KEY]?.toUri()
putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
/* context = */ this,
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
@ -101,5 +103,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
private companion object {
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
const val DEEPLINK_KEY = "deeplink"
}
}

View file

@ -15,14 +15,14 @@ internal object PromoDomainModule {
@Singleton
fun provideShouldShowSwapPromoWalletUseCase(
promoSettingsRepository: PromoRepository,
): ShouldShowSwapPromoWalletUseCase {
return ShouldShowSwapPromoWalletUseCase(promoSettingsRepository)
): ShouldShowPromoWalletUseCase {
return ShouldShowPromoWalletUseCase(promoSettingsRepository)
}
@Provides
@Singleton
fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowSwapPromoTokenUseCase {
return ShouldShowSwapPromoTokenUseCase(promoRepository)
fun provideShouldShowSwapPromoTokenUseCase(promoRepository: PromoRepository): ShouldShowPromoTokenUseCase {
return ShouldShowPromoTokenUseCase(promoRepository)
}
@Provides

View file

@ -123,8 +123,12 @@ object PreferencesKeys {
val WALLETS_NFT_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "walletsNftEnabledStates") }
// region Promo
fun getShouldShowStoriesKey(storyId: String) = booleanPreferencesKey("shouldShowStories_$storyId")
fun getShouldShowPromoKey(promoId: String) = booleanPreferencesKey("shouldShowPromo_$promoId")
// endregion
// region Permission
fun getShouldShowPermission(permission: String) = booleanPreferencesKey("shouldShowPushPermission_$permission")

View file

@ -0,0 +1,13 @@
package com.tangem.core.deeplink.global
import com.tangem.core.deeplink.DeepLink
class ReferralDeepLink(
val onReceive: () -> Unit,
) : DeepLink(shouldHandleDelayed = true) {
override val uri: String = "tangem://referral"
override fun onReceive(params: Map<String, String>) {
onReceive()
}
}

View file

@ -520,6 +520,9 @@
<string name="nft_wallet_title">NFT collections</string>
<string name="nft_wallet_unable_to_load">Unable to load the data</string>
<string name="nft_receive_choose_network">Choose network</string>
<string name="notification_referral_promo_button">Join Now</string>
<string name="notification_referral_promo_text">Share your code - earn 5 USDT per sale. Your friend gets 10% OFF.</string>
<string name="notification_referral_promo_title">Get REWARDS for every friend!</string>
<string name="nft_details_last_sale_price">Last sale price</string>
<string name="nft_details_rarity_label">Rarity label</string>
<string name="nft_details_rarity_rank">Rarity rank</string>

View file

@ -25,6 +25,8 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
@ -55,6 +57,7 @@ fun Notification(
subtitleColor: Color = TangemTheme.colors.text.tertiary,
containerColor: Color? = null,
iconTint: Color? = null,
iconSize: Dp = 20.dp,
isEnabled: Boolean = true,
) {
NotificationBaseContainer(
@ -68,6 +71,7 @@ fun Notification(
MainContent(
iconResId = config.iconResId,
iconTint = iconTint,
iconSize = iconSize,
title = config.title,
subtitle = config.subtitle,
subtitleColor = subtitleColor,
@ -127,6 +131,7 @@ internal fun NotificationBaseContainer(
private fun MainContent(
iconResId: Int,
iconTint: Color?,
iconSize: Dp,
title: TextReference?,
subtitle: TextReference,
subtitleColor: Color,
@ -137,7 +142,7 @@ private fun MainContent(
iconResId = iconResId,
tint = iconTint,
modifier = Modifier
.size(size = TangemTheme.dimens.size20)
.size(size = iconSize)
.align(alignment = Alignment.CenterVertically),
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

@ -22,6 +22,7 @@ dependencies {
implementation(projects.domain.promo)
implementation(projects.domain.promo.models)
implementation(projects.domain.wallets.models)
implementation(projects.features.referral.domain)
implementation(projects.core.datasource)
implementation(projects.core.utils)

View file

@ -4,16 +4,21 @@ import com.tangem.data.promo.converters.StoryContentResponseConverter
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.PreferencesKeys.getShouldShowStoriesKey
import com.tangem.datasource.local.preferences.utils.get
import com.tangem.datasource.local.preferences.utils.getSyncOrDefault
import com.tangem.datasource.local.preferences.utils.store
import com.tangem.datasource.local.promo.PromoStoriesStore
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.promo.models.StoryContent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.referral.domain.ReferralRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
@ -23,24 +28,41 @@ internal class DefaultPromoRepository(
private val appPreferencesStore: AppPreferencesStore,
private val promoStoriesStore: PromoStoriesStore,
private val dispatchers: CoroutineDispatcherProvider,
private val referralRepository: ReferralRepository,
) : PromoRepository {
private val storyContentConverter = StoryContentResponseConverter()
override fun isReadyToShowWalletSwapPromo(): Flow<Boolean> {
return flowOf(false) // Use it on new promo action
override fun isReadyToShowWalletPromo(userWalletId: UserWalletId, promoId: PromoId): Flow<Boolean> {
return appPreferencesStore.get(
key = PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name),
default = true,
).map { shouldShow ->
if (promoId == PromoId.Referral) {
!referralRepository.isReferralParticipant(userWalletId) && shouldShow
} else {
shouldShow
}
}
}
override fun isReadyToShowTokenSwapPromo(): Flow<Boolean> {
return flowOf(false) // Use it on new promo action
override fun isReadyToShowTokenPromo(promoId: PromoId): Flow<Boolean> {
return if (promoId == PromoId.Referral) {
flowOf(false)
} else {
appPreferencesStore.get(
PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name),
default = false,
)
}
}
override suspend fun setNeverToShowWalletSwapPromo() {
// Use it on new promo action
override suspend fun setNeverToShowWalletPromo(promoId: PromoId) {
appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false)
}
override suspend fun setNeverToShowTokenSwapPromo() {
// Use it on new promo action
override suspend fun setNeverToShowTokenPromo(promoId: PromoId) {
appPreferencesStore.store(PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), false)
}
override fun getStoryById(id: String): Flow<StoryContent?> = isReadyToShowStories(id).mapLatest {

View file

@ -5,6 +5,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.promo.PromoStoriesStore
import com.tangem.domain.promo.PromoRepository
import com.tangem.feature.referral.domain.ReferralRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -23,12 +24,14 @@ internal object PromoDataModule {
appPreferencesStore: AppPreferencesStore,
promoStoriesStore: PromoStoriesStore,
dispatchers: CoroutineDispatcherProvider,
referralRepository: ReferralRepository,
): PromoRepository {
return DefaultPromoRepository(
tangemApi = tangemTechApi,
appPreferencesStore = appPreferencesStore,
promoStoriesStore = promoStoriesStore,
dispatchers = dispatchers,
referralRepository = referralRepository,
)
}
}

View file

@ -23,4 +23,8 @@ data class PromoBanner(
private companion object {
const val ACTIVE_STATUS = "active"
}
}
enum class PromoId {
Referral,
}

View file

@ -1,18 +1,20 @@
package com.tangem.domain.promo
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.promo.models.StoryContent
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
interface PromoRepository {
// region Promo
fun isReadyToShowWalletSwapPromo(): Flow<Boolean>
fun isReadyToShowWalletPromo(userWalletId: UserWalletId, promoId: PromoId): Flow<Boolean>
fun isReadyToShowTokenSwapPromo(): Flow<Boolean>
fun isReadyToShowTokenPromo(promoId: PromoId): Flow<Boolean>
suspend fun setNeverToShowWalletSwapPromo()
suspend fun setNeverToShowWalletPromo(promoId: PromoId)
suspend fun setNeverToShowTokenSwapPromo()
suspend fun setNeverToShowTokenPromo(promoId: PromoId)
// endregion
// region Stories

View file

@ -0,0 +1,11 @@
package com.tangem.domain.promo
import com.tangem.domain.promo.models.PromoId
import kotlinx.coroutines.flow.Flow
class ShouldShowPromoTokenUseCase(private val promoRepository: PromoRepository) {
operator fun invoke(promoId: PromoId): Flow<Boolean> = promoRepository.isReadyToShowTokenPromo(promoId)
suspend fun neverToShow(promoId: PromoId) = promoRepository.setNeverToShowTokenPromo(promoId)
}

View file

@ -0,0 +1,19 @@
package com.tangem.domain.promo
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
class ShouldShowPromoWalletUseCase(private val promoRepository: PromoRepository) {
operator fun invoke(userWalletId: UserWalletId, promoId: PromoId): Flow<Boolean> {
return flow {
emit(false)
emitAll(promoRepository.isReadyToShowWalletPromo(userWalletId, promoId))
}
}
suspend fun neverToShow(promoId: PromoId) = promoRepository.setNeverToShowWalletPromo(promoId)
}

View file

@ -1,10 +0,0 @@
package com.tangem.domain.promo
import kotlinx.coroutines.flow.Flow
class ShouldShowSwapPromoTokenUseCase(private val promoRepository: PromoRepository) {
operator fun invoke(): Flow<Boolean> = promoRepository.isReadyToShowTokenSwapPromo()
suspend fun neverToShow() = promoRepository.setNeverToShowTokenSwapPromo()
}

View file

@ -1,10 +0,0 @@
package com.tangem.domain.promo
import kotlinx.coroutines.flow.Flow
class ShouldShowSwapPromoWalletUseCase(private val promoRepository: PromoRepository) {
operator fun invoke(): Flow<Boolean> = promoRepository.isReadyToShowWalletSwapPromo()
suspend fun neverToShow() = promoRepository.setNeverToShowWalletSwapPromo()
}

View file

@ -12,6 +12,7 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.txhistory.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.promo.models)
/** Other dependencies */
implementation(deps.kotlin.serialization)

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.model.warnings
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.tokens.model.CryptoCurrency
import org.joda.time.DateTime
import java.math.BigDecimal
@ -42,6 +43,7 @@ sealed class CryptoCurrencyWarning {
data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning()
data class SwapPromo(
val promoId: PromoId,
val startDateTime: DateTime,
val endDateTime: DateTime,
) : CryptoCurrencyWarning()

View file

@ -39,6 +39,10 @@ internal class ReferralRepositoryImpl @Inject constructor(
}
}
override suspend fun isReferralParticipant(userWalletId: UserWalletId): Boolean {
return getReferralData(userWalletId.stringValue) is ReferralData.ParticipantData
}
override suspend fun startReferral(
walletId: String,
networkId: String,

View file

@ -10,6 +10,9 @@ interface ReferralRepository {
/** Returns data object of [ReferralData] depends on user program status */
suspend fun getReferralData(walletId: String): ReferralData
/** Returns whether user is participating in referral program */
suspend fun isReferralParticipant(userWalletId: UserWalletId): Boolean
/** Starts user referral program */
suspend fun startReferral(walletId: String, networkId: String, tokenId: String, address: String): ReferralData

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model
import com.tangem.common.ui.bottomsheet.receive.AddressModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
@ -49,9 +50,9 @@ interface TokenDetailsClickIntents {
fun onGoToProviderClick(url: String)
fun onSwapPromoDismiss()
fun onSwapPromoDismiss(promoId: PromoId)
fun onSwapPromoClick()
fun onSwapPromoClick(promoId: PromoId)
fun onGenerateExtendedKey()

View file

@ -35,7 +35,8 @@ import com.tangem.domain.card.NetworkHasDerivationUseCase
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.promo.ShouldShowPromoTokenUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
@ -101,7 +102,7 @@ internal class TokenDetailsModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase,
private val shouldShowPromoTokenUseCase: ShouldShowPromoTokenUseCase,
private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase,
private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase,
@ -811,9 +812,9 @@ internal class TokenDetailsModel @Inject constructor(
router.openUrl(url)
}
override fun onSwapPromoDismiss() {
override fun onSwapPromoDismiss(promoId: PromoId) {
modelScope.launch(dispatchers.main) {
shouldShowSwapPromoTokenUseCase.neverToShow()
shouldShowPromoTokenUseCase.neverToShow(promoId)
analyticsEventsHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Token,
@ -824,9 +825,9 @@ internal class TokenDetailsModel @Inject constructor(
}
}
override fun onSwapPromoClick() {
override fun onSwapPromoClick(promoId: PromoId) {
modelScope.launch(dispatchers.main) {
shouldShowSwapPromoTokenUseCase.neverToShow()
shouldShowPromoTokenUseCase.neverToShow(promoId)
analyticsEventsHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Token,

View file

@ -10,10 +10,10 @@ import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.model.warnings.HederaWarnings
import com.tangem.domain.tokens.model.warnings.KaspaWarnings
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification.*
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.removeBy
@ -98,8 +98,8 @@ internal class TokenDetailsNotificationConverter(
is CryptoCurrencyWarning.SwapPromo -> SwapPromo(
startDateTime = warning.startDateTime,
endDateTime = warning.endDateTime,
onSwapClick = clickIntents::onSwapPromoClick,
onCloseClick = clickIntents::onSwapPromoDismiss,
onSwapClick = { clickIntents.onSwapPromoClick(warning.promoId) },
onCloseClick = { clickIntents.onSwapPromoDismiss(warning.promoId) },
)
is CryptoCurrencyWarning.BeaconChainShutdown -> NetworkShutdown(
title = resourceReference(R.string.warning_beacon_chain_retirement_title),

View file

@ -15,6 +15,7 @@ import com.tangem.domain.nft.FetchNFTCollectionsUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
@ -356,9 +357,7 @@ internal class WalletModel @Inject constructor(
coroutineScope = modelScope,
)
if (action.selectedWallet.scanResponse.cardTypesResolver.isSingleWallet()) {
fetchCurrencyStatusUseCase(userWalletId = action.selectedWallet.walletId)
}
fetchIfSingleWallet(action.selectedWallet)
if (action.wallets.size > 1 && isWalletsScrollPreviewEnabled()) {
withContext(dispatchers.io) { delay(timeMillis = 1_800) }
@ -385,6 +384,8 @@ internal class WalletModel @Inject constructor(
coroutineScope = modelScope,
)
fetchIfSingleWallet(userWallet = action.selectedWallet)
stateHolder.update(
ReinitializeWalletTransformer(
prevWalletId = action.prevWalletId,
@ -402,12 +403,7 @@ internal class WalletModel @Inject constructor(
coroutineScope = modelScope,
)
if (action.selectedWallet.scanResponse.cardTypesResolver.isSingleWallet()) {
modelScope.launch {
fetchCurrencyStatusUseCase(userWalletId = action.selectedWallet.walletId)
.onLeft { Timber.e(it.toString()) }
}
}
fetchIfSingleWallet(userWallet = action.selectedWallet)
stateHolder.update(
AddWalletTransformer(
@ -506,6 +502,15 @@ internal class WalletModel @Inject constructor(
}
}
private fun fetchIfSingleWallet(userWallet: UserWallet) {
if (userWallet.scanResponse.cardTypesResolver.isSingleWallet()) {
modelScope.launch {
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId)
.onLeft { Timber.e(it.toString()) }
}
}
}
inner class AskBiometryModelCallbacks : AskBiometryComponent.ModelCallbacks {
override fun onAllowed() {
analyticsEventsHandler.send(MainScreenAnalyticsEvent.EnableBiometrics(AnalyticsParam.OnOffState.On))

View file

@ -2,6 +2,8 @@ package com.tangem.feature.wallet.child.wallet.model.intents
import arrow.core.getOrElse
import com.tangem.common.TangemBlogUrlBuilder
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
@ -13,7 +15,8 @@ import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.NeverToSuggestRateAppUseCase
@ -67,7 +70,9 @@ internal interface WalletWarningsClickIntents {
fun onCloseRateAppWarningClick()
fun onCloseSwapPromoClick()
fun onClosePromoClick(promoId: PromoId)
fun onPromoClick(promoId: PromoId)
fun onSupportClick()
@ -98,13 +103,14 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val reduxStateHolder: ReduxStateHolder,
private val dispatchers: CoroutineDispatcherProvider,
private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase,
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
private val urlOpener: UrlOpener,
private val tokensFeatureToggles: TokensFeatureToggles,
private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
private val appRouter: AppRouter,
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
override fun onAddBackupCardClick() {
@ -268,16 +274,31 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
}
}
override fun onCloseSwapPromoClick() {
override fun onClosePromoClick(promoId: PromoId) {
analyticsEventHandler.send(
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
),
if (promoId == PromoId.Referral) {
MainScreen.ReferralPromoButtonDismiss
} else {
TokenSwapPromoAnalyticsEvent.PromotionBannerClicked(
source = AnalyticsParam.ScreensSources.Main,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action
action = TokenSwapPromoAnalyticsEvent.PromotionBannerClicked.BannerAction.Closed,
)
},
)
modelScope.launch(dispatchers.main) {
shouldShowSwapPromoWalletUseCase.neverToShow()
shouldShowPromoWalletUseCase.neverToShow(promoId)
}
}
override fun onPromoClick(promoId: PromoId) {
if (promoId == PromoId.Referral) {
val userWallet = getSelectedUserWallet() ?: return
analyticsEventHandler.send(MainScreen.ReferralPromoButtonParticipate)
modelScope.launch {
shouldShowPromoWalletUseCase.neverToShow(promoId)
appRouter.push(AppRoute.ReferralProgram(userWalletId = userWallet.walletId))
}
}
}

View file

@ -6,7 +6,9 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeepLink
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.deeplink.global.BuyCurrencyDeepLink
import com.tangem.core.deeplink.global.ReferralDeepLink
import com.tangem.core.deeplink.global.SellCurrencyDeepLink
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.tokens.GetCryptoCurrencyUseCase
import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent
import com.tangem.domain.wallets.models.UserWallet
@ -54,6 +56,7 @@ internal class WalletDeepLinksHandler @Inject constructor(
)
return buildList {
addReferralDeepLink(userWallet)
add(sellCurrencyDeepLink)
if (onrampFeatureToggles.isFeatureEnabled || !userWallet.isMultiCurrency) {
add(
@ -95,4 +98,18 @@ internal class WalletDeepLinksHandler @Inject constructor(
analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol))
}
}
private fun MutableList<DeepLink>.addReferralDeepLink(userWallet: UserWallet) {
add(
ReferralDeepLink(
onReceive = {
if (userWallet.cardTypesResolver.isTangemWallet()) {
router.push(
AppRoute.ReferralProgram(userWalletId = userWallet.walletId),
)
}
},
),
)
}
}

View file

@ -137,5 +137,11 @@ sealed class WalletScreenAnalyticsEvent {
data object NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used")
data object NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined")
// region Referral Promo
data object ReferralPromo : MainScreen(event = "Referral Banner")
data object ReferralPromoButtonParticipate : MainScreen(event = "Button - Referral Participate")
data object ReferralPromoButtonDismiss : MainScreen(event = "Button - Referral Dismiss")
//endregion
}
}

View file

@ -53,6 +53,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
source = AnalyticsParam.ScreensSources.Main,
programName = ProgramName.Empty, // Use it on new promo action
)
is WalletNotification.ReferralPromo -> MainScreen.ReferralPromo
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
is WalletNotification.Informational.NoAccount,
is WalletNotification.Warning.LowSignatures,

View file

@ -6,6 +6,8 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.StatusSource
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
@ -34,6 +36,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val isNeedToBackupUseCase: IsNeedToBackupUseCase,
private val backupValidator: BackupValidator,
private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase,
private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase,
) {
@Suppress("MagicNumber", "MaximumLineLength")
@ -45,12 +48,15 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
flow2 = isReadyToShowRateAppUseCase(),
flow3 = isNeedToBackupUseCase(userWallet.walletId),
flow4 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus ->
flow5 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Referral),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus, shouldShowReferralPromo ->
buildList {
addUsedOutdatedDataNotification(maybeTokenList)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo)
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents)
@ -187,6 +193,20 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
.map(CryptoCurrencyStatus::currency)
}
private fun MutableList<WalletNotification>.addReferralPromoNotification(
cardTypesResolver: CardTypesResolver,
clickIntents: WalletClickIntents,
shouldShowPromo: Boolean,
) {
addIf(
element = WalletNotification.ReferralPromo(
onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.Referral) },
onClick = { clickIntents.onPromoClick(promoId = PromoId.Referral) },
),
condition = shouldShowPromo && cardTypesResolver.isTangemWallet(),
)
}
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver,
tokenList: Lce<TokenListError, TokenList>,

View file

@ -254,4 +254,20 @@ sealed class WalletNotification(val config: NotificationConfig) {
iconResId = R.drawable.ic_error_sync_24,
),
)
data class ReferralPromo(
val onCloseClick: () -> Unit,
val onClick: () -> Unit,
) : WalletNotification(
config = NotificationConfig(
title = resourceReference(R.string.notification_referral_promo_title),
subtitle = resourceReference(R.string.notification_referral_promo_text),
iconResId = R.drawable.img_referral_promo,
onCloseClick = onCloseClick,
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
text = resourceReference(R.string.notification_referral_promo_button),
onClick = onClick,
),
),
)
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.notifications.NoteMigrationNotification
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
@ -38,6 +39,11 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
Notification(
config = it.config,
modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null),
iconSize = if (it is WalletNotification.ReferralPromo) {
54.dp
} else {
20.dp
},
iconTint = when (it) {
is WalletNotification.Critical -> TangemTheme.colors.icon.warning
is WalletNotification.Informational -> TangemTheme.colors.icon.accent