Updated on 2026-08-14
This commit is contained in:
commit
0bdf627def
12 changed files with 219 additions and 30 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -29,4 +29,8 @@ interface SettingsRepository {
|
|||
suspend fun setShouldSaveAccessCodes(value: Boolean)
|
||||
|
||||
suspend fun incrementAppLaunchCounter()
|
||||
|
||||
suspend fun shouldShowMarketsTooltip(): Boolean
|
||||
|
||||
suspend fun setMarketsTooltipShown(value: Boolean)
|
||||
}
|
||||
|
|
@ -158,5 +158,7 @@ internal object WalletScreenPreviewData {
|
|||
onWalletChange = {},
|
||||
event = consumedEvent(),
|
||||
isHidingMode = false,
|
||||
showMarketsOnboarding = false,
|
||||
onDismissMarketsOnboarding = {},
|
||||
)
|
||||
}
|
||||
|
|
@ -94,6 +94,8 @@ internal class WalletStateController @Inject constructor() {
|
|||
onWalletChange = {},
|
||||
event = consumedEvent(),
|
||||
isHidingMode = false,
|
||||
showMarketsOnboarding = false,
|
||||
onDismissMarketsOnboarding = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -34,6 +34,7 @@ internal class InitializeWalletsTransformer(
|
|||
}
|
||||
.toImmutableList(),
|
||||
onWalletChange = clickIntents::onWalletChange,
|
||||
onDismissMarketsOnboarding = clickIntents::onDismissMarketsOnboarding,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue