Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-21 16:03:19 +05:00
parent 054a7f982f
commit a7ab7abbfb
23 changed files with 165 additions and 30 deletions

View file

@ -1,6 +1,7 @@
package com.tangem.features.markets.portfolio.impl.model
import com.tangem.common.routing.AppRoute
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
@ -155,6 +156,7 @@ internal class TokenActionsHandler @AssistedInject constructor(
currencyFrom = cryptoCurrencyData.status.currency,
userWalletId = cryptoCurrencyData.userWallet.walletId,
isInitialReverseOrder = true,
screenSource = AnalyticsParam.ScreensSources.Markets.value,
),
)
}

View file

@ -94,6 +94,7 @@ internal class SwapSelectTokensModel @Inject constructor(
currencyFrom = requireNotNull(fromCurrencyStatus.value).currency,
currencyTo = status.currency,
userWalletId = params.userWalletId,
screenSource = AnalyticsParam.ScreensSources.Main.value,
),
onComplete = {
modelScope.launch {

View file

@ -9,6 +9,7 @@ interface StoriesComponent : ComposableContentComponent {
data class Params(
val storyId: String,
val nextScreen: AppRoute,
val screenSource: String,
)
interface Factory : ComponentFactory<Params, StoriesComponent>

View file

@ -28,6 +28,7 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.navigation)
implementation(projects.core.ui)
implementation(projects.core.analytics)
/** AndroidX */
implementation(deps.androidx.activity.compose)

View file

@ -0,0 +1,33 @@
package com.tangem.feature.stories.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.WATCHED
sealed class StoriesEvents(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Stories", event, params) {
data class SwapStories(
val source: String,
val watchCount: String,
) : StoriesEvents(
event = "Swap Stories",
params = mapOf(
AnalyticsParam.SOURCE to source,
WATCHED to watchCount,
),
)
data class Error(
val source: String,
val type: String,
) : StoriesEvents(
event = "Error",
params = mapOf(
AnalyticsParam.SOURCE to source,
AnalyticsParam.TYPE to type,
),
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.stories.impl.model
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesFactory
import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
@ -9,6 +10,7 @@ import com.tangem.domain.promo.GetStoryContentUseCase
import com.tangem.domain.promo.ShouldShowStoriesUseCase
import com.tangem.domain.promo.models.StoryContentIds
import com.tangem.feature.stories.api.StoriesComponent
import com.tangem.feature.stories.impl.analytics.StoriesEvents
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -23,6 +25,7 @@ internal class StoriesModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val getStoryContentUseCase: GetStoryContentUseCase,
private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
val state: StateFlow<SwapStoriesUM> get() = _state
@ -50,16 +53,36 @@ internal class StoriesModel @Inject constructor(
getStoryContentUseCase.invokeSync(params.storyId).fold(
ifLeft = {
Timber.e("Unable to load stories for ${StoryContentIds.STORY_FIRST_TIME_SWAP.id}")
analyticsEventHandler.send(
StoriesEvents.Error(
source = params.screenSource,
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
),
)
openScreen(hideStories = false) // Fallback to target screen
},
ifRight = { swapStory ->
if (swapStory == null) {
analyticsEventHandler.send(
StoriesEvents.Error(
source = params.screenSource,
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
),
)
openScreen(hideStories = false) // Fallback to target screen
} else {
_state.update {
SwapStoriesFactory.createStoriesState(
swapStory = swapStory,
onStoriesClose = ::openScreen,
onStoriesClose = { watchCount ->
analyticsEventHandler.send(
StoriesEvents.SwapStories(
source = params.screenSource,
watchCount = watchCount.toString(),
),
)
openScreen()
},
)
}
}

View file

@ -12,6 +12,7 @@ interface SwapComponent : ComposableContentComponent {
val currencyTo: CryptoCurrency? = null,
val userWalletId: UserWalletId,
val isInitialReverseOrder: Boolean = false,
val screenSource: String,
)
interface Factory : ComponentFactory<Params, SwapComponent>

View file

@ -0,0 +1,33 @@
package com.tangem.feature.swap.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.WATCHED
sealed class StoriesEvents(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Stories", event, params) {
data class SwapStories(
val source: String,
val watchCount: String,
) : StoriesEvents(
event = "Swap Stories",
params = mapOf(
AnalyticsParam.SOURCE to source,
WATCHED to watchCount,
),
)
data class Error(
val source: String,
val type: String,
) : StoriesEvents(
event = "Error",
params = mapOf(
AnalyticsParam.SOURCE to source,
AnalyticsParam.TYPE to type,
),
)
}

View file

@ -37,6 +37,7 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.feature.swap.analytics.SwapEvents
import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
@ -277,10 +278,23 @@ internal class SwapModel @Inject constructor(
getStoryContentUseCase.invokeSync(StoryContentIds.STORY_FIRST_TIME_SWAP.id).fold(
ifLeft = {
Timber.e("Unable to load stories for ${StoryContentIds.STORY_FIRST_TIME_SWAP.id}")
analyticsEventHandler.send(
StoriesEvents.Error(
source = params.screenSource,
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
),
)
},
ifRight = { story ->
story?.let {
if (story != null) {
uiState = stateBuilder.createStoriesState(uiState, story)
} else {
analyticsEventHandler.send(
StoriesEvents.Error(
source = params.screenSource,
type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType,
),
)
}
},
)
@ -1131,7 +1145,13 @@ internal class SwapModel @Inject constructor(
onSuccess = {
swapRouter.openScreen(SwapNavScreen.Success)
},
onStoriesClose = {
onStoriesClose = { watchCount ->
analyticsEventHandler.send(
StoriesEvents.SwapStories(
source = params.screenSource,
watchCount = watchCount.toString(),
),
)
modelScope.launch { shouldShowStoriesUseCase.neverToShow(StoryContentIds.STORY_FIRST_TIME_SWAP.id) }
swapRouter.openScreen(SwapNavScreen.Main)
},

View file

@ -21,7 +21,7 @@ data class UiActions(
val openPermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
// region new actions
val onStoriesClose: () -> Unit,
val onStoriesClose: (Int) -> Unit,
val onRetryClick: () -> Unit,
val onClickFee: () -> Unit,
val onSelectFeeType: (TxFee) -> Unit,

View file

@ -36,6 +36,7 @@ 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.redux.ReduxStateHolder
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
@ -73,7 +74,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory
import com.tangem.domain.promo.ShouldShowSwapPromoTokenUseCase
import com.tangem.features.onramp.OnrampFeatureToggles
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
@ -645,7 +645,13 @@ internal class TokenDetailsViewModel @Inject constructor(
return
}
appRouter.push(AppRoute.Swap(currencyFrom = cryptoCurrency, userWalletId = userWalletId))
appRouter.push(
AppRoute.Swap(
currencyFrom = cryptoCurrency,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.Token.value,
),
)
}
override fun onDismissDialog() {

View file

@ -358,6 +358,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
AppRoute.Swap(
currencyFrom = cryptoCurrencyStatus.currency,
userWalletId = userWalletId,
screenSource = AnalyticsParam.ScreensSources.LongTap.value,
),
)
}
@ -617,6 +618,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
AppRoute.Stories(
storyId = StoryContentIds.STORY_FIRST_TIME_SWAP.id,
nextScreen = targetRoute,
screenSource = AnalyticsParam.ScreensSources.Main.value,
)
} else {
targetRoute