Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-05 15:13:26 +04:00
commit 0bdf627def
12 changed files with 219 additions and 30 deletions

View file

@ -57,6 +57,14 @@ internal object SettingsDomainModule {
return ShouldShowSaveWalletScreenUseCase(settingsRepository = settingsRepository)
}
@Provides
@Singleton
fun provideShouldShowMarketsTooltipUseCase(
settingsRepository: SettingsRepository,
): ShouldShowMarketsTooltipUseCase {
return ShouldShowMarketsTooltipUseCase(settingsRepository = settingsRepository)
}
@Provides
@Singleton
fun providesCanUseBiometryUseCase(tangemSdkManager: TangemSdkManager): CanUseBiometryUseCase {

View file

@ -83,6 +83,8 @@ object PreferencesKeys {
val SHOULD_SAVE_ACCESS_CODES_KEY by lazy { booleanPreferencesKey(name = "saveAccessCodes") }
val SHOULD_SHOW_MARKETS_TOOLTIP_KEY by lazy { booleanPreferencesKey(name = "shouldShowMarketsTooltip") }
val IS_WALLET_NAMES_MIGRATION_DONE_KEY by lazy { booleanPreferencesKey(name = "isWalletNamesMigrationDone") }
val UNSUBMITTED_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "unsubmittedTransactions") }

View file

@ -88,4 +88,15 @@ internal class DefaultSettingsRepository(
preferences[PreferencesKeys.APP_LAUNCH_COUNT_KEY] = count + 1
}
}
override suspend fun shouldShowMarketsTooltip(): Boolean {
return appPreferencesStore.getSyncOrDefault(
key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY,
default = true,
)
}
override suspend fun setMarketsTooltipShown(value: Boolean) {
appPreferencesStore.store(key = PreferencesKeys.SHOULD_SHOW_MARKETS_TOOLTIP_KEY, value = !value)
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.domain.settings
import com.tangem.domain.settings.repositories.SettingsRepository
class ShouldShowMarketsTooltipUseCase(private val settingsRepository: SettingsRepository) {
suspend operator fun invoke(): Boolean = settingsRepository.shouldShowMarketsTooltip()
suspend operator fun invoke(isShown: Boolean) = settingsRepository.setMarketsTooltipShown(isShown)
}

View file

@ -29,4 +29,8 @@ interface SettingsRepository {
suspend fun setShouldSaveAccessCodes(value: Boolean)
suspend fun incrementAppLaunchCounter()
suspend fun shouldShowMarketsTooltip(): Boolean
suspend fun setMarketsTooltipShown(value: Boolean)
}

View file

@ -158,5 +158,7 @@ internal object WalletScreenPreviewData {
onWalletChange = {},
event = consumedEvent(),
isHidingMode = false,
showMarketsOnboarding = false,
onDismissMarketsOnboarding = {},
)
}

View file

@ -94,6 +94,8 @@ internal class WalletStateController @Inject constructor() {
onWalletChange = {},
event = consumedEvent(),
isHidingMode = false,
showMarketsOnboarding = false,
onDismissMarketsOnboarding = {},
)
}
}

View file

@ -13,4 +13,6 @@ internal data class WalletScreenState(
val onWalletChange: (Int) -> Unit,
val event: StateEvent<WalletEvent>,
val isHidingMode: Boolean,
val showMarketsOnboarding: Boolean,
val onDismissMarketsOnboarding: () -> Unit,
)

View file

@ -34,6 +34,7 @@ internal class InitializeWalletsTransformer(
}
.toImmutableList(),
onWalletChange = clickIntents::onWalletChange,
onDismissMarketsOnboarding = clickIntents::onDismissMarketsOnboarding,
)
}

View file

