Updated on 2026-08-14
This commit is contained in:
commit
8f036f6002
23 changed files with 164 additions and 28 deletions
|
|
@ -212,6 +212,7 @@ internal class ChildFactory @Inject constructor(
|
|||
params = StoriesComponent.Params(
|
||||
storyId = route.storyId,
|
||||
nextScreen = route.nextScreen,
|
||||
screenSource = route.screenSource,
|
||||
),
|
||||
componentFactory = storiesComponentFactory,
|
||||
)
|
||||
|
|
@ -245,6 +246,7 @@ internal class ChildFactory @Inject constructor(
|
|||
currencyTo = route.currencyTo,
|
||||
userWalletId = route.userWalletId,
|
||||
isInitialReverseOrder = route.isInitialReverseOrder,
|
||||
screenSource = route.screenSource,
|
||||
),
|
||||
componentFactory = swapComponentFactory,
|
||||
)
|
||||
|
|
@ -437,6 +439,7 @@ internal class ChildFactory @Inject constructor(
|
|||
currencyTo = route.currencyTo,
|
||||
userWalletId = route.userWalletId,
|
||||
isInitialReverseOrder = route.isInitialReverseOrder,
|
||||
screenSource = route.screenSource,
|
||||
),
|
||||
componentFactory = swapComponentFactory,
|
||||
)
|
||||
|
|
@ -564,6 +567,7 @@ internal class ChildFactory @Inject constructor(
|
|||
params = StoriesComponent.Params(
|
||||
storyId = route.storyId,
|
||||
nextScreen = route.nextScreen,
|
||||
screenSource = route.screenSource,
|
||||
),
|
||||
componentFactory = storiesComponentFactory,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val currencyTo: CryptoCurrency? = null,
|
||||
val userWalletId: UserWalletId,
|
||||
val isInitialReverseOrder: Boolean = false,
|
||||
val screenSource: String,
|
||||
) : AppRoute(
|
||||
path = "/swap" +
|
||||
"/${currencyFrom.id.value}" +
|
||||
|
|
@ -297,6 +298,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
data class Stories(
|
||||
val storyId: String,
|
||||
val nextScreen: AppRoute,
|
||||
val screenSource: String,
|
||||
) : AppRoute(path = "/stories$storyId"), RouteBundleParams {
|
||||
override fun getBundle(): Bundle = bundle(serializer())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ object SwapStoriesFactory {
|
|||
// WARNING! Be careful with indices. Temporary solution.
|
||||
// Use all data from v1/stories api (image url, title, subtitle)
|
||||
@Suppress("MagicNumber")
|
||||
fun createStoriesState(swapStory: StoryContent, onStoriesClose: () -> Unit): SwapStoriesUM {
|
||||
fun createStoriesState(swapStory: StoryContent, onStoriesClose: (Int) -> Unit): SwapStoriesUM {
|
||||
val storyOrderedImageUrls = swapStory.getImageUrls()
|
||||
if (storyOrderedImageUrls.size != 5) return SwapStoriesUM.Empty
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.common.ui.swapStoriesScreen
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -39,7 +38,6 @@ private const val STORIES_RELATIVE_PADDING = 0.7
|
|||
fun SwapStoriesScreen(config: SwapStoriesUM) {
|
||||
if (config !is SwapStoriesUM.Content) return
|
||||
|
||||
BackHandler(onBack = config.onClose)
|
||||
SystemBarsIconsDisposable(darkIcons = false)
|
||||
|
||||
StoriesContainer(
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ sealed class SwapStoriesUM {
|
|||
|
||||
data class Content(
|
||||
override val stories: ImmutableList<Config>,
|
||||
override val onClose: () -> Unit,
|
||||
override val onClose: (Int) -> Unit,
|
||||
) : SwapStoriesUM(), StoriesContentConfig<Content.Config> {
|
||||
override val isRestartable: Boolean = false
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@ sealed class AnalyticsParam {
|
|||
data object Sell : ScreensSources("Sell")
|
||||
data object Backup : ScreensSources("Backup")
|
||||
data object Onboarding : ScreensSources("Onboarding")
|
||||
data object LongTap : ScreensSources("Long Tap")
|
||||
data object Markets : ScreensSources("Markets")
|
||||
}
|
||||
|
||||
sealed class TxSentFrom(val value: String) {
|
||||
|
|
@ -207,5 +209,6 @@ sealed class AnalyticsParam {
|
|||
const val PLACE = "Place"
|
||||
const val RESIDENCE = "Residence"
|
||||
const val PAYMENT_METHOD = "Payment Method"
|
||||
const val WATCHED = "Watched"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.core.ui.components.stories
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
|
|
@ -46,6 +47,7 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
isPauseStories: Boolean = false,
|
||||
crossinline currentStoryContent: @Composable BoxScope.(T, Boolean) -> Unit,
|
||||
) {
|
||||
var watchedCounter by remember { mutableIntStateOf(1) }
|
||||
var isPressed by remember { mutableStateOf(value = false) }
|
||||
val storyState by remember(config) {
|
||||
mutableStateOf(
|
||||
|
|
@ -55,18 +57,23 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
),
|
||||
)
|
||||
}
|
||||
BackHandler(onBack = { config.onClose(watchedCounter) })
|
||||
|
||||
val isPaused = isPressed || isPauseStories
|
||||
|
||||
val onNextClick = {
|
||||
storyState.nextStory()
|
||||
watchedCounter = (watchedCounter + 1).coerceAtMost(config.stories.size)
|
||||
|
||||
if (!storyState.hasNext()) { // Finish stories if nothing left to show
|
||||
config.onClose(watchedCounter)
|
||||
}
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
StoriesClickableArea(
|
||||
onPress = { isPressed = it },
|
||||
onPreviousStory = storyState::prevStory,
|
||||
onNextStory = {
|
||||
if (storyState.nextStory()) {
|
||||
config.onClose()
|
||||
}
|
||||
},
|
||||
onNextStory = onNextClick,
|
||||
)
|
||||
|
||||
currentStoryContent(storyState.currentStory, isPaused)
|
||||
|
|
@ -79,11 +86,7 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
currentStep = storyState.currentIndex.intValue,
|
||||
stepDuration = storyState.currentStory.duration,
|
||||
paused = isPaused,
|
||||
onStepFinish = {
|
||||
if (storyState.nextStory()) {
|
||||
config.onClose()
|
||||
}
|
||||
},
|
||||
onStepFinish = onNextClick,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
|
|
@ -97,7 +100,7 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = LocalIndication.current,
|
||||
onClick = config.onClose,
|
||||
onClick = { config.onClose(watchedCounter) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -133,7 +136,7 @@ private data class StoryContainerPreviewProviderData(
|
|||
StoryConfigPreviewProviderData(title = "Wallet"),
|
||||
),
|
||||
override val isRestartable: Boolean = true,
|
||||
override val onClose: () -> Unit = {},
|
||||
override val onClose: (Int) -> Unit = {},
|
||||
) : StoriesContentConfig<StoryConfigPreviewProviderData>
|
||||
|
||||
private data class StoryConfigPreviewProviderData(
|
||||
|
|
|
|||
|
|
@ -19,20 +19,22 @@ class StoriesStepStateMachine<T>(
|
|||
val currentStory
|
||||
get() = stories[currentIndex.intValue.coerceIn(FIRST_INDEX, steps)]
|
||||
|
||||
fun nextStory(): Boolean {
|
||||
fun nextStory() {
|
||||
_currentIndex.intValue = if (currentIndex.intValue == steps && isRepeatable) {
|
||||
FIRST_INDEX
|
||||
} else {
|
||||
currentIndex.intValue.inc().coerceAtMost(stories.size)
|
||||
}
|
||||
|
||||
return currentIndex.intValue > steps
|
||||
}
|
||||
|
||||
fun prevStory() {
|
||||
_currentIndex.intValue = currentIndex.intValue.dec().coerceAtLeast(FIRST_INDEX)
|
||||
}
|
||||
|
||||
fun hasNext(): Boolean {
|
||||
return currentIndex.intValue <= steps
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val FIRST_INDEX = 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
interface StoriesContentConfig<T : StoryConfig> {
|
||||
val stories: ImmutableList<T>
|
||||
val isRestartable: Boolean
|
||||
val onClose: () -> Unit
|
||||
val onClose: (Int) -> Unit
|
||||
}
|
||||
|
||||
interface StoryConfig {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,6 @@ data class StoryContent(
|
|||
}
|
||||
}
|
||||
|
||||
enum class StoryContentIds(val id: String) {
|
||||
STORY_FIRST_TIME_SWAP("first-time-swap"),
|
||||
enum class StoryContentIds(val id: String, val analyticType: String) {
|
||||
STORY_FIRST_TIME_SWAP(id = "first-time-swap", analyticType = "Swap"),
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface StoriesComponent : ComposableContentComponent {
|
|||
data class Params(
|
||||
val storyId: String,
|
||||
val nextScreen: AppRoute,
|
||||
val screenSource: String,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, StoriesComponent>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -164,7 +164,8 @@ internal class SwapNotificationsFactory(
|
|||
cryptoCurrencyStatus = fromCurrencyStatus,
|
||||
onReduceClick = { reduceBy, reduceByDiff, _ ->
|
||||
actions.onReduceByAmount(
|
||||
amount.copy(value = amount.value.minus(reduceByDiff)),
|
||||
// use in swap notification amountToRequest because fee is already subtracted
|
||||
amountToRequest.copy(value = amountToRequest.value.minus(reduceByDiff)),
|
||||
reduceBy,
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -623,7 +623,13 @@ internal class TokenDetailsModel @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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue