mirror of
https://github.com/bitwarden/android.git
synced 2026-10-22 02:53:19 -05:00
PM-42849: Feat: Update Debug menu UI (#7338)
This commit is contained in:
1 parent
1d80697c2b
commit
8152f5c6f8
8 files changed
+465
-221
No files matched your search
+260
-210
@@ -1,5 +1,6 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.debugmenu
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -14,32 +15,39 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.bitwarden.ui.platform.base.util.scrolledContainerBottomDivider
|
||||
import com.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.bitwarden.ui.platform.base.util.toListItemCardStyle
|
||||
import com.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.bitwarden.ui.platform.components.appbar.NavigationIcon
|
||||
import com.bitwarden.ui.platform.components.appbar.model.TopAppBarDividerStyle
|
||||
import com.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.bitwarden.ui.platform.components.content.BitwardenEmptyContent
|
||||
import com.bitwarden.ui.platform.components.debug.ListItemContent
|
||||
import com.bitwarden.ui.platform.components.divider.BitwardenHorizontalDivider
|
||||
import com.bitwarden.ui.platform.components.header.BitwardenListHeaderText
|
||||
import com.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.bitwarden.ui.platform.components.segment.BitwardenSegmentedButton
|
||||
import com.bitwarden.ui.platform.components.segment.SegmentedButtonState
|
||||
import com.bitwarden.ui.platform.components.segment.transition.segmentedContentTransform
|
||||
import com.bitwarden.ui.platform.components.util.rememberVectorPainter
|
||||
import com.bitwarden.ui.platform.resource.BitwardenDrawable
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.handler.DebugMenuHandler
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.handler.rememberDebugMenuHandler
|
||||
import kotlinx.collections.immutable.ImmutableMap
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Top level screen for the debug menu.
|
||||
@@ -52,7 +60,7 @@ fun DebugMenuScreen(
|
||||
viewModel: DebugMenuViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
|
||||
val handler = rememberDebugMenuHandler(viewModel = viewModel)
|
||||
EventsEffect(viewModel = viewModel) { event ->
|
||||
when (event) {
|
||||
DebugMenuEvent.NavigateBack -> onNavigateBack()
|
||||
@@ -62,152 +70,93 @@ fun DebugMenuScreen(
|
||||
BitwardenScaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
.nestedScroll(connection = scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
BitwardenTopAppBar(
|
||||
title = stringResource(BitwardenString.debug_menu),
|
||||
title = stringResource(id = BitwardenString.debug_menu),
|
||||
scrollBehavior = scrollBehavior,
|
||||
dividerStyle = TopAppBarDividerStyle.NONE,
|
||||
navigationIcon = NavigationIcon(
|
||||
navigationIcon = rememberVectorPainter(BitwardenDrawable.ic_back),
|
||||
navigationIcon = rememberVectorPainter(id = BitwardenDrawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = BitwardenString.back),
|
||||
onNavigationIconClick = {
|
||||
viewModel.trySendAction(DebugMenuAction.NavigateBack)
|
||||
},
|
||||
onNavigationIconClick = handler.onNavigateBack,
|
||||
),
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
if (state.featureFlags.isNotEmpty()) {
|
||||
FeatureFlagContent(
|
||||
featureFlagMap = state.featureFlags,
|
||||
onValueChange = { key, value ->
|
||||
viewModel.trySendAction(DebugMenuAction.UpdateFeatureFlag(key, value))
|
||||
},
|
||||
onResetValues = {
|
||||
viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues)
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
}
|
||||
OnboardingOverrideContent(
|
||||
onStartOnboarding = { viewModel.trySendAction(DebugMenuAction.RestartOnboarding) },
|
||||
onStartOnboardingCarousel = {
|
||||
viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel)
|
||||
},
|
||||
utilityBar = {
|
||||
BitwardenSegmentedButton(
|
||||
options = DebugMenuState
|
||||
.MainTypeOption
|
||||
.entries
|
||||
.map {
|
||||
SegmentedButtonState(
|
||||
text = it.label(),
|
||||
onClick = { handler.onMainTypeOptionClick(it) },
|
||||
isChecked = it == state.mainTypeOption,
|
||||
testTag = it.testTag,
|
||||
)
|
||||
}
|
||||
.toImmutableList(),
|
||||
modifier = Modifier
|
||||
.scrolledContainerBottomDivider(topAppBarScrollBehavior = scrollBehavior)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
},
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = state.mainTypeOption,
|
||||
transitionSpec = { this.segmentedContentTransform() },
|
||||
label = "DebugContentTransition",
|
||||
) {
|
||||
when (it) {
|
||||
DebugMenuState.MainTypeOption.FLAGS -> {
|
||||
FeatureFlagContent(
|
||||
featureFlagMap = state.featureFlags,
|
||||
handler = handler,
|
||||
)
|
||||
}
|
||||
|
||||
DebugMenuState.MainTypeOption.OPTIONS -> {
|
||||
DebugOptionsContent(handler = handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureFlagContent(
|
||||
featureFlagMap: ImmutableMap<FlagKey<Any>, Any>,
|
||||
handler: DebugMenuHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (featureFlagMap.isEmpty()) {
|
||||
BitwardenEmptyContent(
|
||||
text = stringResource(id = BitwardenString.no_feature_flags),
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
Column(modifier = modifier.verticalScroll(state = rememberScrollState())) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
featureFlagMap.forEach { featureFlag ->
|
||||
featureFlag.key.ListItemContent(
|
||||
currentValue = featureFlag.value,
|
||||
onValueChange = handler.onUpdateFeatureFlag,
|
||||
cardStyle = featureFlagMap.keys.toListItemCardStyle(
|
||||
index = featureFlagMap.keys.indexOf(element = featureFlag.key),
|
||||
),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.reset_accessibility_disclaimer),
|
||||
onClick = { viewModel.trySendAction(DebugMenuAction.ResetAccessibilityDisclaimer) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.reset_coach_mark_tour_status),
|
||||
onClick = { viewModel.trySendAction(DebugMenuAction.ResetCoachMarkTourStatuses) },
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(BitwardenString.cookies),
|
||||
label = stringResource(id = BitwardenString.reset_values),
|
||||
onClick = handler.onResetFeatureFlagValues,
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.trigger_cookie_acquisition),
|
||||
onClick = { viewModel.trySendAction(DebugMenuAction.TriggerCookieAcquisition) },
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.clear_sso_cookies),
|
||||
onClick = {
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.ClearSsoCookies,
|
||||
)
|
||||
},
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(BitwardenString.premium),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.reset_premium_upgrade_banner),
|
||||
onClick = {
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.ResetPremiumUpgradeBanner,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.show_upgraded_to_premium_card),
|
||||
onClick = {
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.ShowUpgradedToPremiumCard,
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
Spacer(Modifier.height(height = 16.dp))
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(BitwardenString.error_reports),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.generate_error_report),
|
||||
onClick = { viewModel.trySendAction(DebugMenuAction.GenerateErrorReportClick) },
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.generate_crash),
|
||||
onClick = { viewModel.trySendAction(DebugMenuAction.GenerateCrashClick) },
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
@@ -216,96 +165,201 @@ fun DebugMenuScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeatureFlagContent(
|
||||
featureFlagMap: ImmutableMap<FlagKey<Any>, Any>,
|
||||
onValueChange: (key: FlagKey<Any>, value: Any) -> Unit,
|
||||
onResetValues: () -> Unit,
|
||||
private fun DebugOptionsContent(
|
||||
handler: DebugMenuHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
Column(modifier = modifier.verticalScroll(state = rememberScrollState())) {
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
OnboardingSection(handler = handler)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
CookiesSection(handler = handler)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
PremiumSection(handler = handler)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
ReportsSection(handler = handler)
|
||||
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun OnboardingSection(
|
||||
handler: DebugMenuHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(BitwardenString.feature_flags),
|
||||
label = stringResource(id = BitwardenString.onboarding_override),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
featureFlagMap.forEach { featureFlag ->
|
||||
featureFlag.key.ListItemContent(
|
||||
currentValue = featureFlag.value,
|
||||
onValueChange = onValueChange,
|
||||
cardStyle = featureFlagMap.keys.toListItemCardStyle(
|
||||
index = featureFlagMap.keys.indexOf(element = featureFlag.key),
|
||||
),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
ButtonWithExplanation(
|
||||
label = stringResource(id = BitwardenString.restart_onboarding_cta),
|
||||
explanation = stringResource(id = BitwardenString.restart_onboarding_details),
|
||||
onClick = handler.onRestartOnboarding,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
ButtonWithExplanation(
|
||||
label = stringResource(id = BitwardenString.restart_onboarding_carousel),
|
||||
explanation = stringResource(id = BitwardenString.restart_onboarding_carousel_details),
|
||||
onClick = handler.onRestartOnboardingCarousel,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.reset_values),
|
||||
onClick = onResetValues,
|
||||
label = stringResource(id = BitwardenString.reset_accessibility_disclaimer),
|
||||
onClick = handler.onResetAccessibilityDisclaimer,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.reset_coach_mark_tour_status),
|
||||
onClick = handler.onResetCoachMarkTourStatuses,
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CookiesSection(
|
||||
handler: DebugMenuHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = BitwardenString.cookies),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.trigger_cookie_acquisition),
|
||||
onClick = handler.onTriggerCookieAcquisition,
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.clear_sso_cookies),
|
||||
onClick = handler.onClearSsoCookies,
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PremiumSection(
|
||||
handler: DebugMenuHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = BitwardenString.premium),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.reset_premium_upgrade_banner),
|
||||
onClick = handler.onResetPremiumUpgradeBanner,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.show_upgraded_to_premium_card),
|
||||
onClick = handler.onShowUpgradedToPremiumCard,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 16.dp))
|
||||
BitwardenHorizontalDivider()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReportsSection(
|
||||
handler: DebugMenuHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(id = BitwardenString.error_reports),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 8.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.generate_error_report),
|
||||
onClick = handler.onGenerateErrorReportClick,
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(height = 12.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = BitwardenString.generate_crash),
|
||||
onClick = handler.onGenerateCrashClick,
|
||||
isEnabled = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The content for the onboarding override feature flag.
|
||||
*/
|
||||
@Composable
|
||||
private fun OnboardingOverrideContent(
|
||||
onStartOnboarding: () -> Unit,
|
||||
onStartOnboardingCarousel: () -> Unit,
|
||||
private fun ButtonWithExplanation(
|
||||
label: String,
|
||||
explanation: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier) {
|
||||
BitwardenListHeaderText(
|
||||
label = stringResource(BitwardenString.onboarding_override),
|
||||
modifier = Modifier
|
||||
.standardHorizontalMargin()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Column(modifier = modifier) {
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.restart_onboarding_cta),
|
||||
onClick = onStartOnboarding,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
label = label,
|
||||
onClick = onClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(height = 4.dp))
|
||||
Text(
|
||||
text = stringResource(BitwardenString.restart_onboarding_details),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.standardHorizontalMargin(),
|
||||
text = explanation,
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(BitwardenString.restart_onboarding_carousel),
|
||||
onClick = onStartOnboardingCarousel,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(BitwardenString.restart_onboarding_carousel_details),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.standardHorizontalMargin(),
|
||||
style = BitwardenTheme.typography.bodySmall,
|
||||
color = BitwardenTheme.colorScheme.text.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -318,19 +372,15 @@ private fun FeatureFlagContent_preview() {
|
||||
featureFlagMap = persistentMapOf(
|
||||
FlagKey.DummyBoolean to true,
|
||||
),
|
||||
onValueChange = { _, _ -> },
|
||||
onResetValues = { },
|
||||
handler = DebugMenuHandler.createEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun OnboardingOverrideContent_preview() {
|
||||
private fun DebugOptionsContent_preview() {
|
||||
BitwardenTheme {
|
||||
OnboardingOverrideContent(
|
||||
onStartOnboarding = {},
|
||||
onStartOnboardingCarousel = {},
|
||||
)
|
||||
DebugOptionsContent(handler = DebugMenuHandler.createEmpty())
|
||||
}
|
||||
}
|
||||
+32
-2
@@ -4,6 +4,9 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.bitwarden.data.repository.util.baseWebVaultUrlOrDefault
|
||||
import com.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.bitwarden.ui.platform.resource.BitwardenString
|
||||
import com.bitwarden.ui.util.Text
|
||||
import com.bitwarden.ui.util.asText
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.manager.CookieAcquisitionRequestManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
@@ -37,7 +40,10 @@ class DebugMenuViewModel @Inject constructor(
|
||||
private val cookieAcquisitionRequestManager: CookieAcquisitionRequestManager,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
) : BaseViewModel<DebugMenuState, DebugMenuEvent, DebugMenuAction>(
|
||||
initialState = DebugMenuState(featureFlags = persistentMapOf()),
|
||||
initialState = DebugMenuState(
|
||||
featureFlags = persistentMapOf(),
|
||||
mainTypeOption = DebugMenuState.MainTypeOption.FLAGS,
|
||||
),
|
||||
) {
|
||||
|
||||
private var featureFlagResetJob: Job? = null
|
||||
@@ -68,9 +74,14 @@ class DebugMenuViewModel @Inject constructor(
|
||||
DebugMenuAction.ResetPremiumUpgradeBanner -> handleResetPremiumUpgradeBanner()
|
||||
DebugMenuAction.ShowUpgradedToPremiumCard -> handleShowUpgradedToPremiumCard()
|
||||
DebugMenuAction.ResetAccessibilityDisclaimer -> handleResetAccessibilityDisclaimer()
|
||||
is DebugMenuAction.MainTypeOptionClick -> handleMainTypeOptionClick(action)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMainTypeOptionClick(action: DebugMenuAction.MainTypeOptionClick) {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = action.option) }
|
||||
}
|
||||
|
||||
private fun handleResetAccessibilityDisclaimer() {
|
||||
debugMenuRepository.resetAccessibilityDisclaimer()
|
||||
}
|
||||
@@ -148,7 +159,20 @@ class DebugMenuViewModel @Inject constructor(
|
||||
*/
|
||||
data class DebugMenuState(
|
||||
val featureFlags: ImmutableMap<FlagKey<Any>, Any>,
|
||||
)
|
||||
val mainTypeOption: MainTypeOption,
|
||||
) {
|
||||
/**
|
||||
* Enum representing the main type options for the debug menu, such as Feature flags and
|
||||
* options.
|
||||
*/
|
||||
enum class MainTypeOption(
|
||||
val label: Text,
|
||||
val testTag: String,
|
||||
) {
|
||||
FLAGS(label = BitwardenString.feature_flags.asText(), testTag = "feature_flags"),
|
||||
OPTIONS(label = BitwardenString.debug_options.asText(), testTag = "debug_options"),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Models event for the [DebugMenuViewModel] to send to the UI.
|
||||
@@ -164,6 +188,12 @@ sealed class DebugMenuEvent {
|
||||
* Models action for the [DebugMenuViewModel] to handle.
|
||||
*/
|
||||
sealed class DebugMenuAction {
|
||||
/**
|
||||
* Indicates that the main option type has been changed by the user.
|
||||
*/
|
||||
data class MainTypeOptionClick(
|
||||
val option: DebugMenuState.MainTypeOption,
|
||||
) : DebugMenuAction()
|
||||
|
||||
/**
|
||||
* Updates a feature flag for the given [FlagKey] to the given [newValue].
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package com.x8bit.bitwarden.ui.platform.feature.debugmenu.handler
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.bitwarden.core.data.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.DebugMenuAction
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.DebugMenuState
|
||||
import com.x8bit.bitwarden.ui.platform.feature.debugmenu.DebugMenuViewModel
|
||||
|
||||
/**
|
||||
* Handler for the debug menu screen lambda invocations.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class DebugMenuHandler(
|
||||
val onNavigateBack: () -> Unit,
|
||||
val onMainTypeOptionClick: (option: DebugMenuState.MainTypeOption) -> Unit,
|
||||
val onUpdateFeatureFlag: (flagKey: FlagKey<Any>, newValue: Any) -> Unit,
|
||||
val onResetFeatureFlagValues: () -> Unit,
|
||||
val onRestartOnboarding: () -> Unit,
|
||||
val onRestartOnboardingCarousel: () -> Unit,
|
||||
val onResetAccessibilityDisclaimer: () -> Unit,
|
||||
val onResetCoachMarkTourStatuses: () -> Unit,
|
||||
val onTriggerCookieAcquisition: () -> Unit,
|
||||
val onClearSsoCookies: () -> Unit,
|
||||
val onResetPremiumUpgradeBanner: () -> Unit,
|
||||
val onShowUpgradedToPremiumCard: () -> Unit,
|
||||
val onGenerateErrorReportClick: () -> Unit,
|
||||
val onGenerateCrashClick: () -> Unit,
|
||||
) {
|
||||
@Suppress("UndocumentedPublicClass")
|
||||
companion object {
|
||||
/**
|
||||
* Create [DebugMenuHandler] with the given [viewModel] to send actions to.
|
||||
*/
|
||||
fun create(viewModel: DebugMenuViewModel): DebugMenuHandler = DebugMenuHandler(
|
||||
onNavigateBack = { viewModel.trySendAction(DebugMenuAction.NavigateBack) },
|
||||
onMainTypeOptionClick = {
|
||||
viewModel.trySendAction(DebugMenuAction.MainTypeOptionClick(it))
|
||||
},
|
||||
onUpdateFeatureFlag = { key, value ->
|
||||
viewModel.trySendAction(DebugMenuAction.UpdateFeatureFlag(key, value))
|
||||
},
|
||||
onResetFeatureFlagValues = {
|
||||
viewModel.trySendAction(DebugMenuAction.ResetFeatureFlagValues)
|
||||
},
|
||||
onRestartOnboarding = { viewModel.trySendAction(DebugMenuAction.RestartOnboarding) },
|
||||
onRestartOnboardingCarousel = {
|
||||
viewModel.trySendAction(DebugMenuAction.RestartOnboardingCarousel)
|
||||
},
|
||||
onResetAccessibilityDisclaimer = {
|
||||
viewModel.trySendAction(DebugMenuAction.ResetAccessibilityDisclaimer)
|
||||
},
|
||||
onResetCoachMarkTourStatuses = {
|
||||
viewModel.trySendAction(DebugMenuAction.ResetCoachMarkTourStatuses)
|
||||
},
|
||||
onTriggerCookieAcquisition = {
|
||||
viewModel.trySendAction(DebugMenuAction.TriggerCookieAcquisition)
|
||||
},
|
||||
onClearSsoCookies = { viewModel.trySendAction(DebugMenuAction.ClearSsoCookies) },
|
||||
onResetPremiumUpgradeBanner = {
|
||||
viewModel.trySendAction(DebugMenuAction.ResetPremiumUpgradeBanner)
|
||||
},
|
||||
onShowUpgradedToPremiumCard = {
|
||||
viewModel.trySendAction(DebugMenuAction.ShowUpgradedToPremiumCard)
|
||||
},
|
||||
onGenerateErrorReportClick = {
|
||||
viewModel.trySendAction(DebugMenuAction.GenerateErrorReportClick)
|
||||
},
|
||||
onGenerateCrashClick = { viewModel.trySendAction(DebugMenuAction.GenerateCrashClick) },
|
||||
)
|
||||
|
||||
/**
|
||||
* Create [DebugMenuHandler] with all empty callbacks. This should only be used for
|
||||
* previews.
|
||||
*/
|
||||
fun createEmpty(): DebugMenuHandler = DebugMenuHandler(
|
||||
onNavigateBack = { },
|
||||
onMainTypeOptionClick = { },
|
||||
onUpdateFeatureFlag = { _, _ -> },
|
||||
onResetFeatureFlagValues = { },
|
||||
onRestartOnboarding = { },
|
||||
onRestartOnboardingCarousel = { },
|
||||
onResetAccessibilityDisclaimer = { },
|
||||
onResetCoachMarkTourStatuses = { },
|
||||
onTriggerCookieAcquisition = { },
|
||||
onClearSsoCookies = { },
|
||||
onResetPremiumUpgradeBanner = { },
|
||||
onShowUpgradedToPremiumCard = { },
|
||||
onGenerateErrorReportClick = { },
|
||||
onGenerateCrashClick = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remember [DebugMenuHandler] with the given [viewModel] within a [Composable] scope.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberDebugMenuHandler(
|
||||
viewModel: DebugMenuViewModel,
|
||||
): DebugMenuHandler =
|
||||
remember(viewModel) {
|
||||
DebugMenuHandler.create(viewModel = viewModel)
|
||||
}
|
||||
+14
-5
@@ -54,6 +54,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on generate crash click should send GenerateCrashClick action`() {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS) }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Generate crash")
|
||||
.performScrollTo()
|
||||
@@ -64,6 +65,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `on generate error report click should send GenerateErrorReportClick action`() {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS) }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "Generate error report")
|
||||
.performScrollTo()
|
||||
@@ -74,7 +76,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `feature flag content should not display if the state is empty`() {
|
||||
mutableStateFlow.update { DebugMenuState(featureFlags = persistentMapOf()) }
|
||||
mutableStateFlow.update { it.copy(featureFlags = persistentMapOf()) }
|
||||
composeTestRule
|
||||
.onNodeWithText(text = "dummy-boolean")
|
||||
.assertDoesNotExist()
|
||||
@@ -83,7 +85,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
@Test
|
||||
fun `feature flag content should display if the state is not empty`() {
|
||||
mutableStateFlow.update {
|
||||
DebugMenuState(
|
||||
it.copy(
|
||||
featureFlags = persistentMapOf(
|
||||
FlagKey.DummyBoolean to true,
|
||||
),
|
||||
@@ -97,7 +99,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
@Test
|
||||
fun `boolean feature flag content should send action when clicked`() {
|
||||
mutableStateFlow.update {
|
||||
DebugMenuState(
|
||||
it.copy(
|
||||
featureFlags = persistentMapOf(
|
||||
FlagKey.DummyBoolean to true,
|
||||
),
|
||||
@@ -120,7 +122,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
@Test
|
||||
fun `reset feature flag values should send action when clicked`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("Reset Values", ignoreCase = true)
|
||||
.onNodeWithText("Reset values")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
@@ -129,6 +131,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `restart onboarding should send action when clicked`() {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Restart Onboarding", ignoreCase = true)
|
||||
.performScrollTo()
|
||||
@@ -140,8 +143,9 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `Show onboarding carousel should send action when enabled and clicked`() {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Show Onboarding Carousel", ignoreCase = true)
|
||||
.onNodeWithText("Show Onboarding Carousel")
|
||||
.performScrollTo()
|
||||
.assertIsEnabled()
|
||||
.performClick()
|
||||
@@ -151,6 +155,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `clear SSO cookies should send ClearSsoCookies action`() {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Clear SSO cookies")
|
||||
.performScrollTo()
|
||||
@@ -161,6 +166,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `reset Premium upgrade banner should send ResetPremiumUpgradeBanner action`() {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Reset Premium upgrade banner")
|
||||
.performScrollTo()
|
||||
@@ -173,6 +179,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `reset accessibility disclaimer should send ResetAccessibilityDisclaimer action`() {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Reset accessibility disclaimer")
|
||||
.performScrollTo()
|
||||
@@ -185,6 +192,7 @@ class DebugMenuScreenTest : BitwardenComposeTest() {
|
||||
|
||||
@Test
|
||||
fun `reset all coach mark tours should send ResetCoachMarkTourStatuses action`() {
|
||||
mutableStateFlow.update { it.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS) }
|
||||
composeTestRule
|
||||
.onNodeWithText("Reset all coach mark tours")
|
||||
.performScrollTo()
|
||||
@@ -198,4 +206,5 @@ private val DEFAULT_STATE: DebugMenuState = DebugMenuState(
|
||||
featureFlags = persistentMapOf(
|
||||
FlagKey.DummyBoolean to true,
|
||||
),
|
||||
mainTypeOption = DebugMenuState.MainTypeOption.FLAGS,
|
||||
)
|
||||
+18
-1
@@ -65,6 +65,19 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
||||
assertEquals(viewModel.stateFlow.value, DEFAULT_STATE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleMainTypeOptionClick should update the main option state`() {
|
||||
val viewModel = createViewModel()
|
||||
assertEquals(viewModel.stateFlow.value, DEFAULT_STATE)
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.MainTypeOptionClick(DebugMenuState.MainTypeOption.OPTIONS),
|
||||
)
|
||||
assertEquals(
|
||||
viewModel.stateFlow.value,
|
||||
DEFAULT_STATE.copy(mainTypeOption = DebugMenuState.MainTypeOption.OPTIONS),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleUpdateFeatureFlag should update the feature flag`() {
|
||||
val viewModel = createViewModel()
|
||||
@@ -72,7 +85,10 @@ class DebugMenuViewModelTest : BaseViewModelTest() {
|
||||
viewModel.trySendAction(
|
||||
DebugMenuAction.Internal.UpdateFeatureFlagMap(UPDATED_MAP_VALUE),
|
||||
)
|
||||
assertEquals(viewModel.stateFlow.value, DebugMenuState(UPDATED_MAP_VALUE))
|
||||
assertEquals(
|
||||
viewModel.stateFlow.value,
|
||||
DEFAULT_STATE.copy(featureFlags = UPDATED_MAP_VALUE),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -218,4 +234,5 @@ private val UPDATED_MAP_VALUE: ImmutableMap<FlagKey<Any>, Any> = FlagKey
|
||||
|
||||
private val DEFAULT_STATE = DebugMenuState(
|
||||
featureFlags = DEFAULT_MAP_VALUE,
|
||||
mainTypeOption = DebugMenuState.MainTypeOption.FLAGS,
|
||||
)
|
||||
+6
-1
@@ -7,12 +7,14 @@ import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.displayCutout
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.union
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
@@ -62,9 +64,12 @@ fun BitwardenSegmentedButton(
|
||||
Int,
|
||||
Dp,
|
||||
SegmentedButtonState,
|
||||
) -> Unit = { _, _, optionState ->
|
||||
) -> Unit = { _, weightedWidth, optionState ->
|
||||
this.SegmentedButtonOptionContent(
|
||||
option = optionState,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(width = weightedWidth),
|
||||
)
|
||||
},
|
||||
) {
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.bitwarden.ui.platform.components.segment.transition
|
||||
|
||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
|
||||
private const val TWEEN_DURATION_MS: Int = 300
|
||||
|
||||
/**
|
||||
* A standard [ContentTransform] to be used when animating to different content states of a
|
||||
* segmented control indicated by the [Enum].
|
||||
*/
|
||||
fun <T : Enum<T>> AnimatedContentTransitionScope<T>.segmentedContentTransform(): ContentTransform {
|
||||
// Slide in from right if moving forward, from left if moving backward
|
||||
return if (targetState.ordinal > initialState.ordinal) {
|
||||
(slideInHorizontally { width -> width } + fadeIn(tween(TWEEN_DURATION_MS))) togetherWith
|
||||
slideOutHorizontally { width -> -width } + fadeOut(tween(TWEEN_DURATION_MS))
|
||||
} else {
|
||||
(slideInHorizontally { width -> -width } + fadeIn(tween(TWEEN_DURATION_MS))) togetherWith
|
||||
slideOutHorizontally { width -> width } + fadeOut(tween(TWEEN_DURATION_MS))
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,12 @@
|
||||
<string name="shared_accounts_header" translatable="false">%1$s | %2$s (%3$d)</string>
|
||||
|
||||
<!-- region Debug Menu -->
|
||||
<string name="feature_flags">Feature Flags:</string>
|
||||
<string name="feature_flags">Feature Flags</string>
|
||||
<string name="no_feature_flags">There are currently no feature flags</string>
|
||||
<string name="debug_options">Debug Options</string>
|
||||
<string name="debug_menu">Debug Menu</string>
|
||||
<string name="reset_values">Reset values</string>
|
||||
<string name="onboarding_override">Onboarding Status Override</string>
|
||||
<string name="onboarding_override">Onboarding</string>
|
||||
<string name="restart_onboarding_cta">Restart Onboarding</string>
|
||||
<string name="restart_onboarding_details">This will reset the onboarding status for the current user, if available. After clicking the button you will immediately be redirected to the onboarding flow. Onboarding flag must be enabled.</string>
|
||||
<string name="restart_onboarding_carousel">Show Onboarding Carousel</string>
|
||||
|
||||
Reference in new issue
Block a user