Updated on 2026-08-14

This commit is contained in:
Tangem 2022-02-22 23:49:06 +03:00
parent 3c3db2cc09
commit 2c62f977c1
46 changed files with 660 additions and 73 deletions

View file

@ -2,35 +2,25 @@ package com.tangem.tap.features.home
import android.os.Bundle
import android.view.View
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.ViewStateWidget
import com.tangem.tap.common.transitions.FrontCardEnterTransition
import com.tangem.tap.common.transitions.FrontCardExitTransition
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_home_top.*
import kotlinx.android.synthetic.main.view_bg_home.*
import org.rekotlin.StoreSubscriber
class HomeFragment : BaseOnboardingFragment<HomeState>() {
class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState> {
private lateinit var btnScanCard: ViewStateWidget
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_home_top
var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
store.dispatch(HomeAction.Init)
}
@ -39,50 +29,36 @@ class HomeFragment : BaseOnboardingFragment<HomeState>() {
store.dispatch(BackupAction.CheckForUnfinishedBackup)
toolbar.hide()
val shareTransition = FragmentShareTransition(
listOf(
ShareElement(imv_front_card, ShareElement.imvFrontCard),
ShareElement(imv_back_card, ShareElement.imvBackCard),
ShareElement(bg_circle_large),
ShareElement(bg_circle_medium),
ShareElement(bg_circle_min),
),
FrontCardEnterTransition(),
FrontCardExitTransition()
)
store.dispatch(HomeAction.SetFragmentShareTransition(shareTransition))
tv_header.setText(R.string.home_welcome_header)
tv_body.setText(R.string.home_welcome_body)
btn_main_action.setText(R.string.home_button_scan)
btn_main_action.setOnClickListener { store.dispatch(HomeAction.ReadCard) }
btn_alternative_action.setText(R.string.home_button_get_new_card)
btn_alternative_action.setOnClickListener { store.dispatch(HomeAction.GoToShop) }
btnScanCard = IndeterminateProgressButtonWidget(btn_main_action, progress)
getView()?.findViewById<ComposeView>(R.id.cv_stories)?.setContent {
AppCompatTheme {
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = { store.dispatch(HomeAction.GoToShop) }
)
}
}
}
override fun subscribeToStore() {
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.homeState == newState.homeState
}.select { it.homeState }
}
storeSubscribersList.add(this)
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: HomeState) {
if (activity == null) return
btnScanCard.changeState(state.btnScanState.progressState)
btnScanCard.mainView.isEnabled = state.btnScanState.enabled
homeState.value = state
}
override fun onDestroyView() {
store.dispatch(HomeAction.SetFragmentShareTransition(null))
super.onDestroyView()
}
}

View file