@ -3,10 +3,10 @@ package com.tangem.feature.wallet.presentation.wallet.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.TweenSpec
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.*
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
@ -22,7 +22,10 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
@ -34,6 +37,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.systemuicontroller.rememberSystemUiController
@ -58,6 +63,9 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.lineTo
import com.tangem.core.ui.utils.moveTo
import com.tangem.core.ui.utils.toPx
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState
import com.tangem.feature.wallet.presentation.wallet.state.model.*
@ -75,10 +83,11 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.balances
import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.depositButton
import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.BottomSheetState.*
import com.tangem.features.markets.entry.MarketsEntryComponent
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable
internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent?) {
@ -223,7 +232,7 @@ private fun WalletContent(
}
if (marketsEntryComponent != null) {
val bottomSheetState = remember { mutableStateOf(COLLAPSED) }
val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) }
var headerSize by remember { mutableStateOf(0.dp) }
@ -357,8 +366,9 @@ private inline fun BaseScaffoldWithMarkets(
snackbarHostState = snackbarHostState,
)
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(this).toDp() }
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() }
val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() }
val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight
val maxHeight = LocalWindowSize.current.height
@ -420,28 +430,139 @@ private inline fun BaseScaffoldWithMarkets(
},
)
Column {
WalletTopBar(config = state.topBarConfig)
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
content()
Box {
Column {
WalletTopBar(config = state.topBarConfig)
Box(
modifier = Modifier.pullRefresh(pullRefreshState),
) {
content()
WalletPullToRefreshIndicator(
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
WalletPullToRefreshIndicator(
isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing,
state = pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter),
)
}
}
}
BottomSheetScrim(
color = BottomSheetDefaults.ScrimColor,
visible = bottomSheetState.targetValue == SheetValue.Expanded,
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
)
BottomSheetScrim(
color = BottomSheetDefaults.ScrimColor,
visible = bottomSheetState.targetValue == SheetValue.Expanded || state.showMarketsOnboarding,
onDismissRequest = { coroutineScope.launch { bottomSheetState.partialExpand() } },
)
MarketsTooltip(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 24.dp)
.fillMaxWidth(fraction = 0.7f),
isVisible = state.showMarketsOnboarding,
availableHeight = maxHeight - statusBarHeight - bottomBarHeight,
bottomSheetState = bottomSheetState,
)
}
},
)
LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) {
if (state.showMarketsOnboarding && bottomSheetState.targetValue == SheetValue.Expanded) {
state.onDismissMarketsOnboarding()
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MarketsTooltip(
availableHeight: Dp,
bottomSheetState: SheetState,
isVisible: Boolean,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
val tooltipOffset by remember {
derivedStateOf {
val bottomSheetOffset = try {
// Can throw exception during the first composition
with(density) { bottomSheetState.requireOffset().toDp() }
} catch (e: Exception) {
0.dp
}
bottomSheetOffset - availableHeight
}
}
var visible by remember { mutableStateOf(value = false) }
LaunchedEffect(isVisible) {
if (isVisible) {
delay(timeMillis = 300)
}
visible = isVisible
}
val slideOffset = 40.dp.toPx()
AnimatedVisibility(
modifier = modifier.absoluteOffset(y = tooltipOffset),
visible = visible,
enter = slideIn(
animationSpec = spring(
stiffness = Spring.StiffnessLow,
visibilityThreshold = IntOffset.VisibilityThreshold,
),
initialOffset = { _ -> IntOffset(y = -slideOffset.roundToInt(), x = 0) },
) + fadeIn(),
exit = fadeOut(),
) {
MarketsTooltipContent()
}
}
@Composable
private fun MarketsTooltipContent(modifier: Modifier = Modifier) {
val backgroundColor = TangemTheme.colors.background.primary
val cornerRadius = CornerRadius(x = 14.dp.toPx())
val tipDpSize = DpSize(width = 20.dp, height = 8.dp)
Column(
modifier = modifier
.padding(bottom = tipDpSize.height)
.drawBehind {
val rect = size.toRect()
val tipSize = tipDpSize.toSize()
val tipRect = Rect(
offset = Offset(
x = rect.center.x - tipSize.center.x,
y = rect.bottom,
),
size = tipSize,
)
drawRoundRect(color = backgroundColor, cornerRadius = cornerRadius)
val tipPath = Path().apply {
moveTo(tipRect.topLeft)
lineTo(tipRect.bottomCenter)
lineTo(tipRect.topRight)
}
drawPath(color = backgroundColor, path = tipPath)
}
.padding(all = 12.dp),
verticalArrangement = Arrangement.spacedBy(space = 4.dp),
horizontalAlignment = Alignment.Start,
) {
Text(
text = stringResource(id = R.string.markets_tooltip_title),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = stringResource(id = R.string.markets_tooltip_message),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
}
}
@ -449,7 +570,7 @@ private inline fun BaseScaffoldWithMarkets(
private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) {
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = TweenSpec(),
animationSpec = tween(),
label = "scrim",
)
val dismissSheet = if (visible) {
@ -557,9 +678,9 @@ private fun BottomSheetStateEffects(
LaunchedEffect(isSheetHidden) {
onBottomSheetStateChange(
if (isSheetHidden) {
COLLAPSED
BottomSheetState.COLLAPSED
} else {
EXPANDED
BottomSheetState.EXPANDED
},
)
}

View file

@ -7,10 +7,7 @@ import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.settings.CanUseBiometryUseCase
import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
import com.tangem.domain.settings.*
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
@ -30,6 +27,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender
import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents
import com.tangem.features.markets.MarketsFeatureToggles
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION
import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
@ -58,6 +56,7 @@ internal class WalletViewModel @Inject constructor(
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val getWalletsUseCase: GetWalletsUseCase,
private val shouldShowSaveWalletScreenUseCase: ShouldShowSaveWalletScreenUseCase,
private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase,
private val canUseBiometryUseCase: CanUseBiometryUseCase,
private val isWalletsScrollPreviewEnabled: IsWalletsScrollPreviewEnabled,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
@ -69,6 +68,7 @@ internal class WalletViewModel @Inject constructor(
private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase,
private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase,
private val pushNotificationsFeatureToggles: PushNotificationsFeatureToggles,
private val marketsFeatureToggles: MarketsFeatureToggles,
analyticsEventsHandler: AnalyticsEventHandler,
) : ViewModel() {
@ -83,6 +83,7 @@ internal class WalletViewModel @Inject constructor(
analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened)
suggestToEnableBiometrics()
suggestToOpenMarkets()
maybeMigrateNames()
subscribeToUserWalletsUpdates()
@ -121,6 +122,20 @@ internal class WalletViewModel @Inject constructor(
}
}
private fun suggestToOpenMarkets() {
viewModelScope.launch {
withContext(dispatchers.io) { delay(timeMillis = 1_800) }
if (marketsFeatureToggles.isFeatureEnabled && shouldShowMarketsTooltipUseCase()) {
stateHolder.update {
it.copy(showMarketsOnboarding = true)
}
}
shouldShowMarketsTooltipUseCase(isShown = true)
}
}
private suspend fun isShowSaveWalletScreenEnabled(): Boolean {
return router.isWalletLastScreen() && shouldShowSaveWalletScreenUseCase() && canUseBiometryUseCase()
}

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.TokensAction
@ -35,6 +36,8 @@ internal interface WalletContentClickIntents {
fun onOrganizeTokensClick()
fun onDismissMarketsOnboarding()
fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus)
fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus)
@ -52,6 +55,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase,
private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
private val dispatchers: CoroutineDispatcherProvider,
private val reduxStateHolder: ReduxStateHolder,
@ -98,6 +102,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId())
}
override fun onDismissMarketsOnboarding() {
stateHolder.update { it.copy(showMarketsOnboarding = false) }
viewModelScope.launch {
shouldShowMarketsTooltipUseCase(isShown = true)
}
}
override fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus) {
analyticsEventHandler.send(PortfolioEvent.TokenTapped)
router.openTokenDetails(stateHolder.getSelectedWalletId(), currencyStatus)