Settings redesign (#1576)

* Settings redesign

Closes #1146

* Tests hotfix

* Changelogs update

* Code cleanup

* Add small padding above logo in Settings

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Milan 2024-09-12 09:03:38 +02:00 committed by GitHub
parent b75836f941
commit fa9ea0c03a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 1384 additions and 579 deletions

View File

@ -13,6 +13,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
### Changed
- Choose server screen has been redesigned
- Settings and Advanced Settings screens have been redesigned
## [1.1.7 (718)] - 2024-09-06

View File

@ -16,6 +16,7 @@ directly impact users rather than highlighting other key architectural updates.*
### Changed
- Choose server screen has been redesigned
- Settings and Advanced Settings screens have been redesigned
## [1.1.7 (718)] - 2024-09-06

View File

@ -33,9 +33,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -351,7 +354,7 @@ private fun TopBarOneVisibleActionMenuExample(
@Composable
fun TopAppBarBackNavigation(
backContentDescriptionText: String? = null,
backIconVector: ImageVector = Icons.AutoMirrored.Filled.ArrowBack,
painter: Painter = rememberVectorPainter(Icons.AutoMirrored.Filled.ArrowBack),
backText: String? = null,
onBack: () -> Unit
) {
@ -365,7 +368,7 @@ fun TopAppBarBackNavigation(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = backIconVector,
painter = painter,
contentDescription = backContentDescriptionText,
)
@ -440,6 +443,7 @@ fun SmallTopAppBar(
subTitle: String? = null,
showTitleLogo: Boolean = false,
titleText: String? = null,
titleStyle: TextStyle = SecondaryTypography.headlineSmall,
) {
CenterAlignedTopAppBar(
title = {
@ -451,7 +455,7 @@ fun SmallTopAppBar(
if (titleText != null) {
Text(
text = titleText.uppercase(),
style = SecondaryTypography.headlineSmall,
style = titleStyle,
color = colors.titleColor,
)
restoringSpacerHeight = ZcashTheme.dimens.spacingTiny

View File

@ -0,0 +1,104 @@
package co.electriccoin.zcash.ui.design.component
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.orDark
@Composable
fun ZashiSettingsListItem(
text: String,
@DrawableRes icon: Int,
trailing: @Composable () -> Unit = {
Image(
painter = painterResource(R.drawable.ic_chevron_right orDark R.drawable.ic_chevron_right_dark),
contentDescription = text,
)
},
onClick: () -> Unit
) {
ZashiSettingsListItem(
leading = {
Image(
modifier = Modifier.size(40.dp),
painter = painterResource(icon),
contentDescription = text
)
},
content = {
Text(
text = text,
style = ZcashTheme.typography.primary.titleSmall.copy(fontWeight = FontWeight.SemiBold),
fontSize = 16.sp
)
},
trailing = trailing,
onClick = onClick
)
}
@Composable
fun ZashiSettingsListItem(
leading: @Composable () -> Unit,
content: @Composable () -> Unit,
trailing: @Composable () -> Unit,
onClick: () -> Unit
) {
Row(
modifier =
Modifier
.clip(RoundedCornerShape(12.dp))
.clickable(
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() },
onClick = onClick,
role = Role.Button,
)
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.width(20.dp))
leading()
Spacer(modifier = Modifier.width(16.dp))
content()
Spacer(modifier = Modifier.weight(1f))
trailing()
Spacer(modifier = Modifier.width(20.dp))
}
}
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun ZashiSettingsListItemPreview() =
ZcashTheme {
BlankSurface {
ZashiSettingsListItem(
text = "Test",
icon = R.drawable.ic_radio_button_checked,
onClick = {}
)
}
}

View File

@ -0,0 +1,34 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.RowScope
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.internal.SecondaryTypography
import co.electriccoin.zcash.ui.design.theme.internal.TopAppBarColors
@Composable
@Suppress("LongParameterList")
fun ZashiSmallTopAppBar(
title: String,
subtitle: String?,
modifier: Modifier = Modifier,
showTitleLogo: Boolean = false,
colors: TopAppBarColors = ZcashTheme.colors.topAppBarColors,
navigationAction: @Composable () -> Unit = {},
hamburgerMenuActions: (@Composable RowScope.() -> Unit)? = null,
regularActions: (@Composable RowScope.() -> Unit)? = null,
) {
SmallTopAppBar(
modifier = modifier,
colors = colors,
hamburgerMenuActions = hamburgerMenuActions,
navigationAction = navigationAction,
regularActions = regularActions,
subTitle = subtitle,
showTitleLogo = showTitleLogo,
titleText = title,
titleStyle = SecondaryTypography.headlineSmall.copy(fontWeight = FontWeight.SemiBold)
)
}

View File

@ -0,0 +1,35 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.util.orDark
@Composable
fun ZashiTopAppBarBackNavigation(
backContentDescriptionText: String = stringResource(R.string.back_navigation_content_description),
painter: Painter =
painterResource(
R.drawable.ic_zashi_navigation_back orDark R.drawable.ic_zashi_navigation_back_dark
),
onBack: () -> Unit
) {
Row {
Spacer(modifier = Modifier.width(16.dp))
IconButton(onClick = onBack) {
Icon(
painter = painter,
contentDescription = backContentDescriptionText,
)
}
}
}

View File

@ -2,8 +2,12 @@ package co.electriccoin.zcash.ui.design.newcomponent
import android.content.res.Configuration
import androidx.compose.ui.tooling.preview.Preview
import kotlin.annotation.AnnotationRetention.SOURCE
// TODO [#1580]: Suppress compilation warning on PreviewScreens
// https://github.com/Electric-Coin-Company/zashi-android/issues/1580
@Suppress("UnusedPrivateMember")
@Preview(name = "1: Light preview", showBackground = true)
@Preview(name = "2: Dark preview", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Retention(SOURCE)
annotation class PreviewScreens

View File

@ -36,6 +36,9 @@ data class ZashiColors(
val utilitySuccess700: Color,
val utilitySuccess200: Color,
val utilitySuccess50: Color,
val btnDestroyFg: Color,
val btnDestroyBg: Color,
val btnDestroyBorder: Color,
)
internal val LightZashiColorPalette =
@ -63,6 +66,9 @@ internal val LightZashiColorPalette =
utilitySuccess700 = Color(0xFF098605),
utilitySuccess200 = Color(0xFFA3FF95),
utilitySuccess50 = Color(0xFFEAFFE5),
btnDestroyFg = Color(0xFFD92D20),
btnDestroyBg = Color(0xFFFFFFFF),
btnDestroyBorder = Color(0xFFFDA29B)
)
internal val DarkZashiColorPalette =
@ -90,6 +96,9 @@ internal val DarkZashiColorPalette =
utilitySuccess700 = Color(0xFF098605),
utilitySuccess200 = Color(0xFFA3FF95),
utilitySuccess50 = Color(0xFFEAFFE5),
btnDestroyFg = Color(0xFFFEE4E2),
btnDestroyBg = Color(0xFF55160C),
btnDestroyBorder = Color(0xFF912018)
)
@Suppress("CompositionLocalAllowlist")

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.design.util
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
@Composable
@ReadOnlyComposable
infix fun <T> T.orDark(dark: T): T = if (isSystemInDarkTheme()) dark else this

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M7.5,15L12.5,10L7.5,5"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#87816F"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M7.5,15L12.5,10L7.5,5"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#A7A5A6"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="14dp"
android:viewportWidth="18"
android:viewportHeight="14">
<path
android:pathData="M17,7H1M1,7L7,13M1,7L7,1"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="14dp"
android:viewportWidth="18"
android:viewportHeight="14">
<path
android:pathData="M17,7H1M1,7L7,13M1,7L7,1"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,4 +1,5 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="amount_with_fiat_currency_symbol" formatted="true"><xliff:g id="fiat_currency_symbol" example="$">%1$s</xliff:g><xliff:g id="amount" example="123">%2$s</xliff:g></string>
<string name="hide_balance_placeholder">-----</string>
<string name="back_navigation_content_description">Back</string>
</resources>

View File

@ -3,13 +3,20 @@ package co.electriccoin.zcash.ui.screen.settings
import androidx.compose.ui.test.junit4.ComposeContentTestRule
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.settings.model.SettingsState
import co.electriccoin.zcash.ui.screen.settings.model.SettingsTroubleshootingState
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingItemState
import co.electriccoin.zcash.ui.screen.settings.view.Settings
import java.util.concurrent.atomic.AtomicInteger
class SettingsViewTestSetup(
private val composeTestRule: ComposeContentTestRule,
private val troubleshootingParameters: TroubleshootingParameters
isTroubleshootingEnabled: Boolean = false,
isBackgroundSyncEnabled: Boolean = false,
isKeepScreenOnDuringSyncEnabled: Boolean = false,
isAnalyticsEnabled: Boolean = false,
isRescanEnabled: Boolean = false
) {
private val onBackCount = AtomicInteger(0)
private val onFeedbackCount = AtomicInteger(0)
@ -20,6 +27,30 @@ class SettingsViewTestSetup(
private val onKeepScreenOnChangedCount = AtomicInteger(0)
private val onAnalyticsChangedCount = AtomicInteger(0)
private val settingsTroubleshootingState =
if (isTroubleshootingEnabled) {
SettingsTroubleshootingState(
rescan =
TroubleshootingItemState(isRescanEnabled) {
onRescanCount.incrementAndGet()
},
backgroundSync =
TroubleshootingItemState(isBackgroundSyncEnabled) {
onBackgroundSyncChangedCount.incrementAndGet()
},
keepScreenOnDuringSync =
TroubleshootingItemState(isKeepScreenOnDuringSyncEnabled) {
onKeepScreenOnChangedCount.incrementAndGet()
},
analytics =
TroubleshootingItemState(isAnalyticsEnabled) {
onAnalyticsChangedCount.incrementAndGet()
}
)
} else {
null
}
fun getBackCount(): Int {
composeTestRule.waitForIdle()
return onBackCount.get()
@ -64,31 +95,24 @@ class SettingsViewTestSetup(
composeTestRule.setContent {
ZcashTheme {
Settings(
troubleshootingParameters = troubleshootingParameters,
onBack = {
onBackCount.incrementAndGet()
},
onFeedback = {
onFeedbackCount.incrementAndGet()
},
onAdvancedSettings = {
onAdvancedSettingsCount.incrementAndGet()
},
onAbout = {
onAboutCount.incrementAndGet()
},
onRescanWallet = {
onRescanCount.incrementAndGet()
},
onBackgroundSyncSettingsChanged = {
onBackgroundSyncChangedCount.incrementAndGet()
},
onKeepScreenOnDuringSyncSettingsChanged = {
onKeepScreenOnChangedCount.incrementAndGet()
},
onAnalyticsSettingsChanged = {
onAnalyticsChangedCount.incrementAndGet()
},
state =
SettingsState(
isLoading = false,
version = stringRes("app_version"),
settingsTroubleshootingState = settingsTroubleshootingState,
onBack = {
onBackCount.incrementAndGet()
},
onSendUsFeedbackClick = {
onFeedbackCount.incrementAndGet()
},
onAdvancedSettingsClick = {
onAdvancedSettingsCount.incrementAndGet()
},
onAboutUsClick = {
onAboutCount.incrementAndGet()
},
),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}

View File

@ -1,25 +0,0 @@
package co.electriccoin.zcash.ui.screen.settings.fixture
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
internal object TroubleshootingParametersFixture {
internal const val ENABLED = false
internal const val BACKGROUND_SYNC_ENABLED = false
internal const val KEEP_SCREEN_ON_DURING_SYNC_ENABLED = false
internal const val ANALYTICS_ENABLED = false
internal const val RESCAN_ENABLED = false
fun new(
isEnabled: Boolean = ENABLED,
isBackgroundSyncEnabled: Boolean = BACKGROUND_SYNC_ENABLED,
isKeepScreenOnDuringSyncEnabled: Boolean = KEEP_SCREEN_ON_DURING_SYNC_ENABLED,
isAnalyticsEnabled: Boolean = ANALYTICS_ENABLED,
isRescanEnabled: Boolean = RESCAN_ENABLED,
) = TroubleshootingParameters(
isEnabled,
isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled,
isAnalyticsEnabled,
isRescanEnabled
)
}

View File

@ -13,7 +13,6 @@ import co.electriccoin.zcash.test.UiTestPrerequisites
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.screen.settings.SettingsTag
import co.electriccoin.zcash.ui.screen.settings.SettingsViewTestSetup
import co.electriccoin.zcash.ui.screen.settings.fixture.TroubleshootingParametersFixture
import co.electriccoin.zcash.ui.test.getStringResource
import org.junit.Assert.assertEquals
import org.junit.Rule
@ -26,7 +25,7 @@ class SettingsViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun on_back_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
val testSetup = SettingsViewTestSetup(composeTestRule, isTroubleshootingEnabled = false)
assertEquals(0, testSetup.getBackCount())
@ -42,12 +41,12 @@ class SettingsViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun on_feedback_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
val testSetup = SettingsViewTestSetup(composeTestRule, isTroubleshootingEnabled = false)
assertEquals(0, testSetup.getFeedbackCount())
composeTestRule.onNodeWithText(
text = getStringResource(R.string.settings_send_us_feedback),
text = getStringResource(R.string.settings_feedback),
ignoreCase = true
).also {
it.performClick()
@ -59,7 +58,7 @@ class SettingsViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun on_advanced_settings_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
val testSetup = SettingsViewTestSetup(composeTestRule, isTroubleshootingEnabled = false)
assertEquals(0, testSetup.getAdvancedSettingsCount())
@ -76,12 +75,12 @@ class SettingsViewTest : UiTestPrerequisites() {
@Test
@MediumTest
fun on_about_test() {
val testSetup = SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new())
val testSetup = SettingsViewTestSetup(composeTestRule, isTroubleshootingEnabled = false)
assertEquals(0, testSetup.getAboutCount())
composeTestRule.onNodeWithText(
text = getStringResource(R.string.settings_about),
text = getStringResource(R.string.settings_about_us),
ignoreCase = true
).also {
it.performScrollTo()
@ -94,7 +93,7 @@ class SettingsViewTest : UiTestPrerequisites() {
@Test
@SmallTest
fun troubleshooting_menu_visible_test() {
SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new(isEnabled = true))
SettingsViewTestSetup(composeTestRule, isTroubleshootingEnabled = true)
composeTestRule.onNodeWithTag(SettingsTag.TROUBLESHOOTING_MENU).also {
it.assertExists()
@ -104,7 +103,7 @@ class SettingsViewTest : UiTestPrerequisites() {
@Test
@SmallTest
fun troubleshooting_menu_not_visible_test() {
SettingsViewTestSetup(composeTestRule, TroubleshootingParametersFixture.new(isEnabled = false))
SettingsViewTestSetup(composeTestRule, isTroubleshootingEnabled = false)
composeTestRule.onNodeWithTag(SettingsTag.TROUBLESHOOTING_MENU).also {
it.assertDoesNotExist()
@ -116,11 +115,9 @@ class SettingsViewTest : UiTestPrerequisites() {
fun troubleshooting_rescan_test() {
val testSetup =
SettingsViewTestSetup(
composeTestRule,
TroubleshootingParametersFixture.new(
isEnabled = true,
isRescanEnabled = true
)
composeTestRule = composeTestRule,
isTroubleshootingEnabled = true,
isRescanEnabled = true
)
assertEquals(0, testSetup.getRescanCount())
@ -140,10 +137,8 @@ class SettingsViewTest : UiTestPrerequisites() {
val testSetup =
SettingsViewTestSetup(
composeTestRule,
TroubleshootingParametersFixture.new(
isEnabled = true,
isBackgroundSyncEnabled = true
)
isTroubleshootingEnabled = true,
isBackgroundSyncEnabled = true
)
assertEquals(0, testSetup.getBackgroundSyncCount())
@ -165,10 +160,8 @@ class SettingsViewTest : UiTestPrerequisites() {
val testSetup =
SettingsViewTestSetup(
composeTestRule,
TroubleshootingParametersFixture.new(
isEnabled = true,
isKeepScreenOnDuringSyncEnabled = true
)
isTroubleshootingEnabled = true,
isKeepScreenOnDuringSyncEnabled = true
)
assertEquals(0, testSetup.getKeepScreenOnSyncCount())
@ -190,10 +183,8 @@ class SettingsViewTest : UiTestPrerequisites() {
val testSetup =
SettingsViewTestSetup(
composeTestRule,
TroubleshootingParametersFixture.new(
isEnabled = true,
isAnalyticsEnabled = true
)
isTroubleshootingEnabled = true,
isAnalyticsEnabled = true
)
assertEquals(0, testSetup.getAnalyticsCount())

View File

@ -1,10 +1,12 @@
package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.provider.GetDefaultServersProvider
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module
val providerModule =
module {
factoryOf(::GetDefaultServersProvider)
factoryOf(::GetVersionInfoProvider)
}

View File

@ -1,5 +1,7 @@
package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepositoryImpl
import co.electriccoin.zcash.ui.common.repository.WalletRepository
import co.electriccoin.zcash.ui.common.repository.WalletRepositoryImpl
import org.koin.core.module.dsl.singleOf
@ -9,4 +11,5 @@ import org.koin.dsl.module
val repositoryModule =
module {
singleOf(::WalletRepositoryImpl) bind WalletRepository::class
singleOf(::ConfigurationRepositoryImpl) bind ConfigurationRepository::class
}

View File

@ -3,11 +3,13 @@ package co.electriccoin.zcash.di
import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase
import co.electriccoin.zcash.ui.common.usecase.PersistEndpointUseCase
import co.electriccoin.zcash.ui.common.usecase.RefreshFastestServersUseCase
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@ -23,4 +25,6 @@ val useCaseModule =
singleOf(::ValidateEndpointUseCase)
singleOf(::GetPersistableWalletUseCase)
singleOf(::GetSelectedEndpointUseCase)
singleOf(::ObserveConfigurationUseCase)
singleOf(::RescanBlockchainUseCase)
}

View File

@ -5,6 +5,7 @@ import co.electriccoin.zcash.ui.common.viewmodel.CheckUpdateViewModel
import co.electriccoin.zcash.ui.common.viewmodel.HomeViewModel
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.account.viewmodel.TransactionHistoryViewModel
import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettingsViewModel
import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel
import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel
import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel
@ -31,6 +32,7 @@ val viewModelModule =
viewModelOf(::RestoreViewModel)
viewModelOf(::ScreenBrightnessViewModel)
viewModelOf(::SettingsViewModel)
viewModelOf(::AdvancedSettingsViewModel)
viewModelOf(::SupportViewModel)
viewModelOf(::CreateTransactionsViewModel)
viewModelOf(::RestoreSuccessViewModel)

View File

@ -115,26 +115,10 @@ internal fun MainActivity.Navigation() {
NavigationHome(navController, backStack)
}
composable(SETTINGS) {
WrapSettings(
goAbout = {
navController.navigateJustOnce(ABOUT)
},
goAdvancedSettings = {
navController.navigateJustOnce(ADVANCED_SETTINGS)
},
goBack = {
navController.popBackStackJustOnce(SETTINGS)
},
goFeedback = {
navController.navigateJustOnce(SUPPORT)
},
)
WrapSettings()
}
composable(ADVANCED_SETTINGS) {
WrapAdvancedSettings(
goBack = {
navController.popBackStackJustOnce(ADVANCED_SETTINGS)
},
goExportPrivateData = {
navController.checkProtectedDestination(
scope = lifecycleScope,
@ -151,9 +135,6 @@ internal fun MainActivity.Navigation() {
unProtectedDestination = SEED_RECOVERY
)
},
goChooseServer = {
navController.navigateJustOnce(CHOOSE_SERVER)
},
goDeleteWallet = {
navController.checkProtectedDestination(
scope = lifecycleScope,
@ -162,9 +143,6 @@ internal fun MainActivity.Navigation() {
unProtectedDestination = DELETE_WALLET
)
},
onCurrencyConversion = {
navController.navigateJustOnce(SETTINGS_EXCHANGE_RATE_OPT_IN)
}
)
when {

View File

@ -0,0 +1,8 @@
package co.electriccoin.zcash.ui.common.provider
import android.app.Application
import co.electriccoin.zcash.ui.common.model.VersionInfo
class GetVersionInfoProvider(private val application: Application) {
operator fun invoke() = VersionInfo.new(application)
}

View File

@ -0,0 +1,27 @@
package co.electriccoin.zcash.ui.common.repository
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import co.electriccoin.zcash.configuration.model.map.Configuration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
interface ConfigurationRepository {
val configurationFlow: StateFlow<Configuration?>
}
class ConfigurationRepositoryImpl(androidConfigurationProvider: ConfigurationProvider) : ConfigurationRepository {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
override val configurationFlow: StateFlow<Configuration?> =
androidConfigurationProvider.getConfigurationFlow()
.stateIn(
scope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT.inWholeMilliseconds),
null
)
}

View File

@ -0,0 +1,9 @@
package co.electriccoin.zcash.ui.common.usecase
import co.electriccoin.zcash.ui.common.repository.ConfigurationRepository
class ObserveConfigurationUseCase(
private val configurationRepository: ConfigurationRepository
) {
operator fun invoke() = configurationRepository.configurationFlow
}

View File

@ -0,0 +1,27 @@
package co.electriccoin.zcash.ui.common.usecase
import cash.z.ecc.android.sdk.WalletCoordinator
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.ui.common.model.WalletRestoringState
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
class RescanBlockchainUseCase(
private val walletCoordinator: WalletCoordinator,
private val standardPreferenceProvider: StandardPreferenceProvider
) {
suspend operator fun invoke() =
withContext(Dispatchers.IO + NonCancellable) {
walletCoordinator.rescanBlockchain()
persistWalletRestoringState(WalletRestoringState.RESTORING)
}
private suspend fun persistWalletRestoringState(walletRestoringState: WalletRestoringState) {
StandardPreferenceKeys.WALLET_RESTORING_STATE.putValue(
standardPreferenceProvider(),
walletRestoringState.toNumber()
)
}
}

View File

@ -3,10 +3,10 @@ package co.electriccoin.zcash.ui.common.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.configuration.api.ConfigurationProvider
import co.electriccoin.zcash.configuration.model.map.Configuration
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class HomeViewModel(
androidConfigurationProvider: ConfigurationProvider,
private val observeConfiguration: ObserveConfigurationUseCase,
private val standardPreferenceProvider: StandardPreferenceProvider,
) : ViewModel() {
/**
@ -55,13 +55,7 @@ class HomeViewModel(
}
}
val configurationFlow: StateFlow<Configuration?> =
androidConfigurationProvider.getConfigurationFlow()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT.inWholeMilliseconds),
null
)
val configurationFlow: StateFlow<Configuration?> = observeConfiguration()
//
// PRIVATE HELPERS

View File

@ -0,0 +1,10 @@
package co.electriccoin.zcash.ui.screen.advancedsettings
data class AdvancedSettingsState(
val onBack: () -> Unit,
val onRecoveryPhraseClick: () -> Unit,
val onExportPrivateDataClick: () -> Unit,
val onChooseServerClick: () -> Unit,
val onCurrencyConversionClick: () -> Unit,
val onDeleteZashiClick: () -> Unit,
)

View File

@ -4,60 +4,51 @@ package co.electriccoin.zcash.ui.screen.advancedsettings
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.MainActivity
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.screen.advancedsettings.view.AdvancedSettings
import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettingsViewModel
import org.koin.androidx.compose.koinViewModel
@Suppress("LongParameterList")
@Composable
internal fun MainActivity.WrapAdvancedSettings(
goBack: () -> Unit,
internal fun WrapAdvancedSettings(
goDeleteWallet: () -> Unit,
goExportPrivateData: () -> Unit,
goChooseServer: () -> Unit,
goSeedRecovery: () -> Unit,
onCurrencyConversion: () -> Unit
) {
val navController = LocalNavController.current
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val viewModel = koinViewModel<AdvancedSettingsViewModel>()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
val state =
viewModel.state.collectAsStateWithLifecycle().value.copy(
onDeleteZashiClick = goDeleteWallet,
onExportPrivateDataClick = goExportPrivateData,
onRecoveryPhraseClick = goSeedRecovery
)
WrapAdvancedSettings(
goBack = goBack,
goDeleteWallet = goDeleteWallet,
goExportPrivateData = goExportPrivateData,
goChooseServer = goChooseServer,
goSeedRecovery = goSeedRecovery,
topAppBarSubTitleState = walletState,
onCurrencyConversion = onCurrencyConversion
)
}
@Composable
@Suppress("LongParameterList")
private fun WrapAdvancedSettings(
goBack: () -> Unit,
goExportPrivateData: () -> Unit,
goChooseServer: () -> Unit,
goSeedRecovery: () -> Unit,
goDeleteWallet: () -> Unit,
onCurrencyConversion: () -> Unit,
topAppBarSubTitleState: TopAppBarSubTitleState,
) {
BackHandler {
goBack()
viewModel.onBack()
}
LaunchedEffect(Unit) {
viewModel.navigationCommand.collect {
navController.navigate(it)
}
}
LaunchedEffect(Unit) {
viewModel.backNavigationCommand.collect {
navController.popBackStack()
}
}
AdvancedSettings(
onBack = goBack,
onDeleteWallet = goDeleteWallet,
onExportPrivateData = goExportPrivateData,
onChooseServer = goChooseServer,
onSeedRecovery = goSeedRecovery,
topAppBarSubTitleState = topAppBarSubTitleState,
onCurrencyConversion = onCurrencyConversion
state = state,
topAppBarSubTitleState = walletState,
)
}

View File

@ -1,89 +1,128 @@
package co.electriccoin.zcash.ui.screen.advancedsettings.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
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.compose.ui.unit.sp
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
import co.electriccoin.zcash.ui.design.util.orDark
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsTag
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButton
import co.electriccoin.zcash.ui.screen.exchangerate.ZashiButtonDefaults
// TODO [#1271]: Add AdvancedSettingsView Tests
// TODO [#1271]: https://github.com/Electric-Coin-Company/zashi-android/issues/1271
@Preview("Advanced Settings")
@Suppress("LongMethod")
@Composable
private fun PreviewAdvancedSettings() {
ZcashTheme(forceDarkMode = false) {
AdvancedSettings(
onBack = {},
onDeleteWallet = {},
onExportPrivateData = {},
onChooseServer = {},
onSeedRecovery = {},
topAppBarSubTitleState = TopAppBarSubTitleState.None,
onCurrencyConversion = {}
)
}
}
@Composable
@Suppress("LongParameterList")
fun AdvancedSettings(
onBack: () -> Unit,
onDeleteWallet: () -> Unit,
onExportPrivateData: () -> Unit,
onChooseServer: () -> Unit,
onSeedRecovery: () -> Unit,
onCurrencyConversion: () -> Unit,
state: AdvancedSettingsState,
topAppBarSubTitleState: TopAppBarSubTitleState,
) {
BlankBgScaffold(
topBar = {
AdvancedSettingsTopAppBar(
onBack = onBack,
onBack = state.onBack,
subTitleState = topAppBarSubTitleState,
)
}
) { paddingValues ->
AdvancedSettingsMainContent(
Column(
modifier =
Modifier
.verticalScroll(
rememberScrollState()
)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(
top = paddingValues.calculateTopPadding() + dimens.spacingHuge,
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding(),
start = dimens.screenHorizontalSpacingBig,
end = dimens.screenHorizontalSpacingBig
start = 4.dp,
end = 4.dp
),
onDeleteWallet = onDeleteWallet,
onExportPrivateData = onExportPrivateData,
onSeedRecovery = onSeedRecovery,
onChooseServer = onChooseServer,
onCurrencyConversion = onCurrencyConversion
)
) {
ZashiSettingsListItem(
text = stringResource(id = R.string.advanced_settings_recovery),
icon = R.drawable.ic_advanced_settings_recovery orDark R.drawable.ic_advanced_settings_recovery_dark,
onClick = state.onRecoveryPhraseClick
)
HorizontalDivider(color = ZcashTheme.zashiColors.divider)
ZashiSettingsListItem(
text = stringResource(id = R.string.advanced_settings_export),
icon = R.drawable.ic_advanced_settings_export orDark R.drawable.ic_advanced_settings_export_dark,
onClick = state.onExportPrivateDataClick
)
HorizontalDivider(color = ZcashTheme.zashiColors.divider)
ZashiSettingsListItem(
text = stringResource(id = R.string.advanced_settings_choose_server),
icon =
R.drawable.ic_advanced_settings_choose_server orDark
R.drawable.ic_advanced_settings_choose_server_dark,
onClick = state.onChooseServerClick
)
HorizontalDivider(color = ZcashTheme.zashiColors.divider)
ZashiSettingsListItem(
text = stringResource(id = R.string.advanced_settings_currency_conversion),
icon =
R.drawable.ic_advanced_settings_currency_conversion orDark
R.drawable.ic_advanced_settings_currency_conversion_dark,
onClick = state.onCurrencyConversionClick
)
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.weight(1f))
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_advanced_settings_info),
contentDescription = "",
colorFilter = ColorFilter.tint(ZcashTheme.zashiColors.textTertiary)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(id = R.string.advanced_settings_info),
fontSize = 12.sp,
color = ZcashTheme.zashiColors.textTertiary,
)
}
Spacer(modifier = Modifier.height(20.dp))
ZashiButton(
modifier =
Modifier
.padding(horizontal = 20.dp)
.fillMaxWidth(),
text = stringResource(R.string.advanced_settings_delete_button),
colors = ZashiButtonDefaults.destroyButtonColors(),
onClick = state.onDeleteZashiClick
)
Spacer(modifier = Modifier.height(20.dp))
}
}
}
@ -92,8 +131,9 @@ private fun AdvancedSettingsTopAppBar(
onBack: () -> Unit,
subTitleState: TopAppBarSubTitleState
) {
SmallTopAppBar(
subTitle =
ZashiSmallTopAppBar(
title = stringResource(id = R.string.advanced_settings_title),
subtitle =
when (subTitleState) {
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
@ -102,88 +142,26 @@ private fun AdvancedSettingsTopAppBar(
modifier = Modifier.testTag(AdvancedSettingsTag.ADVANCED_SETTINGS_TOP_APP_BAR),
showTitleLogo = true,
navigationAction = {
TopAppBarBackNavigation(
backText = stringResource(id = R.string.back_navigation).uppercase(),
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
onBack = onBack
)
}
ZashiTopAppBarBackNavigation(onBack = onBack)
},
)
}
@Suppress("LongParameterList")
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun AdvancedSettingsMainContent(
onDeleteWallet: () -> Unit,
onExportPrivateData: () -> Unit,
onChooseServer: () -> Unit,
onCurrencyConversion: () -> Unit,
onSeedRecovery: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
Modifier
.fillMaxSize()
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
PrimaryButton(
onClick = onSeedRecovery,
text = stringResource(R.string.advanced_settings_backup_wallet),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onExportPrivateData,
text = stringResource(R.string.advanced_settings_export_private_data),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onChooseServer,
text = stringResource(R.string.advanced_settings_choose_server),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onCurrencyConversion,
text = stringResource(R.string.advanced_settings_currency_conversion),
modifier = Modifier.fillMaxWidth()
)
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onDeleteWallet,
text =
stringResource(
R.string.advanced_settings_delete_wallet,
stringResource(id = R.string.app_name)
private fun AdvancedSettingsPreview() =
ZcashTheme {
AdvancedSettings(
state =
AdvancedSettingsState(
onBack = {},
onRecoveryPhraseClick = {},
onExportPrivateDataClick = {},
onChooseServerClick = {},
onCurrencyConversionClick = {},
onDeleteZashiClick = {},
),
modifier = Modifier.fillMaxWidth()
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
Text(
text = stringResource(id = R.string.advanced_settings_delete_wallet_footnote),
style = ZcashTheme.extendedTypography.footnote,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(dimens.spacingHuge))
}
}

View File

@ -0,0 +1,43 @@
package co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.electriccoin.zcash.ui.NavigationTargets
import co.electriccoin.zcash.ui.screen.advancedsettings.AdvancedSettingsState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class AdvancedSettingsViewModel : ViewModel() {
val state: StateFlow<AdvancedSettingsState> =
MutableStateFlow(
AdvancedSettingsState(
onBack = ::onBack,
onRecoveryPhraseClick = {},
onExportPrivateDataClick = {},
onChooseServerClick = ::onChooseServerClick,
onCurrencyConversionClick = ::onCurrencyConversionClick,
onDeleteZashiClick = {}
)
).asStateFlow()
val navigationCommand = MutableSharedFlow<String>()
val backNavigationCommand = MutableSharedFlow<Unit>()
private fun onChooseServerClick() =
viewModelScope.launch {
navigationCommand.emit(NavigationTargets.CHOOSE_SERVER)
}
private fun onCurrencyConversionClick() =
viewModelScope.launch {
navigationCommand.emit(NavigationTargets.SETTINGS_EXCHANGE_RATE_OPT_IN)
}
fun onBack() =
viewModelScope.launch {
backNavigationCommand.emit(Unit)
}
}

View File

@ -1,21 +1,25 @@
package co.electriccoin.zcash.ui.screen.exchangerate
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.design.R
import co.electriccoin.zcash.ui.design.component.BlankSurface
import co.electriccoin.zcash.ui.design.component.ButtonState
import co.electriccoin.zcash.ui.design.component.LottieProgress
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.util.getValue
@ -23,7 +27,7 @@ import co.electriccoin.zcash.ui.design.util.getValue
internal fun ZashiButton(
state: ButtonState,
modifier: Modifier = Modifier,
colors: ButtonColors = ZashiButtonDefaults.primaryButtonColors(),
colors: ZashiButtonColors = ZashiButtonDefaults.primaryButtonColors(),
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
) {
ZashiButton(
@ -45,7 +49,7 @@ internal fun ZashiButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
isLoading: Boolean = false,
colors: ButtonColors = ZashiButtonDefaults.primaryButtonColors(),
colors: ZashiButtonColors = ZashiButtonDefaults.primaryButtonColors(),
content: @Composable RowScope.(ZashiButtonScope) -> Unit = ZashiButtonDefaults.content
) {
val scope =
@ -75,7 +79,8 @@ internal fun ZashiButton(
modifier = modifier,
shape = RoundedCornerShape(12.dp),
enabled = enabled,
colors = colors,
colors = colors.toButtonColors(),
border = colors.borderColor.takeIf { it != Color.Unspecified }?.let { BorderStroke(1.dp, it) },
content = {
content(scope)
}
@ -104,13 +109,13 @@ object ZashiButtonDefaults {
contentColor: Color = ZcashTheme.zashiColors.btnPrimaryFg,
disabledContainerColor: Color = ZcashTheme.zashiColors.btnPrimaryBgDisabled,
disabledContentColor: Color = ZcashTheme.zashiColors.btnPrimaryFgDisabled,
): ButtonColors =
ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor
)
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
borderColor = Color.Unspecified
)
@Composable
fun tertiaryButtonColors(
@ -118,11 +123,88 @@ object ZashiButtonDefaults {
contentColor: Color = ZcashTheme.zashiColors.btnTertiaryFg,
disabledContainerColor: Color = Color.Unspecified,
disabledContentColor: Color = Color.Unspecified,
): ButtonColors =
ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor
)
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
borderColor = Color.Unspecified
)
@Composable
fun destroyButtonColors(
containerColor: Color = ZcashTheme.zashiColors.btnDestroyBg,
contentColor: Color = ZcashTheme.zashiColors.btnDestroyFg,
borderColor: Color = ZcashTheme.zashiColors.btnDestroyBorder,
disabledContainerColor: Color = Color.Unspecified,
disabledContentColor: Color = Color.Unspecified,
) = ZashiButtonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
borderColor = borderColor
)
}
@Immutable
data class ZashiButtonColors(
val containerColor: Color,
val contentColor: Color,
val disabledContainerColor: Color,
val disabledContentColor: Color,
val borderColor: Color,
)
@Composable
private fun ZashiButtonColors.toButtonColors() =
ButtonDefaults.buttonColors(
containerColor = containerColor,
contentColor = contentColor,
disabledContainerColor = disabledContainerColor,
disabledContentColor = disabledContentColor,
)
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun PrimaryPreview() =
ZcashTheme {
BlankSurface {
ZashiButton(
modifier = Modifier.fillMaxWidth(),
text = "Primary",
onClick = {},
)
}
}
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun TertiaryPreview() =
ZcashTheme {
BlankSurface {
ZashiButton(
modifier = Modifier.fillMaxWidth(),
text = "Primary",
colors = ZashiButtonDefaults.tertiaryButtonColors(),
onClick = {},
)
}
}
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun DestroyPreview() =
ZcashTheme {
BlankSurface {
ZashiButton(
modifier = Modifier.fillMaxWidth(),
text = "Primary",
colors = ZashiButtonDefaults.destroyButtonColors(),
onClick = {},
)
}
}

View File

@ -2,77 +2,43 @@ package co.electriccoin.zcash.ui.screen.settings
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.electriccoin.zcash.di.koinActivityViewModel
import co.electriccoin.zcash.ui.common.compose.LocalActivity
import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.compose.LocalNavController
import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.configuration.RemoteConfig
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
import co.electriccoin.zcash.ui.screen.settings.view.Settings
import co.electriccoin.zcash.ui.screen.settings.viewmodel.SettingsViewModel
import org.koin.androidx.compose.koinViewModel
@Composable
internal fun WrapSettings(
goAbout: () -> Unit,
goAdvancedSettings: () -> Unit,
goBack: () -> Unit,
goFeedback: () -> Unit,
) {
val activity = LocalActivity.current
internal fun WrapSettings() {
val navController = LocalNavController.current
val walletViewModel = koinActivityViewModel<WalletViewModel>()
val settingsViewModel = koinActivityViewModel<SettingsViewModel>()
val settingsViewModel = koinViewModel<SettingsViewModel>()
val state by settingsViewModel.state.collectAsStateWithLifecycle()
val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value
val isBackgroundSyncEnabled = settingsViewModel.isBackgroundSync.collectAsStateWithLifecycle().value
val isKeepScreenOnWhileSyncing = settingsViewModel.isKeepScreenOnWhileSyncing.collectAsStateWithLifecycle().value
val isAnalyticsEnabled = settingsViewModel.isAnalyticsEnabled.collectAsStateWithLifecycle().value
val versionInfo = VersionInfo.new(activity.applicationContext)
BackHandler {
goBack()
LaunchedEffect(Unit) {
settingsViewModel.navigationCommand.collect {
navController.navigate(it)
}
}
if (null == isAnalyticsEnabled ||
null == isBackgroundSyncEnabled ||
null == isKeepScreenOnWhileSyncing
) {
// TODO [#1146]: Consider moving CircularScreenProgressIndicator from Android layer to View layer
// TODO [#1146]: Improve this by allowing screen composition and updating it after the data is available
// TODO [#1146]: https://github.com/Electric-Coin-Company/zashi-android/issues/1146
CircularScreenProgressIndicator()
} else {
LaunchedEffect(Unit) {
settingsViewModel.backNavigationCommand.collect {
navController.popBackStack()
}
}
BackHandler {
settingsViewModel.onBack()
}
state?.let {
Settings(
onAbout = goAbout,
onAdvancedSettings = goAdvancedSettings,
onBack = goBack,
onFeedback = goFeedback,
troubleshootingParameters =
TroubleshootingParameters(
isEnabled = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService,
isBackgroundSyncEnabled = isBackgroundSyncEnabled,
isKeepScreenOnDuringSyncEnabled = isKeepScreenOnWhileSyncing,
isAnalyticsEnabled = isAnalyticsEnabled,
isRescanEnabled = ConfigurationEntries.IS_RESCAN_ENABLED.getValue(RemoteConfig.current),
),
onRescanWallet = {
walletViewModel.rescanBlockchain()
},
onBackgroundSyncSettingsChanged = {
settingsViewModel.setBackgroundSyncEnabled(it)
},
onKeepScreenOnDuringSyncSettingsChanged = {
settingsViewModel.setKeepScreenOnWhileSyncing(it)
},
onAnalyticsSettingsChanged = {
settingsViewModel.setAnalyticsEnabled(it)
},
state = it,
topAppBarSubTitleState = walletState,
)
}

View File

@ -0,0 +1,13 @@
package co.electriccoin.zcash.ui.screen.settings.model
import co.electriccoin.zcash.ui.design.util.StringResource
data class SettingsState(
val isLoading: Boolean,
val version: StringResource,
val settingsTroubleshootingState: SettingsTroubleshootingState?,
val onBack: () -> Unit,
val onAdvancedSettingsClick: () -> Unit,
val onAboutUsClick: () -> Unit,
val onSendUsFeedbackClick: () -> Unit,
)

View File

@ -0,0 +1,17 @@
package co.electriccoin.zcash.ui.screen.settings.model
import androidx.compose.runtime.Immutable
@Immutable
data class SettingsTroubleshootingState(
val backgroundSync: TroubleshootingItemState,
val keepScreenOnDuringSync: TroubleshootingItemState,
val analytics: TroubleshootingItemState,
val rescan: TroubleshootingItemState,
)
@Immutable
data class TroubleshootingItemState(
val isEnabled: Boolean,
val onClick: () -> Unit,
)

View File

@ -1,9 +0,0 @@
package co.electriccoin.zcash.ui.screen.settings.model
data class TroubleshootingParameters(
val isEnabled: Boolean,
val isBackgroundSyncEnabled: Boolean,
val isKeepScreenOnDuringSyncEnabled: Boolean,
val isAnalyticsEnabled: Boolean,
val isRescanEnabled: Boolean,
)

View File

@ -1,9 +1,9 @@
package co.electriccoin.zcash.ui.screen.settings.view
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@ -14,6 +14,7 @@ import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
@ -22,106 +23,104 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem
import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar
import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation
import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
import co.electriccoin.zcash.ui.design.util.getValue
import co.electriccoin.zcash.ui.design.util.orDark
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.screen.settings.SettingsTag
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingParameters
import co.electriccoin.zcash.ui.screen.settings.model.SettingsState
import co.electriccoin.zcash.ui.screen.settings.model.SettingsTroubleshootingState
@Preview("Settings")
@Suppress("LongMethod")
@Composable
private fun PreviewSettings() {
ZcashTheme(forceDarkMode = false) {
Settings(
onAbout = {},
onAdvancedSettings = {},
onBack = {},
onFeedback = {},
onRescanWallet = {},
onBackgroundSyncSettingsChanged = {},
onKeepScreenOnDuringSyncSettingsChanged = {},
onAnalyticsSettingsChanged = {},
troubleshootingParameters =
TroubleshootingParameters(
isEnabled = false,
isBackgroundSyncEnabled = false,
isKeepScreenOnDuringSyncEnabled = false,
isAnalyticsEnabled = false,
isRescanEnabled = false
),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}
}
@Composable
@Suppress("LongParameterList")
fun Settings(
onAbout: () -> Unit,
onAdvancedSettings: () -> Unit,
onBack: () -> Unit,
onFeedback: () -> Unit,
onRescanWallet: () -> Unit,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit,
troubleshootingParameters: TroubleshootingParameters,
topAppBarSubTitleState: TopAppBarSubTitleState,
state: SettingsState,
topAppBarSubTitleState: TopAppBarSubTitleState
) {
BlankBgScaffold(topBar = {
SettingsTopAppBar(
troubleshootingParameters = troubleshootingParameters,
onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged,
onKeepScreenOnDuringSyncSettingsChanged = onKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged,
onRescanWallet = onRescanWallet,
onBack = onBack,
subTitleState = topAppBarSubTitleState,
)
}) { paddingValues ->
SettingsMainContent(
modifier =
Modifier
.verticalScroll(
rememberScrollState()
)
.padding(
top = paddingValues.calculateTopPadding() + dimens.spacingHuge,
bottom = paddingValues.calculateBottomPadding(),
start = dimens.screenHorizontalSpacingBig,
end = dimens.screenHorizontalSpacingBig
),
onAbout = onAbout,
onAdvancedSettings = onAdvancedSettings,
onFeedback = onFeedback,
)
BlankBgScaffold(
topBar = {
SettingsTopAppBar(
onBack = state.onBack,
subTitleState = topAppBarSubTitleState,
state = state
)
}
) { paddingValues ->
if (state.isLoading) {
CircularScreenProgressIndicator()
} else {
Column(
modifier =
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding(),
start = 4.dp,
end = 4.dp
),
) {
ZashiSettingsListItem(
text = stringResource(id = R.string.settings_advanced_settings),
icon = R.drawable.ic_advanced_settings orDark R.drawable.ic_advanced_settings_dark,
onClick = state.onAdvancedSettingsClick
)
HorizontalDivider(color = ZcashTheme.zashiColors.divider)
ZashiSettingsListItem(
text = stringResource(id = R.string.settings_about_us),
icon = R.drawable.ic_settings_info orDark R.drawable.ic_settings_info_dark,
onClick = state.onAboutUsClick
)
HorizontalDivider(color = ZcashTheme.zashiColors.divider)
ZashiSettingsListItem(
text = stringResource(id = R.string.settings_feedback),
icon = R.drawable.ic_settings_feedback orDark R.drawable.ic_settings_feedback_dark,
onClick = state.onSendUsFeedbackClick
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingMin))
Image(
modifier = Modifier.align(CenterHorizontally),
painter =
painterResource(id = R.drawable.ic_settings_zashi orDark R.drawable.ic_settings_zashi_dark),
contentDescription = ""
)
Spacer(modifier = Modifier.height(16.dp))
Text(
modifier = Modifier.align(CenterHorizontally),
text = state.version.getValue(),
color = ZcashTheme.zashiColors.textTertiary
)
Spacer(modifier = Modifier.height(20.dp))
}
}
}
}
@Composable
@Suppress("LongParameterList")
private fun SettingsTopAppBar(
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit,
onRescanWallet: () -> Unit,
onBack: () -> Unit,
subTitleState: TopAppBarSubTitleState,
troubleshootingParameters: TroubleshootingParameters,
state: SettingsState
) {
SmallTopAppBar(
subTitle =
ZashiSmallTopAppBar(
title = stringResource(id = R.string.settings_title),
subtitle =
when (subTitleState) {
TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label)
TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label)
@ -130,26 +129,73 @@ private fun SettingsTopAppBar(
modifier = Modifier.testTag(SettingsTag.SETTINGS_TOP_APP_BAR),
showTitleLogo = true,
navigationAction = {
TopAppBarBackNavigation(
backText = stringResource(id = R.string.back_navigation).uppercase(),
backContentDescriptionText = stringResource(R.string.back_navigation_content_description),
onBack = onBack
)
ZashiTopAppBarBackNavigation(onBack = onBack)
},
regularActions = {
if (troubleshootingParameters.isEnabled) {
TroubleshootingMenu(
troubleshootingParameters,
onBackgroundSyncSettingsChanged,
onKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged,
onRescanWallet
)
if (state.settingsTroubleshootingState != null) {
TroubleshootingMenu(state = state.settingsTroubleshootingState)
}
},
)
}
@Composable
private fun TroubleshootingMenu(state: SettingsTroubleshootingState) {
Column(
modifier = Modifier.testTag(SettingsTag.TROUBLESHOOTING_MENU)
) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(id = R.string.settings_troubleshooting_menu_content_description)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_background_sync)) },
onClick = {
state.backgroundSync.onClick()
expanded = false
},
leadingIcon = { AddIcon(state.backgroundSync.isEnabled) }
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_keep_screen_on)) },
onClick = {
state.keepScreenOnDuringSync.onClick()
expanded = false
},
leadingIcon = { AddIcon(state.keepScreenOnDuringSync.isEnabled) }
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_analytics)) },
onClick = {
state.analytics.onClick()
expanded = false
},
leadingIcon = { AddIcon(state.analytics.isEnabled) }
)
// isRescanEnabled means if this feature should be visible, not whether it is enabled as in the case of
// the previous booleans
if (state.rescan.isEnabled) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_rescan)) },
onClick = {
state.rescan.onClick()
expanded = false
},
leadingIcon = { AddIcon(true) }
)
}
}
}
}
/**
* Add icon to Troubleshooting menu. No content description, as this is debug only menu.
*/
@ -168,112 +214,44 @@ private fun AddIcon(enabled: Boolean) {
}
}
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun TroubleshootingMenu(
troubleshootParams: TroubleshootingParameters,
onBackgroundSyncSettingsChanged: (Boolean) -> Unit,
onKeepScreenOnDuringSyncSettingsChanged: (Boolean) -> Unit,
onAnalyticsSettingsChanged: (Boolean) -> Unit,
onRescanWallet: () -> Unit
) {
Column(
modifier = Modifier.testTag(SettingsTag.TROUBLESHOOTING_MENU)
) {
var expanded by rememberSaveable { mutableStateOf(false) }
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(id = R.string.settings_troubleshooting_menu_content_description)
)
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_background_sync)) },
onClick = {
onBackgroundSyncSettingsChanged(!troubleshootParams.isBackgroundSyncEnabled)
expanded = false
},
leadingIcon = { AddIcon(troubleshootParams.isBackgroundSyncEnabled) }
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_keep_screen_on)) },
onClick = {
onKeepScreenOnDuringSyncSettingsChanged(!troubleshootParams.isKeepScreenOnDuringSyncEnabled)
expanded = false
},
leadingIcon = { AddIcon(troubleshootParams.isKeepScreenOnDuringSyncEnabled) }
)
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_enable_analytics)) },
onClick = {
onAnalyticsSettingsChanged(!troubleshootParams.isAnalyticsEnabled)
expanded = false
},
leadingIcon = { AddIcon(troubleshootParams.isAnalyticsEnabled) }
)
// isRescanEnabled means if this feature should be visible, not whether it is enabled as in the case of
// the previous booleans
if (troubleshootParams.isRescanEnabled) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.settings_troubleshooting_rescan)) },
onClick = {
onRescanWallet()
expanded = false
},
leadingIcon = { AddIcon(true) }
)
}
}
private fun PreviewSettings() {
ZcashTheme {
Settings(
state =
SettingsState(
isLoading = false,
version = stringRes("Version 1.2"),
settingsTroubleshootingState = null,
onBack = {},
onAdvancedSettingsClick = {},
onAboutUsClick = {},
onSendUsFeedbackClick = {},
),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}
}
@Suppress("UnusedPrivateMember")
@PreviewScreens
@Composable
private fun SettingsMainContent(
onAbout: () -> Unit,
onAdvancedSettings: () -> Unit,
onFeedback: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
Modifier
.fillMaxHeight()
.fillMaxWidth()
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
PrimaryButton(
onClick = onFeedback,
text = stringResource(R.string.settings_send_us_feedback),
modifier = Modifier.fillMaxWidth()
private fun PreviewSettingsLoading() {
ZcashTheme {
Settings(
state =
SettingsState(
isLoading = true,
version = stringRes("Version 1.2"),
settingsTroubleshootingState = null,
onBack = {},
onAdvancedSettingsClick = {},
onAboutUsClick = {},
onSendUsFeedbackClick = {},
),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onAdvancedSettings,
text = stringResource(R.string.settings_advanced_settings),
modifier = Modifier.fillMaxWidth()
)
Spacer(
modifier =
Modifier
.fillMaxHeight()
.weight(MINIMAL_WEIGHT)
)
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton(
onClick = onAbout,
text = stringResource(R.string.settings_about),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(dimens.spacingHuge))
}
}

View File

@ -5,42 +5,124 @@ import androidx.lifecycle.viewModelScope
import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT
import co.electriccoin.zcash.preference.StandardPreferenceProvider
import co.electriccoin.zcash.preference.model.entry.BooleanPreferenceDefault
import co.electriccoin.zcash.ui.NavigationTargets.ABOUT
import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS
import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT
import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider
import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase
import co.electriccoin.zcash.ui.common.usecase.RescanBlockchainUseCase
import co.electriccoin.zcash.ui.configuration.ConfigurationEntries
import co.electriccoin.zcash.ui.design.util.stringRes
import co.electriccoin.zcash.ui.preference.StandardPreferenceKeys
import co.electriccoin.zcash.ui.screen.settings.model.SettingsState
import co.electriccoin.zcash.ui.screen.settings.model.SettingsTroubleshootingState
import co.electriccoin.zcash.ui.screen.settings.model.TroubleshootingItemState
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class SettingsViewModel(
observeConfiguration: ObserveConfigurationUseCase,
private val standardPreferenceProvider: StandardPreferenceProvider,
private val getVersionInfo: GetVersionInfoProvider,
private val rescanBlockchain: RescanBlockchainUseCase
) : ViewModel() {
val isAnalyticsEnabled: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_ANALYTICS_ENABLED)
private val versionInfo by lazy { getVersionInfo() }
val isBackgroundSync: StateFlow<Boolean?> = booleanStateFlow(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED)
val isKeepScreenOnWhileSyncing: StateFlow<Boolean?> =
private val isAnalyticsEnabled = booleanStateFlow(StandardPreferenceKeys.IS_ANALYTICS_ENABLED)
private val isBackgroundSyncEnabled = booleanStateFlow(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED)
private val isKeepScreenOnWhileSyncingEnabled =
booleanStateFlow(StandardPreferenceKeys.IS_KEEP_SCREEN_ON_DURING_SYNC)
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
flow<Boolean?> {
emitAll(default.observe(standardPreferenceProvider()))
private val isLoading =
combine(
isAnalyticsEnabled,
isBackgroundSyncEnabled,
isKeepScreenOnWhileSyncingEnabled
) { isAnalyticsEnabled, isBackgroundSync, isKeepScreenOnWhileSyncing ->
isAnalyticsEnabled == null || isBackgroundSync == null || isKeepScreenOnWhileSyncing == null
}.distinctUntilChanged()
@Suppress("ComplexCondition")
private val troubleshootingState =
combine(
observeConfiguration(),
isAnalyticsEnabled,
isBackgroundSyncEnabled,
isKeepScreenOnWhileSyncingEnabled
) { configuration, isAnalyticsEnabled, isBackgroundSyncEnabled, isKeepScreenOnWhileSyncingEnabled ->
if (configuration != null &&
isAnalyticsEnabled != null &&
isBackgroundSyncEnabled != null &&
isKeepScreenOnWhileSyncingEnabled != null &&
versionInfo.isDebuggable &&
!versionInfo.isRunningUnderTestService
) {
SettingsTroubleshootingState(
backgroundSync =
TroubleshootingItemState(
isBackgroundSyncEnabled
) { setBackgroundSyncEnabled(isBackgroundSyncEnabled.not()) },
keepScreenOnDuringSync =
TroubleshootingItemState(
isKeepScreenOnWhileSyncingEnabled
) { setKeepScreenOnWhileSyncing(isKeepScreenOnWhileSyncingEnabled.not()) },
analytics =
TroubleshootingItemState(
isAnalyticsEnabled
) { setAnalyticsEnabled(isAnalyticsEnabled.not()) },
rescan =
TroubleshootingItemState(
ConfigurationEntries.IS_RESCAN_ENABLED.getValue(configuration),
::onRescanBlockchainClick
)
)
} else {
null
}
}
val state: StateFlow<SettingsState?> =
combine(isLoading, troubleshootingState) { isLoading, troubleshootingState ->
SettingsState(
isLoading = isLoading,
version = stringRes(R.string.settings_version, getVersionInfo().versionName),
settingsTroubleshootingState = troubleshootingState,
onBack = ::onBack,
onAdvancedSettingsClick = ::onAdvancedSettingsClick,
onAboutUsClick = ::onAboutUsClick,
onSendUsFeedbackClick = ::onSendUsFeedbackClick,
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)
fun setAnalyticsEnabled(enabled: Boolean) {
val navigationCommand = MutableSharedFlow<String>()
val backNavigationCommand = MutableSharedFlow<Unit>()
private fun setAnalyticsEnabled(enabled: Boolean) {
setBooleanPreference(StandardPreferenceKeys.IS_ANALYTICS_ENABLED, enabled)
}
fun setBackgroundSyncEnabled(enabled: Boolean) {
private fun setBackgroundSyncEnabled(enabled: Boolean) {
setBooleanPreference(StandardPreferenceKeys.IS_BACKGROUND_SYNC_ENABLED, enabled)
}
fun setKeepScreenOnWhileSyncing(enabled: Boolean) {
private fun setKeepScreenOnWhileSyncing(enabled: Boolean) {
setBooleanPreference(StandardPreferenceKeys.IS_KEEP_SCREEN_ON_DURING_SYNC, enabled)
}
private fun onRescanBlockchainClick() =
viewModelScope.launch {
rescanBlockchain()
}
private fun setBooleanPreference(
default: BooleanPreferenceDefault,
newState: Boolean
@ -49,4 +131,29 @@ class SettingsViewModel(
default.putValue(standardPreferenceProvider(), newState)
}
}
fun onBack() =
viewModelScope.launch {
backNavigationCommand.emit(Unit)
}
private fun onAdvancedSettingsClick() =
viewModelScope.launch {
navigationCommand.emit(ADVANCED_SETTINGS)
}
private fun onAboutUsClick() =
viewModelScope.launch {
navigationCommand.emit(ABOUT)
}
private fun onSendUsFeedbackClick() =
viewModelScope.launch {
navigationCommand.emit(SUPPORT)
}
private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow<Boolean?> =
flow<Boolean?> {
emitAll(default.observe(standardPreferenceProvider()))
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null)
}

View File

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#EBEBE6"/>
<group>
<clip-path
android:pathData="M10,10h20v20h-20z"/>
<path
android:pathData="M20,22.5C21.381,22.5 22.5,21.381 22.5,20C22.5,18.619 21.381,17.5 20,17.5C18.619,17.5 17.5,18.619 17.5,20C17.5,21.381 18.619,22.5 20,22.5Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
<path
android:pathData="M25.606,22.273C25.505,22.501 25.475,22.755 25.52,23C25.564,23.246 25.681,23.473 25.856,23.652L25.902,23.697C26.042,23.838 26.154,24.005 26.23,24.189C26.307,24.373 26.346,24.57 26.346,24.769C26.346,24.968 26.307,25.165 26.23,25.349C26.154,25.533 26.042,25.7 25.902,25.841C25.761,25.982 25.594,26.094 25.41,26.17C25.226,26.246 25.029,26.285 24.83,26.285C24.63,26.285 24.433,26.246 24.249,26.17C24.065,26.094 23.898,25.982 23.757,25.841L23.712,25.795C23.534,25.621 23.307,25.504 23.061,25.459C22.815,25.414 22.562,25.445 22.333,25.545C22.109,25.641 21.918,25.801 21.784,26.004C21.649,26.207 21.577,26.446 21.576,26.689V26.818C21.576,27.22 21.416,27.605 21.132,27.889C20.848,28.174 20.462,28.333 20.061,28.333C19.659,28.333 19.273,28.174 18.989,27.889C18.705,27.605 18.545,27.22 18.545,26.818V26.75C18.539,26.499 18.458,26.256 18.313,26.052C18.167,25.848 17.963,25.693 17.727,25.606C17.499,25.505 17.245,25.475 17,25.52C16.754,25.564 16.527,25.681 16.348,25.856L16.303,25.902C16.162,26.042 15.995,26.154 15.811,26.23C15.627,26.307 15.43,26.346 15.231,26.346C15.032,26.346 14.835,26.307 14.651,26.23C14.467,26.154 14.3,26.042 14.159,25.902C14.018,25.761 13.906,25.594 13.83,25.41C13.754,25.226 13.715,25.029 13.715,24.83C13.715,24.63 13.754,24.433 13.83,24.249C13.906,24.065 14.018,23.898 14.159,23.757L14.205,23.712C14.379,23.534 14.496,23.307 14.541,23.061C14.585,22.815 14.555,22.562 14.455,22.333C14.358,22.109 14.199,21.918 13.996,21.784C13.792,21.649 13.554,21.577 13.311,21.576H13.182C12.78,21.576 12.394,21.416 12.11,21.132C11.826,20.848 11.667,20.462 11.667,20.061C11.667,19.659 11.826,19.273 12.11,18.989C12.394,18.705 12.78,18.545 13.182,18.545H13.25C13.501,18.539 13.744,18.458 13.948,18.313C14.152,18.167 14.307,17.963 14.394,17.727C14.495,17.499 14.525,17.245 14.48,17C14.436,16.754 14.318,16.527 14.144,16.348L14.098,16.303C13.958,16.162 13.846,15.995 13.77,15.811C13.693,15.627 13.654,15.43 13.654,15.231C13.654,15.032 13.693,14.835 13.77,14.651C13.846,14.467 13.958,14.3 14.098,14.159C14.239,14.018 14.406,13.906 14.59,13.83C14.774,13.754 14.971,13.715 15.17,13.715C15.37,13.715 15.567,13.754 15.751,13.83C15.935,13.906 16.102,14.018 16.242,14.159L16.288,14.205C16.466,14.379 16.693,14.496 16.939,14.541C17.185,14.585 17.438,14.555 17.667,14.455H17.727C17.951,14.358 18.142,14.199 18.277,13.996C18.412,13.792 18.484,13.554 18.485,13.311V13.182C18.485,12.78 18.644,12.394 18.929,12.11C19.213,11.826 19.598,11.667 20,11.667C20.402,11.667 20.787,11.826 21.071,12.11C21.355,12.394 21.515,12.78 21.515,13.182V13.25C21.516,13.494 21.588,13.732 21.723,13.935C21.858,14.138 22.049,14.298 22.273,14.394C22.501,14.495 22.755,14.525 23,14.48C23.246,14.436 23.473,14.318 23.652,14.144L23.697,14.098C23.838,13.958 24.005,13.846 24.189,13.77C24.373,13.693 24.57,13.654 24.769,13.654C24.968,13.654 25.165,13.693 25.349,13.77C25.533,13.846 25.7,13.958 25.841,14.098C25.982,14.239 26.094,14.406 26.17,14.59C26.246,14.774 26.285,14.971 26.285,15.17C26.285,15.37 26.246,15.567 26.17,15.751C26.094,15.935 25.982,16.102 25.841,16.242L25.795,16.288C25.621,16.466 25.504,16.693 25.459,16.939C25.414,17.185 25.445,17.438 25.545,17.667V17.727C25.641,17.951 25.801,18.142 26.004,18.277C26.207,18.412 26.446,18.484 26.689,18.485H26.818C27.22,18.485 27.605,18.644 27.889,18.929C28.174,19.213 28.333,19.598 28.333,20C28.333,20.402 28.174,20.787 27.889,21.071C27.605,21.355 27.22,21.515 26.818,21.515H26.75C26.506,21.516 26.268,21.588 26.065,21.723C25.861,21.858 25.702,22.049 25.606,22.273Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#EBEBE6"/>
<path
android:pathData="M15,16.667H15.008M15,23.333H15.008M15,20H25M15,20C13.159,20 11.667,18.508 11.667,16.667C11.667,14.826 13.159,13.333 15,13.333H25C26.841,13.333 28.333,14.826 28.333,16.667C28.333,18.508 26.841,20 25,20M15,20C13.159,20 11.667,21.492 11.667,23.333C11.667,25.174 13.159,26.667 15,26.667H25C26.841,26.667 28.333,25.174 28.333,23.333C28.333,21.492 26.841,20 25,20"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#454243"/>
<path
android:pathData="M15,16.667H15.008M15,23.333H15.008M15,20H25M15,20C13.159,20 11.667,18.508 11.667,16.667C11.667,14.826 13.159,13.333 15,13.333H25C26.841,13.333 28.333,14.826 28.333,16.667C28.333,18.508 26.841,20 25,20M15,20C13.159,20 11.667,21.492 11.667,23.333C11.667,25.174 13.159,26.667 15,26.667H25C26.841,26.667 28.333,25.174 28.333,23.333C28.333,21.492 26.841,20 25,20"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#EBEBE6"/>
<path
android:pathData="M15,23.333C15,25.174 16.492,26.667 18.333,26.667H21.667C23.508,26.667 25,25.174 25,23.333C25,21.492 23.508,20 21.667,20H18.333C16.492,20 15,18.508 15,16.667C15,14.826 16.492,13.333 18.333,13.333H21.667C23.508,13.333 25,14.826 25,16.667M20,11.667V28.333"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#454243"/>
<path
android:pathData="M15,23.333C15,25.174 16.492,26.667 18.333,26.667H21.667C23.508,26.667 25,25.174 25,23.333C25,21.492 23.508,20 21.667,20H18.333C16.492,20 15,18.508 15,16.667C15,14.826 16.492,13.333 18.333,13.333H21.667C23.508,13.333 25,14.826 25,16.667M20,11.667V28.333"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#454243"/>
<group>
<clip-path
android:pathData="M10,10h20v20h-20z"/>
<path
android:pathData="M20,22.5C21.381,22.5 22.5,21.381 22.5,20C22.5,18.619 21.381,17.5 20,17.5C18.619,17.5 17.5,18.619 17.5,20C17.5,21.381 18.619,22.5 20,22.5Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
<path
android:pathData="M25.606,22.273C25.505,22.501 25.475,22.755 25.52,23C25.564,23.246 25.681,23.473 25.856,23.652L25.902,23.697C26.042,23.838 26.154,24.005 26.23,24.189C26.307,24.373 26.346,24.57 26.346,24.769C26.346,24.968 26.307,25.165 26.23,25.349C26.154,25.533 26.042,25.7 25.902,25.841C25.761,25.982 25.594,26.094 25.41,26.17C25.226,26.246 25.029,26.285 24.83,26.285C24.63,26.285 24.433,26.246 24.249,26.17C24.065,26.094 23.898,25.982 23.758,25.841L23.712,25.795C23.534,25.621 23.307,25.504 23.061,25.459C22.815,25.414 22.562,25.445 22.333,25.545C22.109,25.641 21.918,25.801 21.784,26.004C21.649,26.207 21.577,26.446 21.576,26.689V26.818C21.576,27.22 21.416,27.605 21.132,27.889C20.848,28.174 20.462,28.333 20.061,28.333C19.659,28.333 19.273,28.174 18.989,27.889C18.705,27.605 18.545,27.22 18.545,26.818V26.75C18.54,26.499 18.458,26.256 18.313,26.052C18.167,25.848 17.963,25.693 17.727,25.606C17.499,25.505 17.245,25.475 17,25.52C16.754,25.564 16.527,25.681 16.348,25.856L16.303,25.902C16.162,26.042 15.995,26.154 15.811,26.23C15.627,26.307 15.43,26.346 15.231,26.346C15.032,26.346 14.835,26.307 14.651,26.23C14.467,26.154 14.3,26.042 14.159,25.902C14.018,25.761 13.906,25.594 13.83,25.41C13.754,25.226 13.715,25.029 13.715,24.83C13.715,24.63 13.754,24.433 13.83,24.249C13.906,24.065 14.018,23.898 14.159,23.757L14.205,23.712C14.379,23.534 14.496,23.307 14.541,23.061C14.585,22.815 14.555,22.562 14.455,22.333C14.358,22.109 14.199,21.918 13.996,21.784C13.793,21.649 13.554,21.577 13.311,21.576H13.182C12.78,21.576 12.395,21.416 12.11,21.132C11.826,20.848 11.667,20.462 11.667,20.061C11.667,19.659 11.826,19.273 12.11,18.989C12.395,18.705 12.78,18.545 13.182,18.545H13.25C13.501,18.539 13.744,18.458 13.948,18.313C14.152,18.167 14.307,17.963 14.394,17.727C14.495,17.499 14.525,17.245 14.48,17C14.436,16.754 14.319,16.527 14.144,16.348L14.099,16.303C13.958,16.162 13.846,15.995 13.77,15.811C13.693,15.627 13.654,15.43 13.654,15.231C13.654,15.032 13.693,14.835 13.77,14.651C13.846,14.467 13.958,14.3 14.099,14.159C14.239,14.018 14.406,13.906 14.59,13.83C14.774,13.754 14.971,13.715 15.17,13.715C15.37,13.715 15.567,13.754 15.751,13.83C15.935,13.906 16.102,14.018 16.242,14.159L16.288,14.205C16.466,14.379 16.693,14.496 16.939,14.541C17.185,14.585 17.438,14.555 17.667,14.455H17.727C17.951,14.358 18.142,14.199 18.277,13.996C18.412,13.792 18.484,13.554 18.485,13.311V13.182C18.485,12.78 18.645,12.394 18.929,12.11C19.213,11.826 19.598,11.667 20,11.667C20.402,11.667 20.787,11.826 21.071,12.11C21.355,12.394 21.515,12.78 21.515,13.182V13.25C21.516,13.494 21.588,13.732 21.723,13.935C21.858,14.138 22.049,14.298 22.273,14.394C22.501,14.495 22.755,14.525 23,14.48C23.246,14.436 23.473,14.318 23.652,14.144L23.697,14.098C23.838,13.958 24.005,13.846 24.189,13.77C24.373,13.693 24.57,13.654 24.769,13.654C24.968,13.654 25.165,13.693 25.349,13.77C25.533,13.846 25.7,13.958 25.841,14.098C25.982,14.239 26.094,14.406 26.17,14.59C26.246,14.774 26.285,14.971 26.285,15.17C26.285,15.37 26.246,15.567 26.17,15.751C26.094,15.935 25.982,16.102 25.841,16.242L25.795,16.288C25.621,16.466 25.504,16.693 25.459,16.939C25.414,17.185 25.445,17.438 25.545,17.667V17.727C25.642,17.951 25.801,18.142 26.004,18.277C26.207,18.412 26.446,18.484 26.689,18.485H26.818C27.22,18.485 27.605,18.644 27.89,18.929C28.174,19.213 28.333,19.598 28.333,20C28.333,20.402 28.174,20.787 27.89,21.071C27.605,21.355 27.22,21.515 26.818,21.515H26.75C26.506,21.516 26.268,21.588 26.065,21.723C25.862,21.858 25.702,22.049 25.606,22.273Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#EBEBE6"/>
<path
android:pathData="M13.333,23.535C12.328,22.862 11.667,21.717 11.667,20.417C11.667,18.464 13.16,16.859 15.066,16.683C15.457,14.31 17.517,12.5 20,12.5C22.483,12.5 24.544,14.31 24.934,16.683C26.84,16.859 28.333,18.464 28.333,20.417C28.333,21.717 27.672,22.862 26.667,23.535M16.667,24.167L20,27.5M20,27.5L23.333,24.167M20,27.5V20"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#454243"/>
<path
android:pathData="M13.333,23.535C12.328,22.862 11.667,21.717 11.667,20.417C11.667,18.464 13.16,16.859 15.066,16.683C15.457,14.31 17.517,12.5 20,12.5C22.483,12.5 24.544,14.31 24.934,16.683C26.84,16.859 28.333,18.464 28.333,20.417C28.333,21.717 27.672,22.862 26.667,23.535M16.667,24.167L20,27.5M20,27.5L23.333,24.167M20,27.5V20"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<group>
<clip-path
android:pathData="M0,0h20v20h-20z"/>
<path
android:pathData="M10,13.333V10M10,6.667H10.008M18.333,10C18.333,14.602 14.602,18.333 10,18.333C5.398,18.333 1.667,14.602 1.667,10C1.667,5.398 5.398,1.667 10,1.667C14.602,1.667 18.333,5.398 18.333,10Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#716C5D"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#EBEBE6"/>
<path
android:pathData="M24.167,17.5C24.167,17.073 24.004,16.647 23.678,16.322C23.353,15.996 22.927,15.833 22.5,15.833M22.5,22.5C25.261,22.5 27.5,20.261 27.5,17.5C27.5,14.739 25.261,12.5 22.5,12.5C19.739,12.5 17.5,14.739 17.5,17.5C17.5,17.728 17.515,17.953 17.545,18.173C17.594,18.534 17.618,18.715 17.601,18.83C17.584,18.949 17.563,19.013 17.504,19.118C17.448,19.219 17.348,19.319 17.149,19.517L12.891,23.776C12.746,23.92 12.674,23.992 12.623,24.076C12.577,24.151 12.543,24.232 12.523,24.317C12.5,24.413 12.5,24.515 12.5,24.719V26.167C12.5,26.633 12.5,26.867 12.591,27.045C12.671,27.202 12.798,27.329 12.955,27.409C13.133,27.5 13.367,27.5 13.833,27.5H15.833V25.833H17.5V24.167H19.167L20.483,22.851C20.681,22.652 20.781,22.552 20.882,22.496C20.987,22.437 21.051,22.416 21.17,22.399C21.285,22.382 21.466,22.406 21.827,22.455C22.047,22.485 22.272,22.5 22.5,22.5Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#454243"/>
<path
android:pathData="M24.167,17.5C24.167,17.073 24.004,16.647 23.678,16.322C23.353,15.996 22.927,15.833 22.5,15.833M22.5,22.5C25.261,22.5 27.5,20.261 27.5,17.5C27.5,14.739 25.261,12.5 22.5,12.5C19.739,12.5 17.5,14.739 17.5,17.5C17.5,17.728 17.515,17.953 17.545,18.173C17.594,18.534 17.618,18.715 17.601,18.83C17.584,18.949 17.563,19.013 17.504,19.118C17.448,19.219 17.348,19.319 17.149,19.517L12.891,23.776C12.746,23.92 12.674,23.992 12.623,24.076C12.577,24.151 12.543,24.232 12.523,24.317C12.5,24.413 12.5,24.515 12.5,24.719V26.167C12.5,26.633 12.5,26.867 12.591,27.045C12.671,27.202 12.798,27.329 12.955,27.409C13.133,27.5 13.367,27.5 13.833,27.5H15.833V25.833H17.5V24.167H19.167L20.483,22.851C20.681,22.652 20.781,22.552 20.882,22.496C20.987,22.437 21.051,22.416 21.17,22.399C21.285,22.382 21.466,22.406 21.827,22.455C22.047,22.485 22.272,22.5 22.5,22.5Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,11 +1,10 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="advanced_settings_backup_wallet">Recovery phrase</string>
<string name="advanced_settings_export_private_data">Export private data</string>
<string name="advanced_settings_choose_server">Choose a server</string>
<string name="advanced_settings_title">Advanced Settings</string>
<string name="advanced_settings_recovery">Recovery Phrase</string>
<string name="advanced_settings_export">Export Private Data</string>
<string name="advanced_settings_choose_server">Choose a Server</string>
<string name="advanced_settings_currency_conversion">Currency Conversion</string>
<string name="advanced_settings_delete_wallet">
Delete <xliff:g id="app_name" example="Zashi">%1$s</xliff:g>
</string>
<string name="advanced_settings_delete_wallet_footnote">(You will be asked to confirm on next screen)</string>
<string name="advanced_settings_coinbase">Buy ZEC with Coinbase</string>
<string name="advanced_settings_info">You will be asked to confirm on the next screen</string>
<string name="advanced_settings_delete_button">Delete Zashi</string>
</resources>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<group>
<clip-path
android:pathData="M0,0h40v40h-40z"/>
<path
android:pathData="M19.983,0.009C31.004,0.009 39.939,8.944 39.939,19.965C39.939,30.987 31.004,39.922 19.983,39.922C8.961,39.922 0.026,30.987 0.026,19.965C0.026,8.944 8.961,0.009 19.983,0.009Z"
android:fillColor="#0052FF"/>
<path
android:pathData="M19.991,26.978C16.113,26.978 12.978,23.836 12.978,19.965C12.978,16.095 16.121,12.952 19.991,12.952C23.463,12.952 26.346,15.481 26.9,18.796H33.965C33.368,11.593 27.342,5.931 19.983,5.931C12.234,5.931 5.948,12.216 5.948,19.965C5.948,27.714 12.234,34 19.983,34C27.342,34 33.368,28.338 33.965,21.134H26.892C26.338,24.45 23.463,26.978 19.991,26.978Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#EBEBE6"/>
<path
android:pathData="M17.5,21.667C17.5,21.667 18.594,22.917 20.416,22.917C22.239,22.917 23.333,21.667 23.333,21.667M22.708,17.5H22.716M18.125,17.5H18.133M20.416,26.667C24.328,26.667 27.5,23.495 27.5,19.583C27.5,15.671 24.328,12.5 20.416,12.5C16.504,12.5 13.333,15.671 13.333,19.583C13.333,20.375 13.463,21.136 13.703,21.847C13.793,22.115 13.838,22.248 13.846,22.351C13.854,22.453 13.848,22.524 13.823,22.622C13.797,22.722 13.741,22.826 13.629,23.034L12.266,25.557C12.071,25.917 11.974,26.097 11.996,26.236C12.015,26.357 12.086,26.463 12.191,26.527C12.311,26.6 12.514,26.579 12.921,26.537L17.188,26.096C17.318,26.083 17.382,26.076 17.441,26.078C17.499,26.08 17.54,26.086 17.597,26.099C17.654,26.112 17.726,26.14 17.871,26.195C18.661,26.5 19.519,26.667 20.416,26.667ZM23.125,17.5C23.125,17.73 22.938,17.917 22.708,17.917C22.478,17.917 22.291,17.73 22.291,17.5C22.291,17.27 22.478,17.083 22.708,17.083C22.938,17.083 23.125,17.27 23.125,17.5ZM18.541,17.5C18.541,17.73 18.355,17.917 18.125,17.917C17.895,17.917 17.708,17.73 17.708,17.5C17.708,17.27 17.895,17.083 18.125,17.083C18.355,17.083 18.541,17.27 18.541,17.5Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#454243"/>
<path
android:pathData="M17.5,21.667C17.5,21.667 18.593,22.917 20.416,22.917C22.239,22.917 23.333,21.667 23.333,21.667M22.708,17.5H22.716M18.125,17.5H18.133M20.416,26.667C24.328,26.667 27.5,23.495 27.5,19.583C27.5,15.671 24.328,12.5 20.416,12.5C16.504,12.5 13.333,15.671 13.333,19.583C13.333,20.375 13.463,21.136 13.703,21.847C13.793,22.115 13.838,22.248 13.846,22.351C13.854,22.453 13.848,22.524 13.823,22.622C13.797,22.722 13.741,22.826 13.629,23.034L12.266,25.557C12.071,25.917 11.974,26.097 11.996,26.236C12.015,26.357 12.086,26.463 12.191,26.527C12.311,26.6 12.514,26.579 12.921,26.537L17.188,26.096C17.318,26.083 17.382,26.076 17.441,26.078C17.499,26.08 17.54,26.086 17.597,26.099C17.654,26.112 17.726,26.14 17.871,26.195C18.661,26.5 19.519,26.667 20.416,26.667ZM23.125,17.5C23.125,17.73 22.938,17.917 22.708,17.917C22.478,17.917 22.291,17.73 22.291,17.5C22.291,17.27 22.478,17.083 22.708,17.083C22.938,17.083 23.125,17.27 23.125,17.5ZM18.541,17.5C18.541,17.73 18.355,17.917 18.125,17.917C17.895,17.917 17.708,17.73 17.708,17.5C17.708,17.27 17.895,17.083 18.125,17.083C18.355,17.083 18.541,17.27 18.541,17.5Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#EBEBE6"/>
<group>
<clip-path
android:pathData="M10,10h20v20h-20z"/>
<path
android:pathData="M20,23.333V20M20,16.667H20.008M28.333,20C28.333,24.602 24.602,28.333 20,28.333C15.398,28.333 11.667,24.602 11.667,20C11.667,15.398 15.398,11.667 20,11.667C24.602,11.667 28.333,15.398 28.333,20Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#231F20"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="40dp"
android:height="40dp"
android:viewportWidth="40"
android:viewportHeight="40">
<path
android:pathData="M0,20C0,8.954 8.954,0 20,0C31.046,0 40,8.954 40,20C40,31.046 31.046,40 20,40C8.954,40 0,31.046 0,20Z"
android:fillColor="#454243"/>
<group>
<clip-path
android:pathData="M10,10h20v20h-20z"/>
<path
android:pathData="M20,23.333V20M20,16.667H20.008M28.333,20C28.333,24.602 24.602,28.333 20,28.333C15.398,28.333 11.667,24.602 11.667,20C11.667,15.398 15.398,11.667 20,11.667C24.602,11.667 28.333,15.398 28.333,20Z"
android:strokeLineJoin="round"
android:strokeWidth="1.66667"
android:fillColor="#00000000"
android:strokeColor="#E8E8E8"
android:strokeLineCap="round"/>
</group>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="75dp"
android:height="20dp"
android:viewportWidth="75"
android:viewportHeight="20">
<path
android:pathData="M23.077,0.582L26.93,0.003L33.71,19.598L29.301,20M29.301,19.967L27.355,14.952L20.739,14.564L18.793,19.612L15.681,19.969L23.074,0.585M21.908,11.848L26.15,11.854L23.851,6.021L21.905,11.846L21.908,11.848Z"
android:fillColor="#231F20"/>
<path
android:pathData="M41.265,19.969C38.305,19.969 35.584,19.104 34.024,17.021L37.775,13.857C38.885,15.281 39.904,16.209 41.623,16.893L45.043,16.86C45.36,16.253 45.493,15.674 45.493,15.12C45.493,13.328 44.541,12.773 41.001,12.086C36.959,11.321 34.474,9.872 34.474,6.6C34.474,5.414 35.082,3.99 36.031,2.962L42.267,0C44.882,0 47.817,0.903 49.375,2.987L45.621,6.151C44.51,4.727 43.403,3.832 41.684,3.145H38.461C38.169,3.646 38.011,4.201 38.011,4.755C38.011,6.442 39.199,6.969 43.03,7.656C47.364,8.368 49.055,9.95 49.055,13.089C49.055,14.275 48.528,15.672 47.656,16.965L41.262,19.966L41.265,19.969Z"
android:fillColor="#231F20"/>
<path
android:pathData="M62.645,0.003L67.221,0.582V19.612L62.645,19.967V11.192L55.546,11.067V19.609L50.876,19.967V0.538L55.546,0.003V8.349L62.645,8.291V0.003Z"
android:fillColor="#231F20"/>
<path
android:pathData="M69.559,0.529L74.228,0.005V19.969L69.559,19.614V0.529Z"
android:fillColor="#231F20"/>
<path
android:pathData="M6.607,15.22H15.558L13.987,19.509H0.771V18.326L10.111,4.344H1.549L2.715,0.46H15.944"
android:fillColor="#231F20"/>
</vector>

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="75dp"
android:height="20dp"
android:viewportWidth="75"
android:viewportHeight="20">
<path
android:pathData="M23.077,0.582L26.93,0.003L33.71,19.598L29.301,20M29.301,19.967L27.355,14.952L20.739,14.564L18.793,19.612L15.681,19.969L23.074,0.585M21.908,11.848L26.15,11.854L23.851,6.021L21.905,11.846L21.908,11.848Z"
android:fillColor="#E8E8E8"/>
<path
android:pathData="M41.265,19.969C38.305,19.969 35.584,19.104 34.024,17.021L37.775,13.857C38.886,15.281 39.904,16.209 41.623,16.893L45.043,16.86C45.36,16.253 45.493,15.674 45.493,15.12C45.493,13.328 44.541,12.773 41.001,12.086C36.959,11.321 34.474,9.872 34.474,6.6C34.474,5.414 35.082,3.99 36.031,2.962L42.267,0C44.882,0 47.817,0.903 49.375,2.987L45.621,6.151C44.51,4.727 43.403,3.832 41.684,3.145H38.461C38.169,3.646 38.011,4.201 38.011,4.755C38.011,6.442 39.199,6.969 43.031,7.656C47.364,8.368 49.055,9.95 49.055,13.089C49.055,14.275 48.528,15.672 47.656,16.965L41.262,19.966L41.265,19.969Z"
android:fillColor="#E8E8E8"/>
<path
android:pathData="M62.645,0.003L67.221,0.582V19.612L62.645,19.967V11.192L55.546,11.067V19.609L50.876,19.967V0.538L55.546,0.003V8.349L62.645,8.291V0.003Z"
android:fillColor="#E8E8E8"/>
<path
android:pathData="M69.559,0.529L74.228,0.005V19.969L69.559,19.614V0.529Z"
android:fillColor="#E8E8E8"/>
<path
android:pathData="M6.607,15.22H15.559L13.987,19.509H0.772V18.326L10.111,4.344H1.549L2.715,0.46H15.944"
android:fillColor="#E8E8E8"/>
</vector>

View File

@ -1,11 +1,14 @@
<resources>
<string name="settings_title">Settings</string>
<string name="settings_advanced_settings">Advanced Settings</string>
<string name="settings_about_us">About Us</string>
<string name="settings_feedback">Send Us Feedback</string>
<string name="settings_version">Version %s</string>
<string name="settings_troubleshooting_menu_content_description">Additional settings</string>
<string name="settings_troubleshooting_rescan">Rescan blockchain</string>
<string name="settings_troubleshooting_enable_background_sync">Background sync</string>
<string name="settings_troubleshooting_enable_keep_screen_on">Keep screen on during sync</string>
<string name="settings_troubleshooting_enable_analytics">Report crashes</string>
<string name="settings_send_us_feedback">Send us feedback</string>
<string name="settings_advanced_settings">Advanced</string>
<string name="settings_about">About</string>
</resources>

View File

@ -425,7 +425,7 @@ private fun settingsScreenshots(
composeTestRule: ComposeTestRule
) {
composeTestRule.onNode(
hasText(resContext.getString(R.string.settings_send_us_feedback), ignoreCase = true)
hasText(resContext.getString(R.string.settings_feedback), ignoreCase = true)
).also {
it.assertExists()
}