@ -0,0 +1,146 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun FirstStoriesContent(
paused: Boolean, duration: Int = 8_000,
hideContent: (Boolean) -> Unit
) {
val screenState = remember { mutableStateOf(StartingScreenState.INIT) }
val progress = remember { Animatable(0f) }
LaunchedEffect(paused) {
if (paused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 2f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing
)
)
}
}
when (progress.value) {
in 0f..0.2f -> screenState.value = StartingScreenState.INIT
in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY
in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE
in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND
in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY
in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE
in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW
in 0.8f..1f -> screenState.value = StartingScreenState.LEND
in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD
in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM
}
if (screenState.value == StartingScreenState.INIT) hideContent(true)
if (screenState.value == StartingScreenState.BUY) hideContent(false)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier =
Modifier
.fillMaxSize(),
) {
val text = when (screenState.value) {
StartingScreenState.INIT -> null
StartingScreenState.BUY -> R.string.story_meet_buy
StartingScreenState.STORE -> R.string.story_meet_store
StartingScreenState.SEND -> R.string.story_meet_send
StartingScreenState.PAY -> R.string.story_meet_pay
StartingScreenState.EXCHANGE -> R.string.story_meet_exchange
StartingScreenState.BORROW -> R.string.story_meet_borrow
StartingScreenState.LEND -> R.string.story_meet_lend
StartingScreenState.SHOW_CARD -> R.string.story_meet_title
StartingScreenState.MEET_TANGEM -> R.string.story_meet_title
}
if (screenState.value != StartingScreenState.MEET_TANGEM) {
Text(
text = text?.let { stringResource(text) } ?: "",
fontSize = 60.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(start = 40.dp, end = 40.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
color = Color.White,
textAlign = TextAlign.Center,
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.MEET_TANGEM,
enter = slideInVertically() { it / 2 }
) {
Text(
text = text?.let { stringResource(text) } ?: "",
fontSize = 60.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(start = 40.dp, end = 40.dp),
color = Color.White,
textAlign = TextAlign.Center
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.SHOW_CARD ||
screenState.value == StartingScreenState.MEET_TANGEM,
enter = scaleIn(initialScale = 3f)
) {
Image(
painter = painterResource(
id = R.drawable.meet_tangem
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
}
}
enum class StartingScreenState {
INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM
}
@Preview
@Composable
fun Stories1Preview() {
FirstStoriesContent(false, 7_000) {}
}

View file

@ -0,0 +1,66 @@
package com.tangem.tap.features.home.compose
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
import androidx.compose.foundation.layout.fillMaxWidth as fillMaxWidth1
@Composable
fun HomeButtons(
isDarkBackground: Boolean, modifier: Modifier = Modifier,
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit
) {
val darkColorBackground = Color(0xFF26292E)
val darkColorButton = Color(0xFF080C10)
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = modifier.fillMaxWidth1()
) {
Spacer(modifier = Modifier.size(16.dp))
Button(
modifier = Modifier
.weight(1f)
.height(42.dp),
onClick = onScanButtonClick,
colors = ButtonDefaults.textButtonColors(
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
contentColor = if (isDarkBackground) Color.White else darkColorButton
)
) {
Text(
text = stringResource(id = R.string.home_button_scan),
fontWeight = FontWeight.Medium,
fontSize = 16.sp
)
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier
.weight(1f)
.height(42.dp),
onClick = onShopButtonClick,
colors = ButtonDefaults.textButtonColors(
backgroundColor = if (isDarkBackground) Color.White else darkColorBackground,
contentColor = if (isDarkBackground) darkColorButton else Color.White
)
) {
Text(
text = stringResource(id = R.string.home_button_order),
fontWeight = FontWeight.Medium,
fontSize = 16.sp
)
}
Spacer(modifier = Modifier.size(16.dp))
}
}

View file

@ -0,0 +1,145 @@
package com.tangem.tap.features.home.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
import kotlin.math.max
@Composable
fun StoriesScreen(
homeState: MutableState<HomeState> = mutableStateOf(HomeState()),
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit
) {
val steps = 6
val stepDuration = 8_000
val currentStep = remember { mutableStateOf(1) }
val isDarkBackground = currentStep.value !in 3..5
val goToPreviousScreen = {
currentStep.value = max(1, currentStep.value - 1)
}
val goToNextScreen = {
currentStep.value = if (currentStep.value < steps) currentStep.value + 1 else 1
}
val isPressed = remember { mutableStateOf(false) }
val needsToBePaused =
remember(homeState) { mutableStateOf(homeState.value.btnScanState.progressState == ProgressState.Loading) }
val pause = isPressed.value || needsToBePaused.value
val hideContent = remember { mutableStateOf(true) }
Box(
Modifier
.fillMaxSize()
.background(Color(0xFF090E13))
.pointerInput(Unit) {
val maxWidth = this.size.width
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed.value = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) {
val isTapOnRightTwoTiers = (it.x > (maxWidth / 2))
if (isTapOnRightTwoTiers) {
goToNextScreen()
} else {
goToPreviousScreen()
}
}
isPressed.value = false
},
)
}
) {
if (!isDarkBackground) {
Image(
painter = painterResource(id = R.drawable.ic_overlay),
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier
.fillMaxSize()
) {
Spacer(modifier = Modifier.size(24.dp))
StoriesProgressBar(
steps = steps,
currentStep = currentStep.value,
// paused = isPressed.value,
stepDuration = stepDuration,
paused = pause,
onStepFinished = goToNextScreen,
)
Image(
painter = painterResource(id = R.drawable.ic_tangem_logo),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.padding(start = 16.dp, top = 10.dp)
.height(17.dp)
.alpha(if (hideContent.value) 0f else 1f)
.align(Alignment.Start),
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black)
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
when (currentStep.value) {
1 -> FirstStoriesContent(pause, stepDuration) { hideContent.value = it }
2 -> StoriesRevolutionaryWallet()
3 -> StoriesUltraSecureBackup()
4 -> StoriesThousandsOfCurrencies()
5 -> StoriesWeb3()
6 -> StoriesWalletForEveryone()
}
}
HomeButtons(
isDarkBackground = isDarkBackground,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(37.dp),
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick
)
}
}
@Preview
@Composable
fun InstagramScreenPreview() {
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {})
}

View file

@ -0,0 +1,120 @@
package com.tangem.tap.features.home.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
@Composable
fun StoriesGeneralContent(
titleText: String,
subtitleText: String,
imageSource: Int?,
isDarkBackground: Boolean,
imageComposable: (() -> Unit)? = null,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier =
Modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
Text(
text = titleText,
fontSize = 32.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(start = 40.dp, end = 40.dp),
color = if (isDarkBackground) Color.White else Color(0xFF090E13),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = subtitleText,
fontSize = 20.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier
.padding(start = 40.dp, end = 40.dp),
color = Color(0xFFA6AAAD),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(25.dp))
if (imageSource != null) {
Image(
painter = painterResource(id = imageSource),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
imageComposable?.invoke()
}
}
@Composable
fun StoriesRevolutionaryWallet() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_awe_title),
subtitleText = stringResource(id = R.string.story_awe_description),
imageSource = R.drawable.revolutionary_wallet,
isDarkBackground = true
)
}
@Composable
fun StoriesUltraSecureBackup() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_backup_title),
subtitleText = stringResource(id = R.string.story_backup_description),
imageSource = R.drawable.floating_cards,
isDarkBackground = false
)
}
@Composable
fun StoriesThousandsOfCurrencies() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_currencies_title),
subtitleText = stringResource(id = R.string.story_currencies_description),
imageSource = R.drawable.thousands_of_currencies,
isDarkBackground = false
)
}
@Composable
fun StoriesWeb3() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_web3_title),
subtitleText = stringResource(id = R.string.story_web3_description),
imageSource = R.drawable.web_3,
isDarkBackground = false
)
}
@Composable
fun StoriesWalletForEveryone() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_finish_title),
subtitleText = stringResource(id = R.string.story_finish_description),
imageSource = R.drawable.wallet_for_everyone,
isDarkBackground = true
)
}

View file

@ -0,0 +1,80 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun StoriesProgressBar(
steps: Int,
currentStep: Int,
paused: Boolean = false,
stepDuration: Int = 8_000,
onStepFinished: () -> Unit = {},
) {
val progress = remember(currentStep) { Animatable(0f) }
LaunchedEffect(paused, currentStep) {
if (paused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
easing = LinearEasing
)
)
progress.snapTo(0f)
onStepFinished()
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
// .height()
.padding(start = 9.dp, end = 9.dp, top = 16.dp),
) {
for (index in 1..steps) {
Row(
modifier = Modifier
.height(2.dp)
.weight(1f)
.background(Color.White.copy(alpha = 0.4f))
) {
Box(
modifier = Modifier
.background(Color.White)
.fillMaxHeight().let {
when (index) {
currentStep -> it.fillMaxWidth(progress.value)
in 0..currentStep -> it.fillMaxWidth(1f)
else -> it
}
},
) {}
}
if (index != steps) {
Spacer(modifier = Modifier.width(4.dp))
}
}
}
}
@Preview
@Composable
fun StoriesProgressBarPreview() {
StoriesProgressBar(steps = 3, currentStep = 2, paused = false) { }
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import org.rekotlin.Action
sealed class HomeAction : Action {
@ -10,10 +9,9 @@ sealed class HomeAction : Action {
object GoToShop : HomeAction()
// internal
data class ShouldScanCardOnResume(val shouldScanCard: Boolean):HomeAction()
data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction()
object Init : HomeAction()
data class SetFragmentShareTransition(val shareTransition: FragmentShareTransition?) : HomeAction()
data class SetTermsOfUseState(val isDisclaimerAccepted: Boolean) : HomeAction()
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()

View file

@ -73,14 +73,14 @@ private fun handleReadCard() {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo, store.state.homeState.shareTransition)
navigateTo(navigateTo)
} else {
scope.launch {
store.onCardScanned(scanResponse)
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins, store.state.homeState.shareTransition)
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
}
@ -98,7 +98,7 @@ private fun changeButtonState(state: ButtonState) {
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
}
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition?) {
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) {
post(DELAY_SDK_DIALOG_CLOSE) {
changeButtonState(ButtonState.ENABLED)
store.dispatch(NavigationAction.NavigateTo(screen, transition))

View file

@ -15,9 +15,6 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
var state = state.homeState
when (action) {
is HomeAction.SetFragmentShareTransition -> {
state = state.copy(shareTransition = action.shareTransition)
}
is HomeAction.ShouldScanCardOnResume -> {
state = state.copy(shouldScanCardOnResume = action.shouldScanCard)
}

View file

@ -1,12 +1,10 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.send.redux.states.ButtonState
import org.rekotlin.StateType
data class HomeState(
val shouldScanCardOnResume: Boolean = false,
val shareTransition: FragmentShareTransition? = null,
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
) : StateType