diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b27a14dc..e90d3082b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2 - The in-app update logic has been fixed and is now correctly requested with every app launch - The Not enough space and In-app udpate screens have been redesigned - External links now open in in-app browser +- All the Settings screens have been redesigned ### Fixed - Address book toast now correctly shows on send screen when adding both new and known addresses to text field diff --git a/docs/whatsNew/WHATS_NEW_EN.md b/docs/whatsNew/WHATS_NEW_EN.md index ec8bea3d9..663fd1b8e 100644 --- a/docs/whatsNew/WHATS_NEW_EN.md +++ b/docs/whatsNew/WHATS_NEW_EN.md @@ -21,6 +21,7 @@ directly impact users rather than highlighting other key architectural updates.* - The in-app update logic has been fixed and is now correctly requested with every app launch - The Not enough space and In-app udpate screens have been redesigned - External links now open in in-app browser +- All the Settings screens have been redesigned ### Fixed - Address book toast now correctly shows on send screen when adding both new and known addresses to text field diff --git a/docs/whatsNew/WHATS_NEW_ES.md b/docs/whatsNew/WHATS_NEW_ES.md index 174e2bad8..104f7e25b 100644 --- a/docs/whatsNew/WHATS_NEW_ES.md +++ b/docs/whatsNew/WHATS_NEW_ES.md @@ -21,6 +21,7 @@ directly impact users rather than highlighting other key architectural updates.* - The in-app update logic has been fixed and is now correctly requested with every app launch - The Not enough space and In-app udpate screens have been redesigned - External links now open in in-app browser +- All the Settings screens have been redesigned ### Fixed - Address book toast now correctly shows on send screen when adding both new and known addresses to text field diff --git a/tools/detekt.yml b/tools/detekt.yml index 9cf67580a..febece8c3 100644 --- a/tools/detekt.yml +++ b/tools/detekt.yml @@ -32,6 +32,13 @@ style: ignoreAnnotated: - 'Preview' - 'PreviewScreens' + - 'PreviewScreenSizes' + MagicNumber: + active: true + ignoreAnnotated: + - 'Preview' + - 'PreviewScreens' + - 'PreviewScreenSizes' complexity: LongMethod: @@ -39,11 +46,13 @@ complexity: ignoreAnnotated: - 'Preview' - 'PreviewScreens' + - 'PreviewScreenSizes' LongParameterList: active: false ignoreAnnotated: - 'Preview' - 'PreviewScreens' + - 'PreviewScreenSizes' Compose: ModifierMissing: diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ChipGrid.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ChipGrid.kt deleted file mode 100644 index 5bc2ca6bb..000000000 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ChipGrid.kt +++ /dev/null @@ -1,102 +0,0 @@ -package co.electriccoin.zcash.ui.design.component - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.tooling.preview.Preview -import cash.z.ecc.android.sdk.fixture.WalletFixture -import cash.z.ecc.android.sdk.model.SeedPhrase -import co.electriccoin.zcash.spackle.model.Index -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList - -// TODO [#1001]: Row size should probably change for landscape layouts -// TODO [#1001]: https://github.com/Electric-Coin-Company/zashi-android/issues/1001 -const val CHIP_GRID_COLUMN_SIZE = 12 - -@Preview -@Composable -private fun ChipGridPreview() { - ZcashTheme(forceDarkMode = false) { - BlankSurface { - ChipGrid( - SeedPhrase.new(WalletFixture.Alice.seedPhrase).split.toPersistentList(), - onGridClick = {} - ) - } - } -} - -@Preview -@Composable -private fun ChipGridDarkPreview() { - ZcashTheme(forceDarkMode = true) { - BlankSurface { - ChipGrid( - SeedPhrase.new(WalletFixture.Alice.seedPhrase).split.toPersistentList(), - onGridClick = {} - ) - } - } -} - -@Composable -fun ChipGrid( - wordList: ImmutableList, - onGridClick: () -> Unit, - modifier: Modifier = Modifier, - allowCopy: Boolean = false, -) { - val interactionSource = remember { MutableInteractionSource() } - - Row( - modifier = modifier.then(Modifier.fillMaxWidth()), - horizontalArrangement = Arrangement.Center - ) { - Row( - modifier = - Modifier - .wrapContentWidth() - .testTag(CommonTag.CHIP_LAYOUT) - .then( - if (allowCopy) { - Modifier - .clickable( - interactionSource = interactionSource, - // Disable ripple - indication = null, - onClick = onGridClick - ) - } else { - Modifier - } - ) - ) { - wordList.chunked(CHIP_GRID_COLUMN_SIZE).forEachIndexed { chunkIndex, chunk -> - // TODO [#1043]: Correctly align numbers and words on Recovery screen - // TODO [#1043]: https://github.com/Electric-Coin-Company/zashi-android/issues/1043 - Column( - modifier = Modifier.padding(horizontal = ZcashTheme.dimens.spacingDefault) - ) { - chunk.forEachIndexed { subIndex, word -> - ChipIndexed( - index = Index(chunkIndex * CHIP_GRID_COLUMN_SIZE + subIndex), - text = word, - modifier = Modifier.padding(ZcashTheme.dimens.spacingXtiny) - ) - } - } - } - } - } -} diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt index e50c8284a..d8504c20f 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiButton.kt @@ -1,5 +1,6 @@ package co.electriccoin.zcash.ui.design.component +import androidx.annotation.DrawableRes import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.isSystemInDarkTheme @@ -13,12 +14,13 @@ import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.LocalContentColor 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.graphics.painter.Painter +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -39,7 +41,7 @@ fun ZashiButton( ) { ZashiButton( text = state.text.getValue(), - leadingIcon = state.leadingIconVector, + icon = state.icon, onClick = state.onClick, modifier = modifier, enabled = state.isEnabled, @@ -55,7 +57,7 @@ fun ZashiButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, - leadingIcon: Painter? = null, + @DrawableRes icon: Int? = null, enabled: Boolean = true, isLoading: Boolean = false, colors: ZashiButtonColors = ZashiButtonDefaults.primaryColors(), @@ -65,11 +67,12 @@ fun ZashiButton( object : ZashiButtonScope { @Composable override fun LeadingIcon() { - if (leadingIcon != null) { + if (icon != null) { Image( - painter = leadingIcon, + painter = painterResource(icon), contentDescription = null, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), + colorFilter = ColorFilter.tint(LocalContentColor.current) ) } } @@ -98,6 +101,8 @@ fun ZashiButton( } } + val borderColor = if (enabled) colors.borderColor else colors.disabledBorderColor + Button( onClick = onClick, modifier = modifier, @@ -105,7 +110,7 @@ fun ZashiButton( contentPadding = PaddingValues(horizontal = 10.dp), enabled = enabled, colors = colors.toButtonColors(), - border = colors.borderColor.takeIf { it != Color.Unspecified }?.let { BorderStroke(1.dp, it) }, + border = borderColor.takeIf { it != Color.Unspecified }?.let { BorderStroke(1.dp, it) }, content = { content(scope) } @@ -142,9 +147,10 @@ object ZashiButtonDefaults { ) = ZashiButtonColors( containerColor = containerColor, contentColor = contentColor, + borderColor = Color.Unspecified, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, - borderColor = Color.Unspecified + disabledBorderColor = Color.Unspecified ) @Composable @@ -156,9 +162,10 @@ object ZashiButtonDefaults { ) = ZashiButtonColors( containerColor = containerColor, contentColor = contentColor, + borderColor = Color.Unspecified, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, - borderColor = Color.Unspecified + disabledBorderColor = Color.Unspecified ) @Composable @@ -170,9 +177,10 @@ object ZashiButtonDefaults { ) = ZashiButtonColors( containerColor = containerColor, contentColor = contentColor, + borderColor = Color.Unspecified, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, - borderColor = Color.Unspecified + disabledBorderColor = Color.Unspecified ) @Composable @@ -187,7 +195,8 @@ object ZashiButtonDefaults { contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, - borderColor = borderColor + borderColor = borderColor, + disabledBorderColor = Color.Unspecified ) } @@ -195,15 +204,16 @@ object ZashiButtonDefaults { data class ZashiButtonColors( val containerColor: Color, val contentColor: Color, + val borderColor: Color, val disabledContainerColor: Color, val disabledContentColor: Color, - val borderColor: Color, + val disabledBorderColor: Color, ) @Immutable data class ButtonState( val text: StringResource, - val leadingIconVector: Painter? = null, + @DrawableRes val icon: Int? = null, val isEnabled: Boolean = true, val isLoading: Boolean = false, val onClick: () -> Unit = {}, @@ -239,7 +249,7 @@ private fun PrimaryWithIconPreview() = ZashiButton( modifier = Modifier.fillMaxWidth(), text = "Primary", - leadingIcon = painterResource(id = android.R.drawable.ic_secure), + icon = android.R.drawable.ic_secure, onClick = {}, ) } diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiCheckbox.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiCheckbox.kt new file mode 100644 index 000000000..8bfb06b99 --- /dev/null +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/component/ZashiCheckbox.kt @@ -0,0 +1,128 @@ +package co.electriccoin.zcash.ui.design.component + +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +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.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.StringResource +import co.electriccoin.zcash.ui.design.util.getValue +import co.electriccoin.zcash.ui.design.util.stringRes + +@Composable +fun ZashiCheckbox( + text: StringResource, + isChecked: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + ZashiCheckbox( + state = + CheckboxState( + text = text, + isChecked = isChecked, + onClick = onClick, + ), + modifier = modifier, + ) +} + +@Composable +fun ZashiCheckbox( + state: CheckboxState, + modifier: Modifier = Modifier, +) { + Row( + modifier = + modifier + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = state.onClick) + .padding(vertical = 12.dp) + ) { + Box { + Image( + painter = painterResource(R.drawable.ic_zashi_checkbox), + contentDescription = "" + ) + + androidx.compose.animation.AnimatedVisibility( + visible = state.isChecked, + enter = + scaleIn( + spring( + stiffness = Spring.StiffnessMedium, + dampingRatio = Spring.DampingRatioMediumBouncy + ) + ), + exit = + scaleOut( + spring( + stiffness = Spring.StiffnessHigh, + dampingRatio = Spring.DampingRatioMediumBouncy + ) + ) + ) { + Image( + painter = painterResource(R.drawable.ic_zashi_checkbox_checked), + contentDescription = "" + ) + } + } + + Spacer(Modifier.width(ZashiDimensions.Spacing.spacingMd)) + + Text( + text = state.text.getValue(), + style = ZashiTypography.textSm, + fontWeight = FontWeight.Medium, + color = ZashiColors.Text.textPrimary, + ) + } +} + +data class CheckboxState( + val text: StringResource, + val isChecked: Boolean, + val onClick: () -> Unit, +) + +@PreviewScreens +@Composable +private fun ZashiCheckboxPreview() = + ZcashTheme { + var isChecked by remember { mutableStateOf(false) } + BlankSurface { + ZashiCheckbox( + state = + CheckboxState( + text = stringRes("title"), + isChecked = isChecked, + onClick = { isChecked = isChecked.not() } + ) + ) + } + } diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/newcomponent/PreviewScreens.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/newcomponent/PreviewScreens.kt index 6d9945447..38ab9e04f 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/newcomponent/PreviewScreens.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/newcomponent/PreviewScreens.kt @@ -1,6 +1,7 @@ package co.electriccoin.zcash.ui.design.newcomponent import android.content.res.Configuration +import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import kotlin.annotation.AnnotationRetention.SOURCE @@ -8,3 +9,15 @@ import kotlin.annotation.AnnotationRetention.SOURCE @Preview(name = "2: Dark preview", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Retention(SOURCE) annotation class PreviewScreens + +@Preview(name = "1: Light preview", showBackground = true) +@Preview(name = "2: Light preview small", showBackground = true, device = Devices.NEXUS_5) +@Preview(name = "3: Dark preview", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview( + name = "4: Dark preview small", + showBackground = true, + uiMode = Configuration.UI_MODE_NIGHT_YES, + device = Devices.NEXUS_5 +) +@Retention(SOURCE) +annotation class PreviewScreenSizes diff --git a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/util/ScaffoldPadding.kt b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/util/ScaffoldPadding.kt index 5d5255a36..e35ffef62 100644 --- a/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/util/ScaffoldPadding.kt +++ b/ui-design-lib/src/main/java/co/electriccoin/zcash/ui/design/util/ScaffoldPadding.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions @Stable @@ -14,3 +15,11 @@ fun Modifier.scaffoldPadding(paddingValues: PaddingValues) = start = ZashiDimensions.Spacing.spacing3xl, end = ZashiDimensions.Spacing.spacing3xl ) + +fun Modifier.scaffoldScrollPadding(paddingValues: PaddingValues) = + this.padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + ZashiDimensions.Spacing.spacing3xl, + start = 4.dp, + end = 4.dp + ) diff --git a/ui-design-lib/src/main/res/ui/common/drawable-night/ic_zashi_checkbox.xml b/ui-design-lib/src/main/res/ui/common/drawable-night/ic_zashi_checkbox.xml new file mode 100644 index 000000000..015297bb0 --- /dev/null +++ b/ui-design-lib/src/main/res/ui/common/drawable-night/ic_zashi_checkbox.xml @@ -0,0 +1,14 @@ + + + + diff --git a/ui-design-lib/src/main/res/ui/common/drawable-night/ic_zashi_checkbox_checked.xml b/ui-design-lib/src/main/res/ui/common/drawable-night/ic_zashi_checkbox_checked.xml new file mode 100644 index 000000000..686ab6893 --- /dev/null +++ b/ui-design-lib/src/main/res/ui/common/drawable-night/ic_zashi_checkbox_checked.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/ui-design-lib/src/main/res/ui/common/drawable/ic_zashi_checkbox.xml b/ui-design-lib/src/main/res/ui/common/drawable/ic_zashi_checkbox.xml new file mode 100644 index 000000000..bf5418819 --- /dev/null +++ b/ui-design-lib/src/main/res/ui/common/drawable/ic_zashi_checkbox.xml @@ -0,0 +1,14 @@ + + + + diff --git a/ui-design-lib/src/main/res/ui/common/drawable/ic_zashi_checkbox_checked.xml b/ui-design-lib/src/main/res/ui/common/drawable/ic_zashi_checkbox_checked.xml new file mode 100644 index 000000000..3819befdc --- /dev/null +++ b/ui-design-lib/src/main/res/ui/common/drawable/ic_zashi_checkbox_checked.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/ui-lib/build.gradle.kts b/ui-lib/build.gradle.kts index 4f8494a7a..8380d9d12 100644 --- a/ui-lib/build.gradle.kts +++ b/ui-lib/build.gradle.kts @@ -48,7 +48,6 @@ android { "src/main/res/ui/home", "src/main/res/ui/choose_server", "src/main/res/ui/integrations", - "src/main/res/ui/new_wallet_recovery", "src/main/res/ui/onboarding", "src/main/res/ui/payment_request", "src/main/res/ui/qr_code", @@ -62,7 +61,7 @@ android { "src/main/res/ui/send", "src/main/res/ui/send_confirmation", "src/main/res/ui/settings", - "src/main/res/ui/support", + "src/main/res/ui/feedback", "src/main/res/ui/update", "src/main/res/ui/update_contact", "src/main/res/ui/wallet_address", diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/view/AboutViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/view/AboutViewTest.kt index f43352062..13ecc666b 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/view/AboutViewTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/view/AboutViewTest.kt @@ -27,20 +27,14 @@ class AboutViewTest { assertEquals(0, testSetup.getOnBackCount()) composeTestRule - .onNodeWithText( - getStringResource(R.string.back_navigation), + .onNodeWithContentDescription( + getStringResource(R.string.back_navigation_content_description), ignoreCase = true ) .also { it.assertExists() } - composeTestRule.onNodeWithContentDescription( - label = getStringResource(R.string.zcash_logo_content_description) - ).also { - it.assertExists() - } - composeTestRule.onNodeWithText(getStringResource(R.string.about_description)).also { it.assertExists() } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/view/AboutViewTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/view/AboutViewTestSetup.kt index 2b07cd02b..18a74d0f9 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/view/AboutViewTestSetup.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/about/view/AboutViewTestSetup.kt @@ -30,7 +30,6 @@ class AboutViewTestSetup( snackbarHostState = SnackbarHostState(), topAppBarSubTitleState = TopAppBarSubTitleState.None, versionInfo = versionInfo, - onWhatsNew = {} ) } } diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataViewTest.kt index f8c907c7e..d555e6982 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataViewTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataViewTest.kt @@ -53,12 +53,6 @@ class ExportPrivateDataViewTest : UiTestPrerequisites() { it.assertExists() it.assertIsDisplayed() } - - composeTestRule.onNodeWithTag(ExportPrivateDataScreenTag.ADDITIONAL_TEXT_TAG).also { - it.performScrollTo() - it.assertExists() - it.assertIsDisplayed() - } } @Test diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryTestSetup.kt deleted file mode 100644 index 298a115d1..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryTestSetup.kt +++ /dev/null @@ -1,47 +0,0 @@ -package co.electriccoin.zcash.ui.screen.newwalletrecovery.view - -import androidx.compose.runtime.Composable -import androidx.compose.ui.test.junit4.ComposeContentTestRule -import cash.z.ecc.sdk.fixture.PersistableWalletFixture -import co.electriccoin.zcash.ui.common.model.VersionInfo -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import java.util.concurrent.atomic.AtomicInteger - -class NewWalletRecoveryTestSetup( - private val composeTestRule: ComposeContentTestRule, - private val versionInfo: VersionInfo, -) { - private val onBirthdayCopyCount = AtomicInteger(0) - - private val onCompleteCallbackCount = AtomicInteger(0) - - fun getOnBirthdayCopyCount(): Int { - composeTestRule.waitForIdle() - return onBirthdayCopyCount.get() - } - - fun getOnCompleteCount(): Int { - composeTestRule.waitForIdle() - return onCompleteCallbackCount.get() - } - - @Composable - @Suppress("TestFunctionName") - fun DefaultContent() { - ZcashTheme { - NewWalletRecovery( - PersistableWalletFixture.new(), - onSeedCopy = { /* Not tested - debug mode feature only */ }, - onBirthdayCopy = { onBirthdayCopyCount.incrementAndGet() }, - onComplete = { onCompleteCallbackCount.incrementAndGet() }, - versionInfo = versionInfo, - ) - } - } - - fun setDefaultContent() { - composeTestRule.setContent { - DefaultContent() - } - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryViewTest.kt deleted file mode 100644 index 848eb299e..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryViewTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -package co.electriccoin.zcash.ui.screen.newwalletrecovery.view - -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollTo -import androidx.test.filters.MediumTest -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY -import co.electriccoin.zcash.ui.design.component.CommonTag -import co.electriccoin.zcash.ui.fixture.VersionInfoFixture -import co.electriccoin.zcash.ui.test.getStringResource -import org.junit.Rule -import kotlin.test.Test -import kotlin.test.assertEquals - -class NewWalletRecoveryViewTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createComposeRule() - - private fun newTestSetup(): NewWalletRecoveryTestSetup { - return NewWalletRecoveryTestSetup( - composeTestRule, - VersionInfoFixture.new() - ).apply { - setDefaultContent() - } - } - - @Test - @MediumTest - fun default_ui_state_test() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - assertEquals(0, testSetup.getOnCompleteCount()) - - composeTestRule.onNodeWithContentDescription( - label = getStringResource(R.string.zcash_logo_content_description) - ).also { - it.assertExists() - } - - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_recovery_header)).also { - it.assertExists() - } - - composeTestRule.onNodeWithText(getStringResource(R.string.new_wallet_recovery_description)).also { - it.assertExists() - } - - composeTestRule.onNodeWithTag(CommonTag.CHIP_LAYOUT).also { - it.performScrollTo() - it.assertExists() - } - - composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also { - it.performScrollTo() - it.assertExists() - } - - composeTestRule - .onNodeWithText( - getStringResource(R.string.new_wallet_recovery_button_finished), - ignoreCase = true - ) - .also { - it.performScrollTo() - it.assertExists() - } - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - assertEquals(0, testSetup.getOnCompleteCount()) - } - - @Test - @MediumTest - fun copy_birthday_to_clipboard_content_test() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - - composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also { - it.performScrollTo() - it.performClick() - } - - assertEquals(1, testSetup.getOnBirthdayCopyCount()) - } - - @Test - @MediumTest - fun click_finish_test() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - assertEquals(0, testSetup.getOnCompleteCount()) - - composeTestRule.onNodeWithText( - text = getStringResource(R.string.new_wallet_recovery_button_finished), - ignoreCase = true - ).also { - it.performScrollTo() - it.performClick() - } - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - assertEquals(1, testSetup.getOnCompleteCount()) - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryViewsSecuredScreenTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryViewsSecuredScreenTest.kt deleted file mode 100644 index 565c8f1ef..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryViewsSecuredScreenTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package co.electriccoin.zcash.ui.screen.newwalletrecovery.view - -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.ComposeContentTestRule -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.filters.MediumTest -import cash.z.ecc.sdk.fixture.PersistableWalletFixture -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity -import co.electriccoin.zcash.ui.common.compose.ScreenSecurity -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.fixture.VersionInfoFixture -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals - -class NewWalletRecoveryViewsSecuredScreenTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createComposeRule() - - private fun newTestSetup() = - TestSetup(composeTestRule).apply { - setContentView() - } - - @Test - @MediumTest - fun acquireScreenSecurity() = - runTest { - val testSetup = newTestSetup() - - assertEquals(1, testSetup.getSecureScreenCount()) - } - - private class TestSetup(private val composeTestRule: ComposeContentTestRule) { - private val screenSecurity = ScreenSecurity() - - fun getSecureScreenCount() = screenSecurity.referenceCount.value - - fun setContentView() { - composeTestRule.setContent { - CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) { - ZcashTheme { - NewWalletRecovery( - PersistableWalletFixture.new(), - onSeedCopy = {}, - onBirthdayCopy = {}, - onComplete = {}, - versionInfo = VersionInfoFixture.new() - ) - } - } - } - } - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewAndroidTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewAndroidTest.kt index 5aa0b549b..e3f7a8046 100644 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewAndroidTest.kt +++ b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreViewAndroidTest.kt @@ -163,7 +163,7 @@ private fun copyToClipboard( val clipboardManager = context.getSystemService(ClipboardManager::class.java) val data = ClipData.newPlainText( - context.getString(R.string.new_wallet_recovery_seed_clipboard_tag), + "TAG", text ) clipboardManager.setPrimaryClip(data) diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryRecoveryViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryRecoveryViewTest.kt deleted file mode 100644 index 0bbc35de6..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryRecoveryViewTest.kt +++ /dev/null @@ -1,135 +0,0 @@ -package co.electriccoin.zcash.ui.screen.seedrecovery.view - -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performScrollTo -import androidx.test.filters.MediumTest -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY -import co.electriccoin.zcash.ui.design.component.CommonTag -import co.electriccoin.zcash.ui.fixture.VersionInfoFixture -import co.electriccoin.zcash.ui.test.getStringResource -import org.junit.Rule -import kotlin.test.Test -import kotlin.test.assertEquals - -class SeedRecoveryRecoveryViewTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createComposeRule() - - private fun newTestSetup(): SeedRecoveryTestSetup { - return SeedRecoveryTestSetup( - composeTestRule, - VersionInfoFixture.new() - ).apply { - setDefaultContent() - } - } - - @Test - @MediumTest - fun default_ui_state_test() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - assertEquals(0, testSetup.getOnCompleteCount()) - assertEquals(0, testSetup.getOnBackCount()) - - composeTestRule.onNodeWithContentDescription(getStringResource(R.string.back_navigation_content_description)) - .also { - it.assertExists() - } - - composeTestRule.onNodeWithContentDescription( - label = getStringResource(R.string.zcash_logo_content_description) - ).also { - it.assertExists() - } - - composeTestRule.onNodeWithText(getStringResource(R.string.seed_recovery_header)).also { - it.assertExists() - } - - composeTestRule.onNodeWithText(getStringResource(R.string.seed_recovery_description)).also { - it.assertExists() - } - - composeTestRule.onNodeWithTag(CommonTag.CHIP_LAYOUT).also { - it.performScrollTo() - it.assertExists() - } - - composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also { - it.performScrollTo() - it.assertExists() - } - - composeTestRule - .onNodeWithText( - getStringResource(R.string.seed_recovery_button_finished), - ignoreCase = true - ) - .also { - it.performScrollTo() - it.assertExists() - } - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - assertEquals(0, testSetup.getOnCompleteCount()) - assertEquals(0, testSetup.getOnBackCount()) - } - - @Test - @MediumTest - fun back_test() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnBackCount()) - - composeTestRule.onNodeWithContentDescription(getStringResource(R.string.back_navigation_content_description)) - .also { - it.performClick() - } - - assertEquals(1, testSetup.getOnBackCount()) - } - - @Test - @MediumTest - fun copy_birthday_to_clipboard_content_test() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - - composeTestRule.onNodeWithTag(WALLET_BIRTHDAY).also { - it.performScrollTo() - it.performClick() - } - - assertEquals(1, testSetup.getOnBirthdayCopyCount()) - } - - @Test - @MediumTest - fun click_finish_test() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - assertEquals(0, testSetup.getOnCompleteCount()) - - composeTestRule.onNodeWithText( - text = getStringResource(R.string.seed_recovery_button_finished), - ignoreCase = true - ).also { - it.performScrollTo() - it.performClick() - } - - assertEquals(0, testSetup.getOnBirthdayCopyCount()) - assertEquals(1, testSetup.getOnCompleteCount()) - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryRecoveryViewsSecuredScreenTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryRecoveryViewsSecuredScreenTest.kt deleted file mode 100644 index ae94e9d56..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryRecoveryViewsSecuredScreenTest.kt +++ /dev/null @@ -1,60 +0,0 @@ -package co.electriccoin.zcash.ui.screen.seedrecovery.view - -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.test.junit4.ComposeContentTestRule -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.test.filters.MediumTest -import cash.z.ecc.sdk.fixture.PersistableWalletFixture -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.common.compose.LocalScreenSecurity -import co.electriccoin.zcash.ui.common.compose.ScreenSecurity -import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.fixture.VersionInfoFixture -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.Test -import kotlin.test.assertEquals - -class SeedRecoveryRecoveryViewsSecuredScreenTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createComposeRule() - - private fun newTestSetup() = - TestSetup(composeTestRule).apply { - setContentView() - } - - @Test - @MediumTest - fun acquireScreenSecurity() = - runTest { - val testSetup = newTestSetup() - - assertEquals(1, testSetup.getSecureScreenCount()) - } - - private class TestSetup(private val composeTestRule: ComposeContentTestRule) { - private val screenSecurity = ScreenSecurity() - - fun getSecureScreenCount() = screenSecurity.referenceCount.value - - fun setContentView() { - composeTestRule.setContent { - CompositionLocalProvider(LocalScreenSecurity provides screenSecurity) { - ZcashTheme { - SeedRecovery( - PersistableWalletFixture.new(), - onBack = {}, - onBirthdayCopy = {}, - onDone = {}, - onSeedCopy = {}, - topAppBarSubTitleState = TopAppBarSubTitleState.None, - versionInfo = VersionInfoFixture.new(), - ) - } - } - } - } - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryTestSetup.kt deleted file mode 100644 index b7517dc4a..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryTestSetup.kt +++ /dev/null @@ -1,57 +0,0 @@ -package co.electriccoin.zcash.ui.screen.seedrecovery.view - -import androidx.compose.runtime.Composable -import androidx.compose.ui.test.junit4.ComposeContentTestRule -import cash.z.ecc.sdk.fixture.PersistableWalletFixture -import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState -import co.electriccoin.zcash.ui.common.model.VersionInfo -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import java.util.concurrent.atomic.AtomicInteger - -class SeedRecoveryTestSetup( - private val composeTestRule: ComposeContentTestRule, - private val versionInfo: VersionInfo, -) { - private val onBirthdayCopyCount = AtomicInteger(0) - - private val onCompleteCallbackCount = AtomicInteger(0) - - private val onBackCount = AtomicInteger(0) - - fun getOnBirthdayCopyCount(): Int { - composeTestRule.waitForIdle() - return onBirthdayCopyCount.get() - } - - fun getOnCompleteCount(): Int { - composeTestRule.waitForIdle() - return onCompleteCallbackCount.get() - } - - fun getOnBackCount(): Int { - composeTestRule.waitForIdle() - return onBackCount.get() - } - - @Composable - @Suppress("TestFunctionName") - fun DefaultContent() { - ZcashTheme { - SeedRecovery( - PersistableWalletFixture.new(), - onBack = { onBackCount.incrementAndGet() }, - onBirthdayCopy = { onBirthdayCopyCount.incrementAndGet() }, - onDone = { onCompleteCallbackCount.incrementAndGet() }, - onSeedCopy = { /* Not tested - debug mode feature only */ }, - topAppBarSubTitleState = TopAppBarSubTitleState.None, - versionInfo = versionInfo, - ) - } - } - - fun setDefaultContent() { - composeTestRule.setContent { - DefaultContent() - } - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewIntegrationTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewIntegrationTest.kt deleted file mode 100644 index 95c10f73c..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewIntegrationTest.kt +++ /dev/null @@ -1,85 +0,0 @@ -package co.electriccoin.zcash.ui.screen.support.view - -import androidx.compose.ui.test.junit4.StateRestorationTester -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.test.filters.MediumTest -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.test.getStringResource -import co.electriccoin.zcash.ui.test.getStringResourceWithArgs -import org.junit.Rule -import org.junit.Test -import kotlin.test.Ignore - -class SupportViewIntegrationTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createComposeRule() - - @Test - @MediumTest - fun message_state_restoration() { - val restorationTester = StateRestorationTester(composeTestRule) - val testSetup = newTestSetup() - - restorationTester.setContent { - ZcashTheme { - testSetup.DefaultContent() - } - } - - composeTestRule.onNodeWithText("I can haz cheezburger?").also { - it.assertDoesNotExist() - } - - composeTestRule.onNodeWithText(getStringResource(R.string.support_hint)).also { - it.performTextInput("I can haz cheezburger?") - } - - composeTestRule.onNodeWithText("I can haz cheezburger?").also { - it.assertExists() - } - - restorationTester.emulateSavedInstanceStateRestore() - - composeTestRule.onNodeWithText("I can haz cheezburger?").also { - it.assertExists() - } - } - - @Test - @MediumTest - @Ignore("Will be updated as part of #1275") - fun dialog_state_restoration() { - val restorationTester = StateRestorationTester(composeTestRule) - val testSetup = newTestSetup() - - restorationTester.setContent { - testSetup.DefaultContent() - } - - composeTestRule.onNodeWithText("I can haz cheezburger?").also { - it.assertDoesNotExist() - } - - composeTestRule.onNodeWithText(getStringResource(R.string.support_send), ignoreCase = true).also { - it.performClick() - } - - restorationTester.emulateSavedInstanceStateRestore() - - val dialogContent = - getStringResourceWithArgs( - R.string.support_confirmation_explanation, - getStringResource(R.string.app_name) - ) - composeTestRule.onNodeWithText(dialogContent).also { - it.assertExists() - } - } - - private fun newTestSetup() = SupportViewTestSetup(composeTestRule) -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewTest.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewTest.kt deleted file mode 100644 index b62931ab4..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewTest.kt +++ /dev/null @@ -1,134 +0,0 @@ -package co.electriccoin.zcash.ui.screen.support.view - -import androidx.compose.ui.test.junit4.ComposeContentTestRule -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onNodeWithContentDescription -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import androidx.compose.ui.test.performTextInput -import androidx.test.filters.MediumTest -import co.electriccoin.zcash.test.UiTestPrerequisites -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.test.getStringResource -import co.electriccoin.zcash.ui.test.getStringResourceWithArgs -import org.junit.Assert.assertEquals -import org.junit.Rule -import org.junit.Test -import kotlin.test.Ignore - -class SupportViewTest : UiTestPrerequisites() { - @get:Rule - val composeTestRule = createComposeRule() - - companion object { - internal val DEFAULT_MESSAGE = "I can haz cheezburger?" - } - - @Test - @MediumTest - fun back() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnBackCount()) - - composeTestRule.clickBack() - - assertEquals(1, testSetup.getOnBackCount()) - } - - @Test - @MediumTest - @Ignore("Will be updated as part of #1275") - fun send_shows_dialog() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnSendCount()) - assertEquals(null, testSetup.getSendMessage()) - - composeTestRule.typeMessage() - composeTestRule.clickSend() - - assertEquals(0, testSetup.getOnSendCount()) - - val dialogContent = - getStringResourceWithArgs( - R.string.support_confirmation_explanation, - getStringResource(R.string.app_name) - ) - composeTestRule.onNodeWithText(dialogContent).also { - it.assertExists() - } - } - - @Test - @MediumTest - @Ignore("Will be updated as part of #1275") - fun dialog_confirm_sends() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnSendCount()) - assertEquals(null, testSetup.getSendMessage()) - - composeTestRule.typeMessage() - composeTestRule.clickSend() - - composeTestRule.onNodeWithText(getStringResource(R.string.support_confirmation_dialog_ok)).also { - it.performClick() - } - - assertEquals(1, testSetup.getOnSendCount()) - assertEquals(DEFAULT_MESSAGE, testSetup.getSendMessage()) - } - - @Test - @MediumTest - @Ignore("Will be updated as part of #1275") - fun dialog_cancel() { - val testSetup = newTestSetup() - - assertEquals(0, testSetup.getOnSendCount()) - assertEquals(null, testSetup.getSendMessage()) - - composeTestRule.typeMessage() - composeTestRule.clickSend() - - composeTestRule.onNodeWithText(getStringResource(R.string.support_confirmation_dialog_cancel)).also { - it.performClick() - } - - val dialogContent = - getStringResourceWithArgs( - R.string.support_confirmation_explanation, - getStringResource(R.string.app_name) - ) - composeTestRule.onNodeWithText(dialogContent).also { - it.assertDoesNotExist() - } - - assertEquals(0, testSetup.getOnSendCount()) - assertEquals(0, testSetup.getOnBackCount()) - } - - private fun newTestSetup() = - SupportViewTestSetup(composeTestRule).apply { - setDefaultContent() - } -} - -private fun ComposeContentTestRule.clickBack() { - onNodeWithContentDescription(getStringResource(R.string.back_navigation_content_description)).also { - it.performClick() - } -} - -private fun ComposeContentTestRule.clickSend() { - onNodeWithText(getStringResource(R.string.support_send), ignoreCase = true).also { - it.performClick() - } -} - -private fun ComposeContentTestRule.typeMessage() { - onNodeWithText(getStringResource(R.string.support_hint)).also { - it.performTextInput(SupportViewTest.DEFAULT_MESSAGE) - } -} diff --git a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewTestSetup.kt b/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewTestSetup.kt deleted file mode 100644 index deb2a4da2..000000000 --- a/ui-lib/src/androidTest/java/co/electriccoin/zcash/ui/screen/support/view/SupportViewTestSetup.kt +++ /dev/null @@ -1,61 +0,0 @@ -package co.electriccoin.zcash.ui.screen.support.view - -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.ui.test.junit4.ComposeContentTestRule -import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.atomic.AtomicReference - -class SupportViewTestSetup(private val composeTestRule: ComposeContentTestRule) { - private val onBackCount = AtomicInteger(0) - - private val onSendCount = AtomicInteger(0) - - private val onSendMessage = AtomicReference(null) - - fun getOnBackCount(): Int { - composeTestRule.waitForIdle() - return onBackCount.get() - } - - fun getOnSendCount(): Int { - composeTestRule.waitForIdle() - return onSendCount.get() - } - - fun getSendMessage(): String? { - composeTestRule.waitForIdle() - return onSendMessage.get() - } - - // TODO [#1275]: Improve SupportView UI tests - // TODO [#1275]: https://github.com/Electric-Coin-Company/zashi-android/issues/1275 - - @Composable - @Suppress("TestFunctionName") - fun DefaultContent() { - Support( - isShowingDialog = false, - setShowDialog = {}, - onBack = { - onBackCount.incrementAndGet() - }, - onSend = { - onSendCount.incrementAndGet() - onSendMessage.set(it) - }, - snackbarHostState = SnackbarHostState(), - topAppBarSubTitleState = TopAppBarSubTitleState.None, - ) - } - - fun setDefaultContent() { - composeTestRule.setContent { - ZcashTheme { - DefaultContent() - } - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt index 263c0fe8a..4415f52f1 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/UseCaseModule.kt @@ -4,20 +4,24 @@ import co.electriccoin.zcash.ui.common.usecase.CopyToClipboardUseCase import co.electriccoin.zcash.ui.common.usecase.DeleteAddressBookUseCase import co.electriccoin.zcash.ui.common.usecase.DeleteContactUseCase import co.electriccoin.zcash.ui.common.usecase.GetAddressesUseCase +import co.electriccoin.zcash.ui.common.usecase.GetBackupPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetContactByAddressUseCase import co.electriccoin.zcash.ui.common.usecase.GetPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.GetSelectedEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.GetSpendingKeyUseCase +import co.electriccoin.zcash.ui.common.usecase.GetSupportUseCase import co.electriccoin.zcash.ui.common.usecase.GetSynchronizerUseCase import co.electriccoin.zcash.ui.common.usecase.GetTransparentAddressUseCase import co.electriccoin.zcash.ui.common.usecase.IsCoinbaseAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.IsFlexaAvailableUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveAddressBookContactsUseCase +import co.electriccoin.zcash.ui.common.usecase.ObserveBackupPersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveContactByAddressUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveContactPickedUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveFastestServersUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveIsFlexaAvailableUseCase +import co.electriccoin.zcash.ui.common.usecase.ObservePersistableWalletUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSelectedEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveSynchronizerUseCase import co.electriccoin.zcash.ui.common.usecase.ObserveWalletStateUseCase @@ -25,6 +29,8 @@ 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.SaveContactUseCase +import co.electriccoin.zcash.ui.common.usecase.SendEmailUseCase +import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase import co.electriccoin.zcash.ui.common.usecase.SensitiveSettingsVisibleUseCase import co.electriccoin.zcash.ui.common.usecase.ShareImageUseCase import co.electriccoin.zcash.ui.common.usecase.UpdateContactUseCase @@ -34,43 +40,50 @@ import co.electriccoin.zcash.ui.common.usecase.ValidateEndpointUseCase import co.electriccoin.zcash.ui.common.usecase.Zip321BuildUriUseCase import co.electriccoin.zcash.ui.common.usecase.Zip321ParseUriValidationUseCase import co.electriccoin.zcash.ui.common.usecase.Zip321ProposalFromUriUseCase +import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf import org.koin.dsl.module val useCaseModule = module { - singleOf(::ObserveSynchronizerUseCase) - singleOf(::GetSynchronizerUseCase) - singleOf(::ObserveFastestServersUseCase) - singleOf(::ObserveSelectedEndpointUseCase) - singleOf(::RefreshFastestServersUseCase) - singleOf(::PersistEndpointUseCase) - singleOf(::ValidateEndpointUseCase) - singleOf(::GetPersistableWalletUseCase) - singleOf(::GetSelectedEndpointUseCase) - singleOf(::ObserveConfigurationUseCase) - singleOf(::RescanBlockchainUseCase) - singleOf(::GetTransparentAddressUseCase) - singleOf(::ObserveAddressBookContactsUseCase) - singleOf(::DeleteAddressBookUseCase) - singleOf(::ValidateContactAddressUseCase) - singleOf(::ValidateContactNameUseCase) - singleOf(::SaveContactUseCase) - singleOf(::UpdateContactUseCase) - singleOf(::DeleteContactUseCase) - singleOf(::GetContactByAddressUseCase) - singleOf(::ObserveContactByAddressUseCase) + factoryOf(::ObserveSynchronizerUseCase) + factoryOf(::GetSynchronizerUseCase) + factoryOf(::ObserveFastestServersUseCase) + factoryOf(::ObserveSelectedEndpointUseCase) + factoryOf(::RefreshFastestServersUseCase) + factoryOf(::PersistEndpointUseCase) + factoryOf(::ValidateEndpointUseCase) + factoryOf(::GetPersistableWalletUseCase) + factoryOf(::GetSelectedEndpointUseCase) + factoryOf(::ObserveConfigurationUseCase) + factoryOf(::RescanBlockchainUseCase) + factoryOf(::GetTransparentAddressUseCase) + factoryOf(::ObserveAddressBookContactsUseCase) + factoryOf(::DeleteAddressBookUseCase) + factoryOf(::ValidateContactAddressUseCase) + factoryOf(::ValidateContactNameUseCase) + factoryOf(::SaveContactUseCase) + factoryOf(::UpdateContactUseCase) + factoryOf(::DeleteContactUseCase) + factoryOf(::GetContactByAddressUseCase) + factoryOf(::ObserveContactByAddressUseCase) singleOf(::ObserveContactPickedUseCase) - singleOf(::GetAddressesUseCase) - singleOf(::CopyToClipboardUseCase) - singleOf(::IsFlexaAvailableUseCase) - singleOf(::ObserveIsFlexaAvailableUseCase) - singleOf(::ShareImageUseCase) - singleOf(::Zip321BuildUriUseCase) - singleOf(::Zip321ProposalFromUriUseCase) - singleOf(::Zip321ParseUriValidationUseCase) - singleOf(::ObserveWalletStateUseCase) - singleOf(::IsCoinbaseAvailableUseCase) - singleOf(::GetSpendingKeyUseCase) - singleOf(::SensitiveSettingsVisibleUseCase) + factoryOf(::GetAddressesUseCase) + factoryOf(::CopyToClipboardUseCase) + factoryOf(::ShareImageUseCase) + factoryOf(::Zip321BuildUriUseCase) + factoryOf(::Zip321ProposalFromUriUseCase) + factoryOf(::Zip321ParseUriValidationUseCase) + factoryOf(::ObserveWalletStateUseCase) + factoryOf(::IsCoinbaseAvailableUseCase) + factoryOf(::GetSpendingKeyUseCase) + factoryOf(::ObservePersistableWalletUseCase) + factoryOf(::ObserveBackupPersistableWalletUseCase) + factoryOf(::GetBackupPersistableWalletUseCase) + factoryOf(::GetSupportUseCase) + factoryOf(::SendEmailUseCase) + factoryOf(::SendSupportEmailUseCase) + factoryOf(::IsFlexaAvailableUseCase) + factoryOf(::ObserveIsFlexaAvailableUseCase) + factoryOf(::SensitiveSettingsVisibleUseCase) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt index 18deb6607..260061758 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/di/ViewModelModule.kt @@ -11,6 +11,7 @@ import co.electriccoin.zcash.ui.screen.advancedsettings.viewmodel.AdvancedSettin import co.electriccoin.zcash.ui.screen.chooseserver.ChooseServerViewModel import co.electriccoin.zcash.ui.screen.contact.viewmodel.AddContactViewModel import co.electriccoin.zcash.ui.screen.contact.viewmodel.UpdateContactViewModel +import co.electriccoin.zcash.ui.screen.feedback.viewmodel.FeedbackViewModel import co.electriccoin.zcash.ui.screen.integrations.viewmodel.IntegrationsViewModel import co.electriccoin.zcash.ui.screen.onboarding.viewmodel.OnboardingViewModel import co.electriccoin.zcash.ui.screen.paymentrequest.viewmodel.PaymentRequestViewModel @@ -21,6 +22,8 @@ import co.electriccoin.zcash.ui.screen.restore.viewmodel.RestoreViewModel import co.electriccoin.zcash.ui.screen.restoresuccess.viewmodel.RestoreSuccessViewModel import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs import co.electriccoin.zcash.ui.screen.scan.viewmodel.ScanViewModel +import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs +import co.electriccoin.zcash.ui.screen.seed.viewmodel.SeedViewModel import co.electriccoin.zcash.ui.screen.send.SendViewModel import co.electriccoin.zcash.ui.screen.sendconfirmation.viewmodel.CreateTransactionsViewModel import co.electriccoin.zcash.ui.screen.settings.viewmodel.ScreenBrightnessViewModel @@ -89,4 +92,13 @@ val viewModelModule = } viewModelOf(::IntegrationsViewModel) viewModelOf(::SendViewModel) + viewModel { (args: SeedNavigationArgs) -> + SeedViewModel( + observePersistableWallet = get(), + args = args, + walletRepository = get(), + observeBackupPersistableWallet = get(), + ) + } + viewModelOf(::FeedbackViewModel) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt index e687d1e99..727e7197e 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/MainActivity.kt @@ -47,10 +47,11 @@ import co.electriccoin.zcash.ui.screen.authentication.RETRY_TRIGGER_DELAY import co.electriccoin.zcash.ui.screen.authentication.WrapAuthentication import co.electriccoin.zcash.ui.screen.authentication.view.AnimationConstants import co.electriccoin.zcash.ui.screen.authentication.view.WelcomeAnimationAutostart -import co.electriccoin.zcash.ui.screen.newwalletrecovery.WrapNewWalletRecovery import co.electriccoin.zcash.ui.screen.onboarding.WrapOnboarding import co.electriccoin.zcash.ui.screen.onboarding.persistExistingWalletWithSeedPhrase import co.electriccoin.zcash.ui.screen.securitywarning.WrapSecurityWarning +import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs +import co.electriccoin.zcash.ui.screen.seed.WrapSeed import co.electriccoin.zcash.ui.screen.warning.viewmodel.StorageCheckViewModel import co.electriccoin.zcash.work.WorkIds import kotlinx.coroutines.delay @@ -320,9 +321,9 @@ class MainActivity : FragmentActivity() { } is SecretState.NeedsBackup -> { - WrapNewWalletRecovery( - secretState.persistableWallet, - onBackupComplete = { walletViewModel.persistOnboardingState(OnboardingState.READY) } + WrapSeed( + args = SeedNavigationArgs.NEW_WALLET, + goBackOverride = null ) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt index 4a7598217..18faeacbd 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/Navigation.kt @@ -76,6 +76,7 @@ import co.electriccoin.zcash.ui.screen.disconnected.WrapDisconnected import co.electriccoin.zcash.ui.screen.exchangerate.optin.AndroidExchangeRateOptIn import co.electriccoin.zcash.ui.screen.exchangerate.settings.AndroidSettingsExchangeRateOptIn import co.electriccoin.zcash.ui.screen.exportdata.WrapExportPrivateData +import co.electriccoin.zcash.ui.screen.feedback.WrapFeedback import co.electriccoin.zcash.ui.screen.home.WrapHome import co.electriccoin.zcash.ui.screen.integrations.WrapIntegrations import co.electriccoin.zcash.ui.screen.paymentrequest.WrapPaymentRequest @@ -85,14 +86,14 @@ import co.electriccoin.zcash.ui.screen.receive.model.ReceiveAddressType import co.electriccoin.zcash.ui.screen.request.WrapRequest import co.electriccoin.zcash.ui.screen.scan.ScanNavigationArgs import co.electriccoin.zcash.ui.screen.scan.WrapScanValidator -import co.electriccoin.zcash.ui.screen.seedrecovery.WrapSeedRecovery +import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs +import co.electriccoin.zcash.ui.screen.seed.WrapSeed import co.electriccoin.zcash.ui.screen.send.ext.toSerializableAddress import co.electriccoin.zcash.ui.screen.send.model.SendArguments import co.electriccoin.zcash.ui.screen.sendconfirmation.WrapSendConfirmation import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationArguments import co.electriccoin.zcash.ui.screen.sendconfirmation.model.SendConfirmationStage import co.electriccoin.zcash.ui.screen.settings.WrapSettings -import co.electriccoin.zcash.ui.screen.support.WrapSupport import co.electriccoin.zcash.ui.screen.update.WrapCheckForUpdate import co.electriccoin.zcash.ui.screen.warning.WrapNotEnoughSpace import co.electriccoin.zcash.ui.screen.whatsnew.WrapWhatsNew @@ -204,20 +205,16 @@ internal fun MainActivity.Navigation() { WrapChooseServer() } composable(SEED_RECOVERY) { - WrapSeedRecovery( - goBack = { - setSeedRecoveryAuthentication(false) - navController.popBackStackJustOnce(SEED_RECOVERY) - }, - onDone = { + WrapSeed( + args = SeedNavigationArgs.RECOVERY, + goBackOverride = { setSeedRecoveryAuthentication(false) - navController.popBackStackJustOnce(SEED_RECOVERY) - }, + } ) } composable(SUPPORT) { // Pop back stack won't be right if we deep link into support - WrapSupport(goBack = { navController.popBackStackJustOnce(SUPPORT) }) + WrapFeedback() } composable(DELETE_WALLET) { WrapDeleteWallet( @@ -234,7 +231,6 @@ internal fun MainActivity.Navigation() { composable(ABOUT) { WrapAbout( goBack = { navController.popBackStackJustOnce(ABOUT) }, - goWhatsNew = { navController.navigateJustOnce(WHATS_NEW) } ) } composable(WHATS_NEW) { diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/ZashiTooltip.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/ZashiTooltip.kt new file mode 100644 index 000000000..0be415e84 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/ZashiTooltip.kt @@ -0,0 +1,167 @@ +package co.electriccoin.zcash.ui.common.compose + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.shape.GenericShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.unit.dp +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens +import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.StringResource +import co.electriccoin.zcash.ui.design.util.getValue +import co.electriccoin.zcash.ui.design.util.stringRes + +@Composable +fun ZashiAnimatedTooltip( + isVisible: Boolean, + title: StringResource, + message: StringResource, + onDismissRequest: () -> Unit +) { + AnimatedVisibility( + visible = isVisible, + enter = enterTransition(), + exit = exitTransition(), + ) { + ZashiTooltip(title, message, onDismissRequest) + } +} + +@Composable +fun ZashiAnimatedTooltip( + visibleState: MutableTransitionState, + title: StringResource, + message: StringResource, + onDismissRequest: () -> Unit +) { + AnimatedVisibility( + visibleState = visibleState, + enter = enterTransition(), + exit = exitTransition(), + ) { + ZashiTooltip(title, message, onDismissRequest) + } +} + +@Composable +fun ZashiTooltip( + title: StringResource, + message: StringResource, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + showCaret: Boolean = true, +) { + Column( + modifier = modifier.padding(horizontal = 22.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (showCaret) { + Box( + modifier = + Modifier + .width(16.dp) + .height(8.dp) + .background(ZashiColors.HintTooltips.surfacePrimary, TriangleShape) + ) + } + Box( + Modifier + .fillMaxWidth() + .background(ZashiColors.HintTooltips.surfacePrimary, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onDismissRequest) + .padding(start = 12.dp, bottom = 12.dp), + ) { + Row { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + modifier = Modifier.padding(top = 12.dp), + color = ZashiColors.Text.textLight, + style = ZashiTypography.textMd, + text = title.getValue() + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + color = ZashiColors.Text.textLightSupport, + style = ZashiTypography.textSm, + text = message.getValue() + ) + } + IconButton(onClick = onDismissRequest) { + Icon( + painter = painterResource(R.drawable.ic_exchange_rate_unavailable_dialog_close), + contentDescription = "", + tint = ZashiColors.HintTooltips.defaultFg + ) + } + } + } + } +} + +@Composable +private fun exitTransition() = + fadeOut() + + scaleOut(animationSpec = spring(stiffness = Spring.StiffnessMedium)) + + slideOutVertically() + +@Composable +private fun enterTransition() = + fadeIn() + + slideInVertically(spring(stiffness = Spring.StiffnessHigh)) + + scaleIn(spring(stiffness = Spring.StiffnessMedium, dampingRatio = Spring.DampingRatioLowBouncy)) + +@PreviewScreens +@Composable +private fun Preview() = + ZcashTheme { + ZashiTooltip( + title = stringRes(R.string.exchange_rate_unavailable_title), + message = stringRes(R.string.exchange_rate_unavailable_subtitle), + onDismissRequest = {} + ) + } + +private val TriangleShape = + GenericShape { size, _ -> + + // 1) Start at the top center + moveTo(size.width / 2f, 0f) + + // 2) Draw a line to the bottom right corner + lineTo(size.width, size.height) + + // 3) Draw a line to the bottom left corner and implicitly close the shape + lineTo(0f, size.height) + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/ZashiTooltipBox.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/ZashiTooltipBox.kt new file mode 100644 index 000000000..8a7546032 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/compose/ZashiTooltipBox.kt @@ -0,0 +1,161 @@ +package co.electriccoin.zcash.ui.common.compose + +import android.content.res.Configuration +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipScope +import androidx.compose.material3.TooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.CacheDrawScope +import androidx.compose.ui.draw.DrawResult +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupPositionProvider + +@Composable +@ExperimentalMaterial3Api +fun ZashiTooltipBox( + tooltip: @Composable TooltipScope.() -> Unit, + state: TooltipState, + modifier: Modifier = Modifier, + positionProvider: PopupPositionProvider = rememberTooltipPositionProvider(), + focusable: Boolean = true, + enableUserInput: Boolean = true, + content: @Composable () -> Unit, +) { + TooltipBox( + positionProvider = positionProvider, + tooltip = tooltip, + state = state, + modifier = modifier, + focusable = focusable, + enableUserInput = enableUserInput, + content = content + ) +} + +@Composable +fun rememberTooltipPositionProvider(spacingBetweenTooltipAndAnchor: Dp = 8.dp): PopupPositionProvider { + val tooltipAnchorSpacing = + with(LocalDensity.current) { + spacingBetweenTooltipAndAnchor.roundToPx() + } + return remember(tooltipAnchorSpacing) { + object : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize + ): IntOffset { + val x = anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2 + + // Tooltip prefers to be above the anchor, + // but if this causes the tooltip to overlap with the anchor + // then we place it below the anchor + + var y = anchorBounds.bottom + tooltipAnchorSpacing + if (y + popupContentSize.height > windowSize.height) { + y = anchorBounds.top - popupContentSize.height - tooltipAnchorSpacing + } + return IntOffset(x, y) + } + } + } +} + +@ExperimentalMaterial3Api +fun CacheDrawScope.drawCaretWithPath( + density: Density, + configuration: Configuration, + containerColor: Color, + caretProperties: DpSize = DpSize(height = 8.dp, width = 16.dp), + anchorLayoutCoordinates: LayoutCoordinates? +): DrawResult { + val path = Path() + + if (anchorLayoutCoordinates != null) { + val caretHeightPx: Int + val caretWidthPx: Int + val screenWidthPx: Int + val screenHeightPx: Int + val tooltipAnchorSpacing: Int + with(density) { + caretHeightPx = caretProperties.height.roundToPx() + caretWidthPx = caretProperties.width.roundToPx() + screenWidthPx = configuration.screenWidthDp.dp.roundToPx() + screenHeightPx = configuration.screenHeightDp.dp.roundToPx() + tooltipAnchorSpacing = 4.dp.roundToPx() + } + val anchorBounds = anchorLayoutCoordinates.boundsInWindow() + val anchorLeft = anchorBounds.left + val anchorRight = anchorBounds.right + val anchorMid = (anchorRight + anchorLeft) / 2 + val anchorWidth = anchorRight - anchorLeft + val tooltipWidth = this.size.width + val tooltipHeight = this.size.height + + val isCaretTop = (anchorBounds.bottom + tooltipAnchorSpacing + tooltipHeight) <= screenHeightPx + val caretY = + if (isCaretTop) { + 0f + } else { + tooltipHeight + } + + val position = + if (anchorMid + tooltipWidth / 2 > screenWidthPx) { + val anchorMidFromRightScreenEdge = + screenWidthPx - anchorMid + val caretX = tooltipWidth - anchorMidFromRightScreenEdge + Offset(caretX, caretY) + } else { + val tooltipLeft = + anchorLeft - (this.size.width / 2 - anchorWidth / 2) + val caretX = anchorMid - maxOf(tooltipLeft, 0f) + Offset(caretX, caretY) + } + + if (isCaretTop) { + path.apply { + moveTo(x = position.x, y = position.y) + lineTo(x = position.x + caretWidthPx / 2, y = position.y) + lineTo(x = position.x, y = position.y - caretHeightPx) + lineTo(x = position.x - caretWidthPx / 2, y = position.y) + close() + } + } else { + path.apply { + moveTo(x = position.x, y = position.y) + lineTo(x = position.x + caretWidthPx / 2, y = position.y) + lineTo(x = position.x, y = position.y + caretHeightPx.toFloat()) + lineTo(x = position.x - caretWidthPx / 2, y = position.y) + close() + } + } + } + + return onDrawWithContent { + if (anchorLayoutCoordinates != null) { + drawContent() + drawPath( + path = path, + color = containerColor + ) + } + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CopyToClipboardUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CopyToClipboardUseCase.kt index 27952d1de..c1881b590 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CopyToClipboardUseCase.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/CopyToClipboardUseCase.kt @@ -3,9 +3,10 @@ package co.electriccoin.zcash.ui.common.usecase import android.content.Context import co.electriccoin.zcash.spackle.ClipboardManagerUtil -class CopyToClipboardUseCase { +class CopyToClipboardUseCase( + private val context: Context +) { operator fun invoke( - context: Context, tag: String, value: String ) = ClipboardManagerUtil.copyToClipboard( diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetBackupPersistableWalletUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetBackupPersistableWalletUseCase.kt new file mode 100644 index 000000000..2f40c85b6 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetBackupPersistableWalletUseCase.kt @@ -0,0 +1,17 @@ +package co.electriccoin.zcash.ui.common.usecase + +import co.electriccoin.zcash.ui.common.repository.WalletRepository +import co.electriccoin.zcash.ui.common.viewmodel.SecretState +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +class GetBackupPersistableWalletUseCase( + private val walletRepository: WalletRepository +) { + suspend operator fun invoke() = + walletRepository.secretState + .map { (it as? SecretState.NeedsBackup)?.persistableWallet } + .filterNotNull() + .first() +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetSupportUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetSupportUseCase.kt new file mode 100644 index 000000000..e59774f3e --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/GetSupportUseCase.kt @@ -0,0 +1,12 @@ +package co.electriccoin.zcash.ui.common.usecase + +import android.content.Context +import co.electriccoin.zcash.configuration.api.ConfigurationProvider +import co.electriccoin.zcash.ui.screen.support.model.SupportInfo + +class GetSupportUseCase( + private val context: Context, + private val androidConfigurationProvider: ConfigurationProvider +) { + suspend operator fun invoke() = SupportInfo.new(context, androidConfigurationProvider) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveBackupPersistableWalletUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveBackupPersistableWalletUseCase.kt new file mode 100644 index 000000000..46cb2c8b7 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/ObserveBackupPersistableWalletUseCase.kt @@ -0,0 +1,15 @@ +package co.electriccoin.zcash.ui.common.usecase + +import cash.z.ecc.android.sdk.model.PersistableWallet +import co.electriccoin.zcash.ui.common.repository.WalletRepository +import co.electriccoin.zcash.ui.common.viewmodel.SecretState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class ObserveBackupPersistableWalletUseCase( + private val walletRepository: WalletRepository +) { + operator fun invoke(): Flow = + walletRepository + .secretState.map { (it as? SecretState.NeedsBackup)?.persistableWallet } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SendEmailUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SendEmailUseCase.kt new file mode 100644 index 000000000..b32945410 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SendEmailUseCase.kt @@ -0,0 +1,28 @@ +package co.electriccoin.zcash.ui.common.usecase + +import android.content.Context +import android.content.Intent +import co.electriccoin.zcash.ui.design.util.StringResource +import co.electriccoin.zcash.ui.design.util.getString +import co.electriccoin.zcash.ui.util.EmailUtil + +class SendEmailUseCase( + private val context: Context, +) { + suspend operator fun invoke( + address: StringResource, + subject: StringResource, + message: StringResource + ) { + val intent = + EmailUtil.newMailActivityIntent( + recipientAddress = address.getString(context), + messageSubject = subject.getString(context), + messageBody = message.getString(context) + ).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + + context.startActivity(intent) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SendSupportEmailUseCase.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SendSupportEmailUseCase.kt new file mode 100644 index 000000000..59372ad09 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/common/usecase/SendSupportEmailUseCase.kt @@ -0,0 +1,45 @@ +package co.electriccoin.zcash.ui.common.usecase + +import android.content.Context +import android.content.Intent +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.design.util.StringResource +import co.electriccoin.zcash.ui.design.util.getString +import co.electriccoin.zcash.ui.screen.feedback.model.FeedbackEmoji +import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType +import co.electriccoin.zcash.ui.util.EmailUtil + +class SendSupportEmailUseCase( + private val context: Context, + private val getSupport: GetSupportUseCase +) { + suspend operator fun invoke( + emoji: FeedbackEmoji, + message: StringResource + ) { + val intent = + EmailUtil.newMailActivityIntent( + recipientAddress = context.getString(R.string.support_email_address), + messageSubject = context.getString(R.string.app_name), + messageBody = + buildString { + appendLine( + context.getString( + R.string.support_email_part_1, + emoji.encoding, + emoji.order.toString() + ) + ) + appendLine() + appendLine(context.getString(R.string.support_email_part_2, message.getString(context))) + appendLine() + appendLine() + appendLine(getSupport().toSupportString(SupportInfoType.entries.toSet())) + } + ).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + + context.startActivity(intent) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAbout.kt similarity index 95% rename from ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt rename to ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAbout.kt index 05ccd661b..5bc2724d6 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAboutView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/AndroidAbout.kt @@ -24,12 +24,8 @@ import kotlinx.coroutines.launch import org.koin.compose.koinInject @Composable -internal fun WrapAbout( - goBack: () -> Unit, - goWhatsNew: () -> Unit, -) { +internal fun WrapAbout(goBack: () -> Unit) { val activity = LocalActivity.current - val walletViewModel = koinActivityViewModel() val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value @@ -64,7 +60,6 @@ internal fun WrapAbout( }, snackbarHostState = snackbarHostState, topAppBarSubTitleState = walletState, - onWhatsNew = goWhatsNew ) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/view/AboutView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/view/AboutView.kt index 6a49aac0c..d5b5e10e5 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/view/AboutView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/about/view/AboutView.kt @@ -1,11 +1,11 @@ package co.electriccoin.zcash.ui.screen.about.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.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -14,6 +14,7 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -22,23 +23,25 @@ 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.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.text.font.FontWeight +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.common.model.VersionInfo -import co.electriccoin.zcash.ui.design.component.BlankBgScaffold -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation -import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItem +import co.electriccoin.zcash.ui.design.component.ZashiSettingsListItemState +import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar +import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation +import co.electriccoin.zcash.ui.design.component.ZashiVersion +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.design.util.scaffoldPadding +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.stringRes import co.electriccoin.zcash.ui.fixture.ConfigInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.screen.support.model.ConfigInfo @@ -49,12 +52,11 @@ fun About( onBack: () -> Unit, configInfo: ConfigInfo, onPrivacyPolicy: () -> Unit, - onWhatsNew: () -> Unit, snackbarHostState: SnackbarHostState, topAppBarSubTitleState: TopAppBarSubTitleState, versionInfo: VersionInfo, ) { - BlankBgScaffold( + Scaffold( topBar = { AboutTopAppBar( onBack = onBack, @@ -68,14 +70,18 @@ fun About( AboutMainContent( versionInfo = versionInfo, onPrivacyPolicy = onPrivacyPolicy, - onWhatsNew = onWhatsNew, modifier = Modifier .fillMaxHeight() .verticalScroll( rememberScrollState() ) - .scaffoldPadding(paddingValues) + .padding( + top = paddingValues.calculateTopPadding() + ZashiDimensions.Spacing.spacingLg, + bottom = paddingValues.calculateBottomPadding() + ZashiDimensions.Spacing.spacing3xl, + start = 4.dp, + end = 4.dp + ) ) } } @@ -87,20 +93,16 @@ private fun AboutTopAppBar( configInfo: ConfigInfo, subTitleState: TopAppBarSubTitleState ) { - SmallTopAppBar( - subTitle = + ZashiSmallTopAppBar( + subtitle = when (subTitleState) { TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) TopAppBarSubTitleState.None -> null }, - titleText = stringResource(id = R.string.about_title), + title = stringResource(id = R.string.about_title), navigationAction = { - TopAppBarBackNavigation( - backText = stringResource(id = R.string.back_navigation), - backContentDescriptionText = stringResource(R.string.back_navigation_content_description), - onBack = onBack - ) + ZashiTopAppBarBackNavigation(onBack = onBack) }, regularActions = { if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) { @@ -149,84 +151,57 @@ private fun DebugMenu( @Composable fun AboutMainContent( - onWhatsNew: () -> Unit, onPrivacyPolicy: () -> Unit, versionInfo: VersionInfo, modifier: Modifier = Modifier ) { Column(modifier) { - Image( - modifier = - Modifier - .height(ZcashTheme.dimens.inScreenZcashTextLogoHeight) - .align(Alignment.CenterHorizontally), - painter = painterResource(id = co.electriccoin.zcash.ui.design.R.drawable.zashi_text_logo_small), - colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor), - contentDescription = stringResource(R.string.zcash_logo_content_description) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) - Text( - modifier = Modifier.fillMaxWidth(), - text = - stringResource( - R.string.about_version_format, - versionInfo.versionName - ), - textAlign = TextAlign.Center, - style = ZcashTheme.typography.primary.titleSmall + modifier = Modifier.padding(horizontal = ZashiDimensions.Spacing.spacingXl), + text = stringResource(id = R.string.about_subtitle), + color = ZashiColors.Text.textPrimary, + style = ZashiTypography.header6, + fontWeight = FontWeight.SemiBold ) - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) + Spacer(Modifier.height(12.dp)) Text( + modifier = Modifier.padding(horizontal = ZashiDimensions.Spacing.spacingXl), text = stringResource(id = R.string.about_description), - color = ZcashTheme.colors.textDescriptionDark, - style = ZcashTheme.extendedTypography.aboutText + color = ZashiColors.Text.textPrimary, + style = ZashiTypography.textSm ) - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingUpLarge)) + Spacer(Modifier.height(32.dp)) - ZashiButton( - modifier = Modifier.fillMaxWidth(), - onClick = onWhatsNew, - text = stringResource(R.string.about_button_whats_new), + ZashiSettingsListItem( + ZashiSettingsListItemState( + icon = R.drawable.ic_settings_info, + text = stringRes(R.string.about_button_privacy_policy), + onClick = onPrivacyPolicy + ) ) - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) + Spacer(Modifier.weight(1f)) - ZashiButton( + ZashiVersion( modifier = Modifier.fillMaxWidth(), - onClick = onPrivacyPolicy, - text = stringResource(R.string.about_button_privacy_policy), + version = stringRes(R.string.settings_version, versionInfo.versionName) ) } } +@PreviewScreens @Composable -private fun AboutPreview() { - About( - onBack = {}, - configInfo = ConfigInfoFixture.new(), - onPrivacyPolicy = {}, - onWhatsNew = {}, - snackbarHostState = SnackbarHostState(), - topAppBarSubTitleState = TopAppBarSubTitleState.None, - versionInfo = VersionInfoFixture.new(), - ) -} - -@Preview("About") -@Composable -private fun AboutPreviewLight() = - ZcashTheme(forceDarkMode = false) { - AboutPreview() - } - -@Preview("About") -@Composable -private fun AboutPreviewDark() = - ZcashTheme(forceDarkMode = true) { - AboutPreview() +private fun AboutPreview() = + ZcashTheme { + About( + onBack = {}, + configInfo = ConfigInfoFixture.new(), + onPrivacyPolicy = {}, + snackbarHostState = SnackbarHostState(), + topAppBarSubTitleState = TopAppBarSubTitleState.None, + versionInfo = VersionInfoFixture.new(), + ) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/chooseserver/ChooseServerView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/chooseserver/ChooseServerView.kt index 209d298ef..1b0b02428 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/chooseserver/ChooseServerView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/chooseserver/ChooseServerView.kt @@ -57,15 +57,15 @@ import co.electriccoin.zcash.ui.design.component.LottieProgress import co.electriccoin.zcash.ui.design.component.RadioButton import co.electriccoin.zcash.ui.design.component.RadioButtonCheckedContent import co.electriccoin.zcash.ui.design.component.RadioButtonState -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar import co.electriccoin.zcash.ui.design.component.TextFieldState -import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation import co.electriccoin.zcash.ui.design.component.ZashiBadge import co.electriccoin.zcash.ui.design.component.ZashiBottomBar import co.electriccoin.zcash.ui.design.component.ZashiButton import co.electriccoin.zcash.ui.design.component.ZashiHorizontalDivider +import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar import co.electriccoin.zcash.ui.design.component.ZashiTextField import co.electriccoin.zcash.ui.design.component.ZashiTextFieldDefaults +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.colors.ZashiColors @@ -208,9 +208,9 @@ private fun ChooseServerTopAppBar( onBack: () -> Unit, subTitleState: TopAppBarSubTitleState ) { - SmallTopAppBar( - titleText = stringResource(id = R.string.choose_server_title), - subTitle = + ZashiSmallTopAppBar( + title = stringResource(id = R.string.choose_server_title), + subtitle = when (subTitleState) { TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) @@ -219,11 +219,7 @@ private fun ChooseServerTopAppBar( modifier = Modifier.testTag(CHOOSE_SERVER_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) } ) } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/deletewallet/view/DeleteWalletView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/deletewallet/view/DeleteWalletView.kt index d07d6bfa1..0367b6b5c 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/deletewallet/view/DeleteWalletView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/deletewallet/view/DeleteWalletView.kt @@ -16,26 +16,29 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.font.FontWeight 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.Body -import co.electriccoin.zcash.ui.design.component.LabeledCheckBox -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation -import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiButtonDefaults +import co.electriccoin.zcash.ui.design.component.ZashiCheckbox +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.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.util.scaffoldPadding +import co.electriccoin.zcash.ui.design.util.stringRes -@Preview("Delete Wallet") +@PreviewScreens @Composable -private fun ExportPrivateDataPreview() { - ZcashTheme(forceDarkMode = false) { +private fun ExportPrivateDataPreview() = + ZcashTheme { DeleteWallet( snackbarHostState = SnackbarHostState(), onBack = {}, @@ -43,7 +46,6 @@ private fun ExportPrivateDataPreview() { topAppBarSubTitleState = TopAppBarSubTitleState.None, ) } -} @Composable fun DeleteWallet( @@ -77,17 +79,16 @@ private fun DeleteWalletDataTopAppBar( onBack: () -> Unit, subTitleState: TopAppBarSubTitleState ) { - SmallTopAppBar( - subTitle = + ZashiSmallTopAppBar( + title = stringResource(R.string.delete_wallet_title), + subtitle = when (subTitleState) { TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) TopAppBarSubTitleState.None -> null }, navigationAction = { - TopAppBarBackNavigation( - backText = stringResource(id = R.string.back_navigation).uppercase(), - backContentDescriptionText = stringResource(R.string.back_navigation_content_description), + ZashiTopAppBarBackNavigation( onBack = onBack ) } @@ -99,46 +100,36 @@ private fun DeleteWalletContent( onConfirm: () -> Unit, modifier: Modifier = Modifier, ) { - val appName = stringResource(id = R.string.app_name) - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally + modifier = modifier ) { - TopScreenLogoTitle( - title = stringResource(R.string.delete_wallet_title, appName), - logoContentDescription = stringResource(R.string.zcash_logo_content_description) + Text( + text = stringResource(R.string.delete_wallet_title), + style = ZashiTypography.header6, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.SemiBold ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingBig)) + Spacer(Modifier.height(ZashiDimensions.Spacing.spacingXl)) Text( text = stringResource(R.string.delete_wallet_text_1), - style = ZcashTheme.extendedTypography.deleteWalletWarnStyle + style = ZashiTypography.textMd, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.SemiBold ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingUpLarge)) + Spacer(Modifier.height(ZashiDimensions.Spacing.spacingXl)) - Body( - text = - stringResource( - R.string.delete_wallet_text_2, - appName - ) + Text( + text = stringResource(R.string.delete_wallet_text_2), + style = ZashiTypography.textSm, + color = ZashiColors.Text.textPrimary, ) Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) val checkedState = rememberSaveable { mutableStateOf(false) } - Row(Modifier.fillMaxWidth()) { - LabeledCheckBox( - checked = checkedState.value, - onCheckedChange = { - checkedState.value = it - }, - text = stringResource(R.string.delete_wallet_acknowledge), - ) - } Spacer( modifier = @@ -147,13 +138,26 @@ private fun DeleteWalletContent( .weight(MINIMAL_WEIGHT) ) - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingXl)) + + Row(Modifier.fillMaxWidth()) { + ZashiCheckbox( + isChecked = checkedState.value, + onClick = { + checkedState.value = checkedState.value.not() + }, + text = stringRes(R.string.delete_wallet_acknowledge), + ) + } + + Spacer(Modifier.height(ZashiDimensions.Spacing.spacingLg)) ZashiButton( onClick = onConfirm, - text = stringResource(R.string.delete_wallet_button, appName), + text = stringResource(R.string.delete_wallet_button), enabled = checkedState.value, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + colors = ZashiButtonDefaults.destructive1Colors() ) } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exchangerate/widget/StyledExchangeUnavailablePopup.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exchangerate/widget/StyledExchangeUnavailablePopup.kt index 064497edf..9e1177125 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exchangerate/widget/StyledExchangeUnavailablePopup.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exchangerate/widget/StyledExchangeUnavailablePopup.kt @@ -1,42 +1,13 @@ package co.electriccoin.zcash.ui.screen.exchangerate.widget -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -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.shape.GenericShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreens -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors -import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.common.compose.ZashiAnimatedTooltip +import co.electriccoin.zcash.ui.design.util.stringRes @Composable internal fun StyledExchangeUnavailablePopup( @@ -49,88 +20,11 @@ internal fun StyledExchangeUnavailablePopup( onDismissRequest = onDismissRequest, offset = offset ) { - AnimatedVisibility( + ZashiAnimatedTooltip( visibleState = transitionState, - enter = - fadeIn() + - slideInVertically(spring(stiffness = Spring.StiffnessHigh)) + - scaleIn(spring(stiffness = Spring.StiffnessMedium, dampingRatio = Spring.DampingRatioLowBouncy)), - exit = - fadeOut() + - scaleOut(animationSpec = spring(stiffness = Spring.StiffnessMedium)) + - slideOutVertically(), - ) { - PopupContent(onDismissRequest = onDismissRequest) - } - } -} - -@Suppress("MagicNumber") -@Composable -private fun PopupContent(onDismissRequest: () -> Unit) { - Column( - modifier = Modifier.padding(horizontal = 22.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Box( - modifier = - Modifier - .width(16.dp) - .height(8.dp) - .background(ZashiColors.HintTooltips.surfacePrimary, TriangleShape) + title = stringRes(R.string.exchange_rate_unavailable_title), + message = stringRes(R.string.exchange_rate_unavailable_subtitle), + onDismissRequest = onDismissRequest ) - Box( - Modifier - .fillMaxWidth() - .background(ZashiColors.HintTooltips.surfacePrimary, RoundedCornerShape(8.dp)) - .padding(start = 12.dp, bottom = 12.dp), - ) { - Row { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - modifier = Modifier.padding(top = 12.dp), - color = ZashiColors.Text.textLight, - style = ZashiTypography.textMd, - text = stringResource(R.string.exchange_rate_unavailable_title) - ) - Spacer(modifier = Modifier.height(6.dp)) - Text( - color = ZashiColors.Text.textLightSupport, - style = ZashiTypography.textSm, - text = stringResource(id = R.string.exchange_rate_unavailable_subtitle) - ) - } - IconButton(onClick = onDismissRequest) { - Icon( - painter = painterResource(R.drawable.ic_exchange_rate_unavailable_dialog_close), - contentDescription = "", - tint = ZashiColors.HintTooltips.defaultBg - ) - } - } - } } } - -private val TriangleShape = - GenericShape { size, _ -> - - // 1) Start at the top center - moveTo(size.width / 2f, 0f) - - // 2) Draw a line to the bottom right corner - lineTo(size.width, size.height) - - // 3) Draw a line to the bottom left corner and implicitly close the shape - lineTo(0f, size.height) - } - -@Suppress("UnusedPrivateMember") -@PreviewScreens -@Composable -private fun PopupContentPreview() = - ZcashTheme { - PopupContent(onDismissRequest = {}) - } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataScreenTag.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataScreenTag.kt index 452911d2c..51c22c8ed 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataScreenTag.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataScreenTag.kt @@ -6,5 +6,4 @@ package co.electriccoin.zcash.ui.screen.exportdata.view object ExportPrivateDataScreenTag { const val AGREE_CHECKBOX_TAG = "agree_checkbox" const val WARNING_TEXT_TAG = "warning_text" - const val ADDITIONAL_TEXT_TAG = "additional_text" } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataView.kt index 9a8e03401..239e564f4 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/exportdata/view/ExportPrivateDataView.kt @@ -1,52 +1,36 @@ package co.electriccoin.zcash.ui.screen.exportdata.view 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.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.sp +import androidx.compose.ui.text.font.FontWeight 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.Body -import co.electriccoin.zcash.ui.design.component.LabeledCheckBox -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation -import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiCheckbox +import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar +import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreenSizes import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.util.scaffoldPadding - -@Preview("Export Private Data") -@Composable -private fun ExportPrivateDataPreview() { - ZcashTheme(forceDarkMode = false) { - ExportPrivateData( - snackbarHostState = SnackbarHostState(), - onBack = {}, - onAgree = {}, - onConfirm = {}, - topAppBarSubTitleState = TopAppBarSubTitleState.None, - ) - } -} +import co.electriccoin.zcash.ui.design.util.stringRes @Composable fun ExportPrivateData( @@ -56,7 +40,7 @@ fun ExportPrivateData( onConfirm: () -> Unit, topAppBarSubTitleState: TopAppBarSubTitleState, ) { - BlankBgScaffold( + Scaffold( topBar = { ExportPrivateDataTopAppBar( onBack = onBack, @@ -82,19 +66,16 @@ private fun ExportPrivateDataTopAppBar( onBack: () -> Unit, subTitleState: TopAppBarSubTitleState ) { - SmallTopAppBar( - subTitle = + ZashiSmallTopAppBar( + title = stringResource(R.string.export_data_title), + subtitle = when (subTitleState) { TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) TopAppBarSubTitleState.None -> null }, navigationAction = { - TopAppBarBackNavigation( - backText = stringResource(id = R.string.back_navigation).uppercase(), - backContentDescriptionText = stringResource(R.string.back_navigation_content_description), - onBack = onBack - ) + ZashiTopAppBarBackNavigation(onBack = onBack) }, ) } @@ -105,50 +86,35 @@ private fun ExportPrivateDataContent( onConfirm: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally - ) { - TopScreenLogoTitle( - title = stringResource(R.string.export_data_header), - logoContentDescription = stringResource(R.string.zcash_logo_content_description) - ) - - Spacer(Modifier.height(ZcashTheme.dimens.spacingLarge)) - - Body( - modifier = Modifier.testTag(ExportPrivateDataScreenTag.WARNING_TEXT_TAG), - text = stringResource(R.string.export_data_text_1) + Column(modifier = modifier) { + Text( + text = stringResource(R.string.export_data_header), + style = ZashiTypography.header6, + fontWeight = FontWeight.SemiBold, + color = ZashiColors.Text.textPrimary ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) + Spacer(Modifier.height(ZashiDimensions.Spacing.spacingLg)) Text( - modifier = Modifier.testTag(ExportPrivateDataScreenTag.ADDITIONAL_TEXT_TAG), - text = stringResource(R.string.export_data_text_2), - fontSize = 14.sp + modifier = Modifier.testTag(ExportPrivateDataScreenTag.WARNING_TEXT_TAG), + text = stringResource(R.string.export_data_text), + style = ZashiTypography.textSm, + color = ZashiColors.Text.textPrimary ) - Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault)) + Spacer(Modifier.weight(1f)) val checkedState = rememberSaveable { mutableStateOf(false) } - Row(Modifier.fillMaxWidth()) { - LabeledCheckBox( - checked = checkedState.value, - onCheckedChange = { - checkedState.value = it - onAgree(it) - }, - text = stringResource(R.string.export_data_agree), - checkBoxTestTag = ExportPrivateDataScreenTag.AGREE_CHECKBOX_TAG - ) - } - - Spacer( - modifier = - Modifier - .fillMaxHeight() - .weight(MINIMAL_WEIGHT) + ZashiCheckbox( + modifier = Modifier.testTag(ExportPrivateDataScreenTag.AGREE_CHECKBOX_TAG), + isChecked = checkedState.value, + onClick = { + val new = checkedState.value.not() + checkedState.value = new + onAgree(new) + }, + text = stringRes(R.string.export_data_agree), ) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) @@ -161,3 +127,16 @@ private fun ExportPrivateDataContent( ) } } + +@PreviewScreenSizes +@Composable +private fun ExportPrivateDataPreview() = + ZcashTheme { + ExportPrivateData( + snackbarHostState = SnackbarHostState(), + onBack = {}, + onAgree = {}, + onConfirm = {}, + topAppBarSubTitleState = TopAppBarSubTitleState.None, + ) + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/WrapFeedback.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/WrapFeedback.kt new file mode 100644 index 000000000..c463ae044 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/WrapFeedback.kt @@ -0,0 +1,50 @@ +@file:Suppress("ktlint:standard:filename") + +package co.electriccoin.zcash.ui.screen.feedback + +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.LocalNavController +import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel +import co.electriccoin.zcash.ui.design.component.AppAlertDialog +import co.electriccoin.zcash.ui.screen.feedback.view.FeedbackView +import co.electriccoin.zcash.ui.screen.feedback.viewmodel.FeedbackViewModel +import org.koin.androidx.compose.koinViewModel + +@Composable +internal fun WrapFeedback() { + val navController = LocalNavController.current + val walletViewModel = koinActivityViewModel() + val viewModel = koinViewModel() + + val walletState by walletViewModel.walletStateInformation.collectAsStateWithLifecycle() + val state by viewModel.state.collectAsStateWithLifecycle() + val dialogState by viewModel.dialogState.collectAsStateWithLifecycle() + + LaunchedEffect(Unit) { + viewModel.onBackNavigationCommand.collect { + navController.popBackStack() + } + } + + BackHandler { + state?.onBack?.invoke() + } + + state?.let { + FeedbackView( + state = it, + topAppBarSubTitleState = walletState + ) + } + + dialogState?.let { + AppAlertDialog( + state = it + ) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/model/FeedbackState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/model/FeedbackState.kt new file mode 100644 index 000000000..cc201fc95 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/model/FeedbackState.kt @@ -0,0 +1,50 @@ +package co.electriccoin.zcash.ui.screen.feedback.model + +import androidx.annotation.DrawableRes +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.TextFieldState + +data class FeedbackState( + val onBack: () -> Unit, + val emojiState: FeedbackEmojiState, + val feedback: TextFieldState, + val sendButton: ButtonState +) + +data class FeedbackEmojiState( + val selection: FeedbackEmoji, + val onSelected: (FeedbackEmoji) -> Unit, +) + +enum class FeedbackEmoji( + @DrawableRes val res: Int, + val order: Int, + val encoding: String +) { + FIRST( + res = R.drawable.ic_emoji_1, + order = 1, + encoding = "😠" + ), + SECOND( + res = R.drawable.ic_emoji_2, + order = 2, + encoding = "😒" + ), + THIRD( + res = R.drawable.ic_emoji_3, + order = 3, + encoding = "😊" + ), + FOURTH( + res = R.drawable.ic_emoji_4, + order = 4, + encoding = "😄" + ), + FIFTH( + res = R.drawable.ic_emoji_5, + order = 5, + encoding = "😍" + ) +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/view/FeedbackView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/view/FeedbackView.kt new file mode 100644 index 000000000..b03aec537 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/view/FeedbackView.kt @@ -0,0 +1,268 @@ +package co.electriccoin.zcash.ui.screen.feedback.view + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +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.component.BlankSurface +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.TextFieldState +import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar +import co.electriccoin.zcash.ui.design.component.ZashiTextField +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.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.scaffoldPadding +import co.electriccoin.zcash.ui.design.util.stringRes +import co.electriccoin.zcash.ui.screen.feedback.model.FeedbackEmoji +import co.electriccoin.zcash.ui.screen.feedback.model.FeedbackEmojiState +import co.electriccoin.zcash.ui.screen.feedback.model.FeedbackState + +@Composable +fun FeedbackView( + state: FeedbackState, + topAppBarSubTitleState: TopAppBarSubTitleState, +) { + Scaffold( + topBar = { + SupportTopAppBar( + state = state, + subTitleState = topAppBarSubTitleState, + ) + }, + ) { paddingValues -> + SupportMainContent( + state = state, + modifier = Modifier.scaffoldPadding(paddingValues) + ) + } +} + +@Composable +private fun SupportTopAppBar( + state: FeedbackState, + subTitleState: TopAppBarSubTitleState +) { + ZashiSmallTopAppBar( + subtitle = + when (subTitleState) { + TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) + TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) + TopAppBarSubTitleState.None -> null + }, + title = stringResource(id = R.string.support_header), + navigationAction = { + ZashiTopAppBarBackNavigation(onBack = state.onBack) + }, + ) +} + +@Composable +private fun SupportMainContent( + state: FeedbackState, + modifier: Modifier = Modifier +) { + val focusRequester = remember { FocusRequester() } + + Column( + Modifier + .fillMaxHeight() + .verticalScroll(rememberScrollState()) + .then(modifier), + ) { + Image( + painter = painterResource(R.drawable.ic_feedback), + contentDescription = null, + ) + + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacing3xl)) + + Text( + text = stringResource(id = R.string.support_title), + style = ZashiTypography.header6, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.SemiBold + ) + + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingMd)) + + Text( + text = stringResource(id = R.string.support_information), + color = ZashiColors.Text.textPrimary, + style = ZashiTypography.textSm + ) + + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacing4xl)) + + Text( + text = stringResource(id = R.string.support_experience_title), + color = ZashiColors.Inputs.Default.label, + style = ZashiTypography.textSm, + fontWeight = FontWeight.Medium + ) + + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingLg)) + + EmojiRow(state.emojiState) + + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacing3xl)) + + Text( + text = stringResource(id = R.string.support_help_title), + color = ZashiColors.Inputs.Default.label, + style = ZashiTypography.textSm, + fontWeight = FontWeight.Medium + ) + + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingLg)) + + ZashiTextField( + state = state.feedback, + minLines = 3, + modifier = + Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + placeholder = { + Text( + text = stringResource(id = R.string.support_hint), + style = ZashiTypography.textMd, + color = ZashiColors.Inputs.Default.text + ) + }, + ) + + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingLg)) + + Spacer( + modifier = Modifier.weight(1f) + ) + + // TODO [#1467]: Support screen - keep button above keyboard + // TODO [#1467]: https://github.com/Electric-Coin-Company/zashi-android/issues/1467 + ZashiButton( + state = state.sendButton, + modifier = Modifier.fillMaxWidth() + ) + } + + LaunchedEffect(Unit) { + // Causes the TextField to focus on the first screen visit + focusRequester.requestFocus() + } +} + +@Composable +private fun EmojiRow(state: FeedbackEmojiState) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = spacedBy(2.dp), + ) { + listOf( + FeedbackEmoji.FIRST, + FeedbackEmoji.SECOND, + FeedbackEmoji.THIRD, + FeedbackEmoji.FOURTH, + FeedbackEmoji.FIFTH, + ).forEach { + Emoji( + modifier = Modifier.weight(1f), + emoji = it, + isSelected = state.selection == it, + onClick = { state.onSelected(it) } + ) + } + } +} + +@Composable +private fun Emoji( + emoji: FeedbackEmoji, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = + modifier + .aspectRatio(EMOJI_CARD_RATIO) + .border( + width = 2.5.dp, + color = if (isSelected) ZashiColors.Text.textPrimary else Color.Transparent, + shape = RoundedCornerShape(ZashiDimensions.Radius.radiusXl) + ) + .padding(4.5.dp) + .background( + color = ZashiColors.Surfaces.bgSecondary, + shape = RoundedCornerShape(8.dp) + ) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + Image( + modifier = Modifier.size(24.dp), + painter = painterResource(emoji.res), + contentDescription = "" + ) + } +} + +@PreviewScreens +@Composable +private fun Preview() = + ZcashTheme { + BlankSurface { + FeedbackView( + state = + FeedbackState( + onBack = {}, + sendButton = ButtonState(stringRes("Button")), + feedback = TextFieldState(stringRes("")) {}, + emojiState = + FeedbackEmojiState( + selection = FeedbackEmoji.FIRST, + onSelected = {} + ) + ), + topAppBarSubTitleState = TopAppBarSubTitleState.None, + ) + } + } + +private const val EMOJI_CARD_RATIO = 1.25f diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/viewmodel/FeedbackViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/viewmodel/FeedbackViewModel.kt new file mode 100644 index 000000000..a41c9ec24 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/feedback/viewmodel/FeedbackViewModel.kt @@ -0,0 +1,93 @@ +package co.electriccoin.zcash.ui.screen.feedback.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.usecase.SendSupportEmailUseCase +import co.electriccoin.zcash.ui.design.component.AlertDialogState +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.TextFieldState +import co.electriccoin.zcash.ui.design.util.stringRes +import co.electriccoin.zcash.ui.screen.feedback.model.FeedbackEmoji +import co.electriccoin.zcash.ui.screen.feedback.model.FeedbackEmojiState +import co.electriccoin.zcash.ui.screen.feedback.model.FeedbackState +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class FeedbackViewModel( + private val sendSupportEmail: SendSupportEmailUseCase +) : ViewModel() { + private val feedback = MutableStateFlow("") + private val selectedEmoji = MutableStateFlow(FeedbackEmoji.FIFTH) + private val isDialogShown = MutableStateFlow(false) + + val onBackNavigationCommand = MutableSharedFlow() + + val state = + combine(feedback, selectedEmoji) { feedbackText, emoji -> + FeedbackState( + onBack = ::onBack, + emojiState = + FeedbackEmojiState( + selection = emoji, + onSelected = { new -> selectedEmoji.update { new } } + ), + feedback = + TextFieldState( + value = stringRes(feedbackText), + onValueChange = { new -> feedback.update { new } } + ), + sendButton = + ButtonState( + text = stringRes(R.string.support_send), + isEnabled = feedbackText.isNotEmpty(), + onClick = ::onSendClicked + ) + ) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null) + + val dialogState = + isDialogShown.map { isShown -> + AlertDialogState( + title = stringRes(R.string.support_confirmation_dialog_title), + text = stringRes(R.string.support_confirmation_explanation), + confirmButtonState = + ButtonState( + text = stringRes(R.string.support_confirmation_dialog_ok), + onClick = ::onConfirmSendFeedback + ), + dismissButtonState = + ButtonState( + text = stringRes(R.string.support_confirmation_dialog_cancel), + onClick = { isDialogShown.update { false } } + ), + onDismissRequest = { isDialogShown.update { false } } + ).takeIf { isShown } + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null) + + private fun onConfirmSendFeedback() = + viewModelScope.launch { + isDialogShown.update { false } + sendSupportEmail( + emoji = selectedEmoji.value, + message = stringRes(feedback.value) + ) + } + + private fun onSendClicked() { + isDialogShown.update { true } + } + + private fun onBack() = + viewModelScope.launch { + onBackNavigationCommand.emit(Unit) + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/AndroidNewWalletRecovery.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/AndroidNewWalletRecovery.kt deleted file mode 100644 index 1dc687b79..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/AndroidNewWalletRecovery.kt +++ /dev/null @@ -1,39 +0,0 @@ -package co.electriccoin.zcash.ui.screen.newwalletrecovery - -import androidx.compose.runtime.Composable -import cash.z.ecc.android.sdk.model.PersistableWallet -import co.electriccoin.zcash.spackle.ClipboardManagerUtil -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.compose.LocalActivity -import co.electriccoin.zcash.ui.common.model.VersionInfo -import co.electriccoin.zcash.ui.screen.newwalletrecovery.view.NewWalletRecovery - -@Composable -fun WrapNewWalletRecovery( - persistableWallet: PersistableWallet, - onBackupComplete: () -> Unit -) { - val activity = LocalActivity.current - - val versionInfo = VersionInfo.new(activity.applicationContext) - - NewWalletRecovery( - persistableWallet, - onSeedCopy = { - ClipboardManagerUtil.copyToClipboard( - activity.applicationContext, - activity.getString(R.string.new_wallet_recovery_seed_clipboard_tag), - persistableWallet.seedPhrase.joinToString() - ) - }, - onBirthdayCopy = { - ClipboardManagerUtil.copyToClipboard( - activity.applicationContext, - activity.getString(R.string.new_wallet_recovery_birthday_clipboard_tag), - persistableWallet.birthday?.value.toString() - ) - }, - onComplete = onBackupComplete, - versionInfo = versionInfo - ) -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryTag.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryTag.kt deleted file mode 100644 index a1e15b33c..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryTag.kt +++ /dev/null @@ -1,8 +0,0 @@ -package co.electriccoin.zcash.ui.screen.newwalletrecovery.view - -/** - * These are only used for automated testing. - */ -object NewWalletRecoveryTag { - const val DEBUG_MENU_TAG = "debug_menu" -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryView.kt deleted file mode 100644 index 233917cb9..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/newwalletrecovery/view/NewWalletRecoveryView.kt +++ /dev/null @@ -1,266 +0,0 @@ -package co.electriccoin.zcash.ui.screen.newwalletrecovery.view - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -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.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import cash.z.ecc.android.sdk.model.PersistableWallet -import cash.z.ecc.sdk.fixture.PersistableWalletFixture -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.compose.SecureScreen -import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen -import co.electriccoin.zcash.ui.common.model.VersionInfo -import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY -import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT -import co.electriccoin.zcash.ui.design.component.BlankBgScaffold -import co.electriccoin.zcash.ui.design.component.BodySmall -import co.electriccoin.zcash.ui.design.component.ChipGrid -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle -import co.electriccoin.zcash.ui.design.component.ZashiButton -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions -import co.electriccoin.zcash.ui.design.util.scaffoldPadding -import co.electriccoin.zcash.ui.fixture.VersionInfoFixture -import kotlinx.collections.immutable.toPersistentList - -@Preview -@Composable -private fun NewWalletRecoveryPreview() { - ZcashTheme(forceDarkMode = false) { - NewWalletRecovery( - PersistableWalletFixture.new(), - onSeedCopy = {}, - onBirthdayCopy = {}, - onComplete = {}, - versionInfo = VersionInfoFixture.new(), - ) - } -} - -@Preview -@Composable -private fun NewWalletRecoveryDarkPreview() { - ZcashTheme(forceDarkMode = true) { - NewWalletRecovery( - PersistableWalletFixture.new(), - onSeedCopy = {}, - onBirthdayCopy = {}, - onComplete = {}, - versionInfo = VersionInfoFixture.new(), - ) - } -} - -/** - * @param onComplete Callback when the user has confirmed viewing the seed phrase. - */ -@Composable -fun NewWalletRecovery( - wallet: PersistableWallet, - onSeedCopy: () -> Unit, - onBirthdayCopy: () -> Unit, - onComplete: () -> Unit, - versionInfo: VersionInfo, -) { - BlankBgScaffold( - topBar = { - NewWalletRecoveryTopAppBar( - onSeedCopy = onSeedCopy, - versionInfo = versionInfo, - ) - } - ) { paddingValues -> - NewWalletRecoveryMainContent( - wallet = wallet, - onComplete = onComplete, - onSeedCopy = onSeedCopy, - onBirthdayCopy = onBirthdayCopy, - versionInfo = versionInfo, - // Horizontal paddings will be part of each UI element to minimize a possible truncation on very - // small screens - modifier = - Modifier - .scaffoldPadding(paddingValues) - ) - } -} - -@Composable -private fun NewWalletRecoveryTopAppBar( - versionInfo: VersionInfo, - modifier: Modifier = Modifier, - onSeedCopy: () -> Unit -) { - SmallTopAppBar( - modifier = modifier, - regularActions = { - if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) { - DebugMenu(onCopyToClipboard = onSeedCopy) - } - }, - ) -} - -@Composable -private fun DebugMenu(onCopyToClipboard: () -> Unit) { - Column( - modifier = Modifier.testTag(NewWalletRecoveryTag.DEBUG_MENU_TAG) - ) { - var expanded by rememberSaveable { mutableStateOf(false) } - IconButton(onClick = { expanded = true }) { - Icon(Icons.Default.MoreVert, contentDescription = null) - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - DropdownMenuItem( - text = { - Text(stringResource(id = R.string.new_wallet_recovery_copy)) - }, - onClick = { - onCopyToClipboard() - expanded = false - } - ) - } - } -} - -@Composable -@Suppress("LongParameterList") -private fun NewWalletRecoveryMainContent( - wallet: PersistableWallet, - onSeedCopy: () -> Unit, - onBirthdayCopy: () -> Unit, - onComplete: () -> Unit, - versionInfo: VersionInfo, - modifier: Modifier = Modifier, -) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .then(modifier), - horizontalAlignment = Alignment.CenterHorizontally - ) { - TopScreenLogoTitle( - title = stringResource(R.string.new_wallet_recovery_header), - logoContentDescription = stringResource(R.string.zcash_logo_content_description), - modifier = Modifier.padding(horizontal = ZashiDimensions.Spacing.spacing3xl) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) - - BodySmall( - text = stringResource(R.string.new_wallet_recovery_description), - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = ZashiDimensions.Spacing.spacing3xl) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) - - NewWalletRecoverySeedPhrase( - persistableWallet = wallet, - onSeedCopy = onSeedCopy, - onBirthdayCopy = onBirthdayCopy, - versionInfo = versionInfo - ) - - Spacer( - modifier = - Modifier - .fillMaxHeight() - .weight(MINIMAL_WEIGHT) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) - - ZashiButton( - onClick = onComplete, - text = stringResource(R.string.new_wallet_recovery_button_finished), - modifier = - Modifier - .fillMaxWidth() - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun NewWalletRecoverySeedPhrase( - persistableWallet: PersistableWallet, - onSeedCopy: () -> Unit, - onBirthdayCopy: () -> Unit, - versionInfo: VersionInfo, -) { - if (shouldSecureScreen) { - SecureScreen() - } - - Column { - ChipGrid( - wordList = persistableWallet.seedPhrase.split.toPersistentList(), - onGridClick = onSeedCopy, - allowCopy = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService, - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) - - persistableWallet.birthday?.let { - val interactionSource = remember { MutableInteractionSource() } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - BodySmall( - text = stringResource(R.string.new_wallet_recovery_birthday_height, it.value), - modifier = - Modifier - .testTag(WALLET_BIRTHDAY) - .padding(horizontal = ZcashTheme.dimens.spacingDefault) - .basicMarquee() - // Apply click callback to the text only as the wrapping layout can be much wider - .clickable( - interactionSource = interactionSource, - // Disable ripple - indication = null, - onClick = onBirthdayCopy - ) - ) - } - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/view/QrCodeView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/view/QrCodeView.kt index 3ad2287ac..12cb56528 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/view/QrCodeView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/view/QrCodeView.kt @@ -190,7 +190,7 @@ private fun QrCodeBottomBar( ZashiBottomBar { ZashiButton( text = stringResource(id = R.string.qr_code_share_btn), - leadingIcon = painterResource(R.drawable.ic_share), + icon = R.drawable.ic_share, onClick = { state.onQrCodeShare(qrCodeImage) }, modifier = Modifier @@ -202,7 +202,7 @@ private fun QrCodeBottomBar( ZashiButton( text = stringResource(id = R.string.qr_code_copy_btn), - leadingIcon = painterResource(R.drawable.ic_copy), + icon = R.drawable.ic_copy, onClick = { state.onAddressCopy(state.walletAddress.address) }, colors = ZashiButtonDefaults.secondaryColors(), modifier = diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/viewmodel/QrCodeViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/viewmodel/QrCodeViewModel.kt index cf86d089b..07774baa0 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/viewmodel/QrCodeViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/qrcode/viewmodel/QrCodeViewModel.kt @@ -87,7 +87,6 @@ class QrCodeViewModel( private fun onAddressCopyClick(address: String) = copyToClipboard( - context = application.applicationContext, tag = application.getString(R.string.qr_code_clipboard_tag), value = address ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt index 09fea3a22..fdd573eba 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/receive/viewmodel/ReceiveViewModel.kt @@ -33,7 +33,6 @@ class ReceiveViewModel( isTestnet = getVersionInfo().isTestnet, onAddressCopy = { address -> copyToClipboard( - context = application.applicationContext, tag = application.getString(R.string.receive_clipboard_tag), value = address ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/request/view/RequestView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/request/view/RequestView.kt index b3e6889f5..3eb5632dc 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/request/view/RequestView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/request/view/RequestView.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle @@ -175,7 +174,7 @@ private fun RequestBottomBar( is RequestState.QrCode -> { ZashiButton( text = stringResource(id = R.string.request_qr_share_btn), - leadingIcon = painterResource(R.drawable.ic_share), + icon = R.drawable.ic_share, enabled = state.request.qrCodeState.isValid(), onClick = { state.onQrCodeShare(state.request.qrCodeState.bitmap!!) }, modifier = diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt index 68ae1b9ad..97323effd 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/restore/view/RestoreView.kt @@ -422,7 +422,7 @@ private fun RestoreSeedMainContent( .then(modifier), horizontalAlignment = Alignment.CenterHorizontally ) { - // Used to calculate necessary scroll to have the seed TextFiled visible + // Used to calculate necessary scroll to have the seed TextField visible Column( modifier = Modifier.onSizeChanged { size -> @@ -478,7 +478,7 @@ private fun RestoreSeedMainContent( } LaunchedEffect(parseResult) { - // Causes the TextFiled to refocus + // Causes the TextField to refocus if (!isSeedValid) { focusRequester.requestFocus() } @@ -853,7 +853,7 @@ private fun RestoreBirthdayMainContent( } LaunchedEffect(Unit) { - // Causes the TextFiled to focus on the first screen visit + // Causes the TextField to focus on the first screen visit focusRequester.requestFocus() } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/AndroidSeedRecovery.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/AndroidSeedRecovery.kt new file mode 100644 index 000000000..f9999f380 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/AndroidSeedRecovery.kt @@ -0,0 +1,59 @@ +package co.electriccoin.zcash.ui.screen.seed + +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.LocalNavController +import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel +import co.electriccoin.zcash.ui.screen.seed.view.SeedView +import co.electriccoin.zcash.ui.screen.seed.viewmodel.SeedViewModel +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + +@Composable +internal fun WrapSeed( + args: SeedNavigationArgs, + goBackOverride: (() -> Unit)? +) { + val navController = LocalNavController.current + val walletViewModel = koinActivityViewModel() + val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value + val viewModel = koinViewModel { parametersOf(args) } + val state by viewModel.state.collectAsStateWithLifecycle() + + val normalizedState = + state?.copy( + onBack = + state?.onBack?.let { + { + goBackOverride?.invoke() + it.invoke() + } + } + ) + + LaunchedEffect(Unit) { + viewModel.navigateBack.collect { + navController.popBackStack() + } + } + + BackHandler { + normalizedState?.onBack?.invoke() + } + + normalizedState?.let { + SeedView( + state = normalizedState, + topAppBarSubTitleState = walletState, + ) + } +} + +enum class SeedNavigationArgs { + NEW_WALLET, + RECOVERY +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/model/SeedState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/model/SeedState.kt new file mode 100644 index 000000000..34257fe9a --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/model/SeedState.kt @@ -0,0 +1,31 @@ +package co.electriccoin.zcash.ui.screen.seed.model + +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.util.StringResource + +data class SeedState( + val seed: SeedSecretState, + val birthday: SeedSecretState, + val button: ButtonState, + val onBack: (() -> Unit)? +) + +data class SeedSecretState( + val title: StringResource, + val text: StringResource, + val isRevealed: Boolean, + val isRevealPhraseVisible: Boolean, + val mode: Mode, + val tooltip: SeedSecretStateTooltip?, + val onClick: (() -> Unit)?, +) { + enum class Mode { + SEED, + BIRTHDAY + } +} + +data class SeedSecretStateTooltip( + val title: StringResource, + val message: StringResource, +) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/view/SeedView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/view/SeedView.kt new file mode 100644 index 000000000..ca14aa527 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/view/SeedView.kt @@ -0,0 +1,465 @@ +package co.electriccoin.zcash.ui.screen.seed.view + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowColumn +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.blur +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import co.electriccoin.zcash.spackle.AndroidApiVersion +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.compose.SecureScreen +import co.electriccoin.zcash.ui.common.compose.ZashiTooltip +import co.electriccoin.zcash.ui.common.compose.ZashiTooltipBox +import co.electriccoin.zcash.ui.common.compose.drawCaretWithPath +import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen +import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.component.ZashiButton +import co.electriccoin.zcash.ui.design.component.ZashiSmallTopAppBar +import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation +import co.electriccoin.zcash.ui.design.newcomponent.PreviewScreenSizes +import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography +import co.electriccoin.zcash.ui.design.util.getValue +import co.electriccoin.zcash.ui.design.util.scaffoldPadding +import co.electriccoin.zcash.ui.design.util.stringRes +import co.electriccoin.zcash.ui.screen.seed.model.SeedSecretState +import co.electriccoin.zcash.ui.screen.seed.model.SeedSecretStateTooltip +import co.electriccoin.zcash.ui.screen.seed.model.SeedState +import kotlinx.coroutines.launch + +@Composable +fun SeedView( + topAppBarSubTitleState: TopAppBarSubTitleState, + state: SeedState, +) { + if (shouldSecureScreen) { + SecureScreen() + } + + Scaffold( + topBar = { + SeedRecoveryTopAppBar( + state = state, + subTitleState = topAppBarSubTitleState, + ) + } + ) { paddingValues -> + SeedRecoveryMainContent( + modifier = Modifier.scaffoldPadding(paddingValues), + state = state, + ) + } +} + +@Composable +private fun SeedRecoveryTopAppBar( + state: SeedState, + subTitleState: TopAppBarSubTitleState, + modifier: Modifier = Modifier, +) { + ZashiSmallTopAppBar( + title = stringResource(R.string.seed_recovery_title), + subtitle = + when (subTitleState) { + TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) + TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) + TopAppBarSubTitleState.None -> null + }, + modifier = modifier, + navigationAction = { + if (state.onBack != null) { + ZashiTopAppBarBackNavigation(onBack = state.onBack) + } + } + ) +} + +@Composable +private fun SeedRecoveryMainContent( + state: SeedState, + modifier: Modifier = Modifier, +) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .then(modifier), + ) { + Text( + text = stringResource(R.string.seed_recovery_header), + fontWeight = FontWeight.SemiBold, + color = ZashiColors.Text.textPrimary, + style = ZashiTypography.header6 + ) + + Spacer(Modifier.height(ZashiDimensions.Spacing.spacingMd)) + + Text( + text = stringResource(R.string.seed_recovery_description), + color = ZashiColors.Text.textPrimary, + style = ZashiTypography.textSm + ) + + Spacer(Modifier.height(ZashiDimensions.Spacing.spacing4xl)) + + SeedSecret(modifier = Modifier.fillMaxWidth(), state = state.seed) + + Spacer(Modifier.height(ZashiDimensions.Spacing.spacing3xl)) + + SeedSecret(modifier = Modifier.fillMaxWidth(), state = state.birthday) + + Spacer(Modifier.weight(1f)) + + Spacer(Modifier.height(ZashiDimensions.Spacing.spacing3xl)) + + Row { + Image( + painterResource(R.drawable.ic_warning), + contentDescription = null, + colorFilter = ColorFilter.tint(ZashiColors.Utility.WarningYellow.utilityOrange500) + ) + + Spacer(Modifier.width(ZashiDimensions.Spacing.spacingLg)) + + Text( + text = stringResource(R.string.seed_recovery_warning), + color = ZashiColors.Utility.WarningYellow.utilityOrange500, + style = ZashiTypography.textXs, + fontWeight = FontWeight.Medium + ) + } + + Spacer(Modifier.height(ZashiDimensions.Spacing.spacing3xl)) + + ZashiButton( + state = state.button, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SeedSecret( + state: SeedSecretState, + modifier: Modifier = Modifier, +) { + val tooltipState = rememberTooltipState(isPersistent = true) + val scope = rememberCoroutineScope() + Column( + modifier = modifier + ) { + Row( + modifier = + if (state.tooltip != null) { + Modifier.clickable { + scope.launch { + if (tooltipState.isVisible) { + tooltipState.dismiss() + } else { + tooltipState.show() + } + } + } + } else { + Modifier + } + ) { + Text( + text = state.title.getValue(), + color = ZashiColors.Text.textPrimary, + style = ZashiTypography.textSm, + fontWeight = FontWeight.Medium + ) + if (state.tooltip != null) { + val density = LocalDensity.current + val configuration = LocalConfiguration.current + val containerColor = ZashiColors.HintTooltips.defaultBg + Spacer(Modifier.width(2.dp)) + ZashiTooltipBox( + tooltip = { + ZashiTooltip( + modifier = + Modifier.drawCaret { + drawCaretWithPath( + density = density, + configuration = configuration, + containerColor = containerColor, + anchorLayoutCoordinates = it + ) + }, + showCaret = false, + title = state.tooltip.title, + message = state.tooltip.message, + onDismissRequest = { + scope.launch { + tooltipState.dismiss() + } + } + ) + }, + state = tooltipState, + ) { + Image( + painter = painterResource(id = R.drawable.ic_zashi_tooltip), + contentDescription = "", + colorFilter = ColorFilter.tint(ZashiColors.Inputs.Default.icon) + ) + } + } + } + Spacer(Modifier.height(ZashiDimensions.Spacing.spacingSm)) + + SecretContent(state = state) + } +} + +@Composable +private fun SecretContent(state: SeedSecretState) { + val blur = animateDpAsState(if (state.isRevealed) 0.dp else 14.dp, label = "") + Surface( + modifier = + Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(10.dp), + color = ZashiColors.Inputs.Default.bg + ) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Box( + modifier = + Modifier then + if (state.onClick != null) { + Modifier.clickable(onClick = state.onClick) + } else { + Modifier + } then + Modifier + .blurCompat(blur.value, 14.dp) + .padding(horizontal = 24.dp, vertical = 18.dp) + ) { + if (state.mode == SeedSecretState.Mode.SEED) { + SecretSeedContent(state) + } else { + SecretBirthdayContent(state) + } + } + + AnimatedVisibility( + modifier = Modifier.fillMaxWidth(), + visible = + state.isRevealPhraseVisible && + state.isRevealed.not() && + state.mode == SeedSecretState.Mode.SEED, + enter = fadeIn(), + exit = fadeOut(), + ) { + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 18.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(R.drawable.ic_reveal), + contentDescription = "", + colorFilter = ColorFilter.tint(ZashiColors.Text.textPrimary) + ) + + Spacer(Modifier.height(ZashiDimensions.Spacing.spacingMd)) + + Text( + text = stringResource(R.string.seed_recovery_reveal), + style = ZashiTypography.textLg, + fontWeight = FontWeight.SemiBold, + color = ZashiColors.Text.textPrimary + ) + } + } + } + } +} + +private fun Modifier.blurCompat( + radius: Dp, + max: Dp +): Modifier { + return if (AndroidApiVersion.isAtLeastS) { + this.blur(radius) + } else { + val progression = 1 - (radius.value / max.value) + this + .alpha(progression) + } +} + +@Composable +private fun SecretBirthdayContent(state: SeedSecretState) { + Text( + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Start, + text = state.text.getValue(), + style = ZashiTypography.textMd, + fontWeight = FontWeight.Medium, + color = ZashiColors.Inputs.Filled.text + ) +} + +@Composable +@OptIn(ExperimentalLayoutApi::class) +private fun SecretSeedContent(state: SeedSecretState) { + FlowColumn( + modifier = + Modifier + .fillMaxWidth() + .padding(end = 8.dp), + maxItemsInEachColumn = 8, + horizontalArrangement = Arrangement.SpaceBetween, + verticalArrangement = spacedBy(6.dp), + maxLines = 8 + ) { + state.text.getValue().split(" ").fastForEachIndexed { i, s -> + Row( + modifier = Modifier + ) { + Text( + modifier = Modifier.width(18.dp), + textAlign = TextAlign.End, + text = "${i + 1}", + style = ZashiTypography.textSm, + fontWeight = FontWeight.Normal, + color = ZashiColors.Text.textPrimary, + maxLines = 1 + ) + + Spacer(modifier = Modifier.width(ZashiDimensions.Spacing.spacingLg)) + + Text( + text = s, + style = ZashiTypography.textSm, + fontWeight = FontWeight.Normal, + color = ZashiColors.Text.textPrimary, + maxLines = 1 + ) + } + } + } +} + +@Composable +@PreviewScreenSizes +private fun RevealedPreview() = + ZcashTheme { + SeedView( + topAppBarSubTitleState = TopAppBarSubTitleState.None, + state = + SeedState( + seed = + SeedSecretState( + title = stringRes("Seed"), + text = stringRes((1..24).joinToString(" ") { "trala" }), + tooltip = null, + isRevealed = true, + mode = SeedSecretState.Mode.SEED, + isRevealPhraseVisible = true + ) {}, + birthday = + SeedSecretState( + title = stringRes("Birthday"), + text = stringRes(value = "asdads"), + tooltip = SeedSecretStateTooltip(title = stringRes(""), message = stringRes("")), + isRevealed = true, + mode = SeedSecretState.Mode.BIRTHDAY, + isRevealPhraseVisible = false + ) {}, + button = + ButtonState( + text = stringRes("Text"), + icon = R.drawable.ic_seed_show, + onClick = {}, + ), + onBack = {} + ) + ) + } + +@Composable +@PreviewScreenSizes +private fun HiddenPreview() = + ZcashTheme { + SeedView( + topAppBarSubTitleState = TopAppBarSubTitleState.None, + state = + SeedState( + seed = + SeedSecretState( + title = stringRes("Seed"), + text = stringRes((1..24).joinToString(" ") { "trala" }), + tooltip = null, + isRevealed = false, + mode = SeedSecretState.Mode.SEED, + isRevealPhraseVisible = true + ) {}, + birthday = + SeedSecretState( + title = stringRes("Birthday"), + text = stringRes(value = "asdads"), + tooltip = SeedSecretStateTooltip(title = stringRes(""), message = stringRes("")), + isRevealed = false, + mode = SeedSecretState.Mode.BIRTHDAY, + isRevealPhraseVisible = false + ) {}, + button = + ButtonState( + text = stringRes("Text"), + icon = R.drawable.ic_seed_show, + onClick = {}, + ), + onBack = {} + ) + ) + } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/viewmodel/SeedViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/viewmodel/SeedViewModel.kt new file mode 100644 index 000000000..9d3ba7918 --- /dev/null +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seed/viewmodel/SeedViewModel.kt @@ -0,0 +1,117 @@ +package co.electriccoin.zcash.ui.screen.seed.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import cash.z.ecc.sdk.ANDROID_STATE_FLOW_TIMEOUT +import co.electriccoin.zcash.ui.R +import co.electriccoin.zcash.ui.common.model.OnboardingState +import co.electriccoin.zcash.ui.common.repository.WalletRepository +import co.electriccoin.zcash.ui.common.usecase.ObserveBackupPersistableWalletUseCase +import co.electriccoin.zcash.ui.common.usecase.ObservePersistableWalletUseCase +import co.electriccoin.zcash.ui.design.component.ButtonState +import co.electriccoin.zcash.ui.design.util.stringRes +import co.electriccoin.zcash.ui.screen.seed.SeedNavigationArgs +import co.electriccoin.zcash.ui.screen.seed.model.SeedSecretState +import co.electriccoin.zcash.ui.screen.seed.model.SeedSecretStateTooltip +import co.electriccoin.zcash.ui.screen.seed.model.SeedState +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.WhileSubscribed +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class SeedViewModel( + observeBackupPersistableWallet: ObserveBackupPersistableWalletUseCase, + observePersistableWallet: ObservePersistableWalletUseCase, + private val args: SeedNavigationArgs, + private val walletRepository: WalletRepository, +) : ViewModel() { + private val isRevealed = MutableStateFlow(false) + + private val observableWallet = + when (args) { + SeedNavigationArgs.NEW_WALLET -> observeBackupPersistableWallet() + SeedNavigationArgs.RECOVERY -> observePersistableWallet() + } + + val navigateBack = MutableSharedFlow() + + val state = + combine(isRevealed, observableWallet) { isRevealed, wallet -> + SeedState( + button = + ButtonState( + text = + when { + args == SeedNavigationArgs.NEW_WALLET -> stringRes(R.string.seed_recovery_next_button) + isRevealed -> stringRes(R.string.seed_recovery_hide_button) + else -> stringRes(R.string.seed_recovery_reveal_button) + }, + onClick = ::onPrimaryButtonClicked, + isEnabled = wallet != null, + isLoading = wallet == null, + icon = + when { + args == SeedNavigationArgs.NEW_WALLET -> null + isRevealed -> R.drawable.ic_seed_hide + else -> R.drawable.ic_seed_show + } + ), + seed = + SeedSecretState( + title = stringRes(R.string.seed_recovery_phrase_title), + text = stringRes(wallet?.seedPhrase?.joinToString().orEmpty()), + isRevealed = isRevealed, + tooltip = null, + onClick = + when (args) { + SeedNavigationArgs.NEW_WALLET -> ::onNewWalletSeedClicked + SeedNavigationArgs.RECOVERY -> null + }, + mode = SeedSecretState.Mode.SEED, + isRevealPhraseVisible = args == SeedNavigationArgs.NEW_WALLET, + ), + birthday = + SeedSecretState( + title = stringRes(R.string.seed_recovery_bday_title), + text = stringRes(wallet?.birthday?.value?.toString().orEmpty()), + isRevealed = isRevealed, + tooltip = + SeedSecretStateTooltip( + title = stringRes(R.string.seed_recovery_bday_tooltip_title), + message = stringRes(R.string.seed_recovery_bday_tooltip_message) + ), + onClick = null, + mode = SeedSecretState.Mode.BIRTHDAY, + isRevealPhraseVisible = false, + ), + onBack = + when (args) { + SeedNavigationArgs.NEW_WALLET -> null + SeedNavigationArgs.RECOVERY -> ::onBack + } + ) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null) + + private fun onBack() { + viewModelScope.launch { + navigateBack.emit(Unit) + } + } + + private fun onPrimaryButtonClicked() { + when (args) { + SeedNavigationArgs.NEW_WALLET -> walletRepository.persistOnboardingState(OnboardingState.READY) + SeedNavigationArgs.RECOVERY -> isRevealed.update { !it } + } + } + + private fun onNewWalletSeedClicked() { + viewModelScope.launch { + isRevealed.update { !it } + } + } +} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/AndroidSeedRecovery.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/AndroidSeedRecovery.kt deleted file mode 100644 index bc6963d33..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/AndroidSeedRecovery.kt +++ /dev/null @@ -1,92 +0,0 @@ -package co.electriccoin.zcash.ui.screen.seedrecovery - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.Composable -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import cash.z.ecc.android.sdk.Synchronizer -import co.electriccoin.zcash.di.koinActivityViewModel -import co.electriccoin.zcash.spackle.ClipboardManagerUtil -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.compose.LocalActivity -import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState -import co.electriccoin.zcash.ui.common.model.VersionInfo -import co.electriccoin.zcash.ui.common.viewmodel.SecretState -import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel -import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator -import co.electriccoin.zcash.ui.screen.seedrecovery.view.SeedRecovery - -@Composable -internal fun WrapSeedRecovery( - goBack: () -> Unit, - onDone: () -> Unit, -) { - val walletViewModel = koinActivityViewModel() - - val synchronizer = walletViewModel.synchronizer.collectAsStateWithLifecycle().value - - val secretState = walletViewModel.secretState.collectAsStateWithLifecycle().value - - val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value - - WrapSeedRecovery( - goBack = goBack, - onDone = onDone, - secretState = secretState, - synchronizer = synchronizer, - topAppBarSubTitleState = walletState - ) -} - -@Composable -@Suppress("LongParameterList") -private fun WrapSeedRecovery( - goBack: () -> Unit, - onDone: () -> Unit, - topAppBarSubTitleState: TopAppBarSubTitleState, - synchronizer: Synchronizer?, - secretState: SecretState, -) { - val activity = LocalActivity.current - - BackHandler { - goBack() - } - - val versionInfo = VersionInfo.new(activity.applicationContext) - - val persistableWallet = - if (secretState is SecretState.Ready) { - secretState.persistableWallet - } else { - null - } - - if (null == synchronizer || null == persistableWallet) { - // 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 { - SeedRecovery( - persistableWallet, - onBack = goBack, - onSeedCopy = { - ClipboardManagerUtil.copyToClipboard( - activity.applicationContext, - activity.getString(R.string.seed_recovery_seed_clipboard_tag), - persistableWallet.seedPhrase.joinToString() - ) - }, - onBirthdayCopy = { - ClipboardManagerUtil.copyToClipboard( - activity.applicationContext, - activity.getString(R.string.seed_recovery_birthday_clipboard_tag), - persistableWallet.birthday?.value.toString() - ) - }, - onDone = onDone, - topAppBarSubTitleState = topAppBarSubTitleState, - versionInfo = versionInfo, - ) - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryTag.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryTag.kt deleted file mode 100644 index 586847903..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryTag.kt +++ /dev/null @@ -1,8 +0,0 @@ -package co.electriccoin.zcash.ui.screen.seedrecovery.view - -/** - * These are only used for automated testing. - */ -object SeedRecoveryTag { - const val DEBUG_MENU_TAG = "debug_menu" -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryView.kt deleted file mode 100644 index aaf6244f5..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/seedrecovery/view/SeedRecoveryView.kt +++ /dev/null @@ -1,278 +0,0 @@ -package co.electriccoin.zcash.ui.screen.seedrecovery.view - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -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.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import cash.z.ecc.android.sdk.model.PersistableWallet -import cash.z.ecc.sdk.fixture.PersistableWalletFixture -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.compose.SecureScreen -import co.electriccoin.zcash.ui.common.compose.shouldSecureScreen -import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState -import co.electriccoin.zcash.ui.common.model.VersionInfo -import co.electriccoin.zcash.ui.common.test.CommonTag.WALLET_BIRTHDAY -import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT -import co.electriccoin.zcash.ui.design.component.BlankBgScaffold -import co.electriccoin.zcash.ui.design.component.BodySmall -import co.electriccoin.zcash.ui.design.component.ChipGrid -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation -import co.electriccoin.zcash.ui.design.component.TopScreenLogoTitle -import co.electriccoin.zcash.ui.design.component.ZashiButton -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions -import co.electriccoin.zcash.ui.design.util.scaffoldPadding -import co.electriccoin.zcash.ui.fixture.VersionInfoFixture -import kotlinx.collections.immutable.toPersistentList - -@Preview(name = "SeedRecovery", device = Devices.PIXEL_4) -@Composable -private fun ComposablePreview() { - ZcashTheme(forceDarkMode = false) { - SeedRecovery( - PersistableWalletFixture.new(), - onBack = {}, - onBirthdayCopy = {}, - onDone = {}, - onSeedCopy = {}, - versionInfo = VersionInfoFixture.new(), - topAppBarSubTitleState = TopAppBarSubTitleState.None, - ) - } -} - -/** - * @param onDone Callback when the user has confirmed viewing the seed phrase. - */ -@Composable -@Suppress("LongParameterList") -fun SeedRecovery( - wallet: PersistableWallet, - onBack: () -> Unit, - onBirthdayCopy: () -> Unit, - onDone: () -> Unit, - onSeedCopy: () -> Unit, - topAppBarSubTitleState: TopAppBarSubTitleState, - versionInfo: VersionInfo, -) { - BlankBgScaffold( - topBar = { - SeedRecoveryTopAppBar( - onBack = onBack, - onSeedCopy = onSeedCopy, - versionInfo = versionInfo, - subTitleState = topAppBarSubTitleState, - ) - } - ) { paddingValues -> - SeedRecoveryMainContent( - wallet = wallet, - onDone = onDone, - onSeedCopy = onSeedCopy, - onBirthdayCopy = onBirthdayCopy, - versionInfo = versionInfo, - // Horizontal paddings will be part of each UI element to minimize a possible truncation on very - // small screens - modifier = - Modifier.scaffoldPadding(paddingValues) - ) - } -} - -@Composable -private fun SeedRecoveryTopAppBar( - onBack: () -> Unit, - onSeedCopy: () -> Unit, - subTitleState: TopAppBarSubTitleState, - versionInfo: VersionInfo, - modifier: Modifier = Modifier, -) { - SmallTopAppBar( - subTitle = - when (subTitleState) { - TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) - TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) - TopAppBarSubTitleState.None -> null - }, - modifier = modifier, - navigationAction = { - TopAppBarBackNavigation( - backText = stringResource(id = R.string.back_navigation).uppercase(), - backContentDescriptionText = stringResource(R.string.back_navigation_content_description), - onBack = onBack - ) - }, - regularActions = { - if (versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService) { - DebugMenu( - onCopyToClipboard = onSeedCopy - ) - } - }, - ) -} - -@Composable -private fun DebugMenu(onCopyToClipboard: () -> Unit) { - Column( - modifier = Modifier.testTag(SeedRecoveryTag.DEBUG_MENU_TAG) - ) { - var expanded by rememberSaveable { mutableStateOf(false) } - IconButton(onClick = { expanded = true }) { - Icon(Icons.Default.MoreVert, contentDescription = null) - } - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - DropdownMenuItem( - text = { - Text(stringResource(id = R.string.seed_recovery_copy)) - }, - onClick = { - onCopyToClipboard() - expanded = false - } - ) - } - } -} - -@Composable -@Suppress("LongParameterList") -private fun SeedRecoveryMainContent( - wallet: PersistableWallet, - onSeedCopy: () -> Unit, - onBirthdayCopy: () -> Unit, - onDone: () -> Unit, - versionInfo: VersionInfo, - modifier: Modifier = Modifier, -) { - Column( - Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .then(modifier), - horizontalAlignment = Alignment.CenterHorizontally - ) { - TopScreenLogoTitle( - title = stringResource(R.string.seed_recovery_header), - logoContentDescription = stringResource(R.string.zcash_logo_content_description), - modifier = Modifier.padding(horizontal = ZashiDimensions.Spacing.spacing3xl) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) - - BodySmall( - text = stringResource(R.string.seed_recovery_description), - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = ZashiDimensions.Spacing.spacing3xl) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) - - SeedRecoverySeedPhrase( - persistableWallet = wallet, - onSeedCopy = onSeedCopy, - onBirthdayCopy = onBirthdayCopy, - versionInfo = versionInfo, - ) - - Spacer( - modifier = - Modifier - .fillMaxHeight() - .weight(MINIMAL_WEIGHT) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) - - ZashiButton( - onClick = onDone, - text = stringResource(R.string.seed_recovery_button_finished), - modifier = - Modifier - .fillMaxWidth() - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun SeedRecoverySeedPhrase( - persistableWallet: PersistableWallet, - onSeedCopy: () -> Unit, - onBirthdayCopy: () -> Unit, - versionInfo: VersionInfo, -) { - if (shouldSecureScreen) { - SecureScreen() - } - - Column { - ChipGrid( - wordList = persistableWallet.seedPhrase.split.toPersistentList(), - onGridClick = onSeedCopy, - allowCopy = versionInfo.isDebuggable && !versionInfo.isRunningUnderTestService, - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingDefault)) - - persistableWallet.birthday?.let { - val interactionSource = remember { MutableInteractionSource() } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - BodySmall( - text = stringResource(R.string.seed_recovery_birthday_height, it.value), - modifier = - Modifier - .testTag(WALLET_BIRTHDAY) - .padding(horizontal = ZcashTheme.dimens.spacingDefault) - .basicMarquee() - // Apply click callback to the text only as the wrapping layout can be much wider - .clickable( - interactionSource = interactionSource, - // Disable ripple - indication = null, - onClick = onBirthdayCopy - ) - ) - } - } - } -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt index 994b3ab00..c10e361d0 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/settings/viewmodel/SettingsViewModel.kt @@ -9,6 +9,7 @@ import co.electriccoin.zcash.ui.NavigationTargets.ABOUT import co.electriccoin.zcash.ui.NavigationTargets.ADVANCED_SETTINGS import co.electriccoin.zcash.ui.NavigationTargets.INTEGRATIONS import co.electriccoin.zcash.ui.NavigationTargets.SUPPORT +import co.electriccoin.zcash.ui.NavigationTargets.WHATS_NEW import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.common.provider.GetVersionInfoProvider import co.electriccoin.zcash.ui.common.usecase.ObserveConfigurationUseCase @@ -140,6 +141,11 @@ class SettingsViewModel( icon = R.drawable.ic_advanced_settings, onClick = ::onAdvancedSettingsClick ), + ZashiSettingsListItemState( + text = stringRes(R.string.settings_whats_new), + icon = R.drawable.ic_settings_whats_new, + onClick = ::onWhatsNewClick + ), ZashiSettingsListItemState( text = stringRes(R.string.settings_about_us), icon = R.drawable.ic_settings_info, @@ -214,6 +220,12 @@ class SettingsViewModel( } } + private fun onWhatsNewClick() { + viewModelScope.launch { + navigationCommand.emit(WHATS_NEW) + } + } + private fun booleanStateFlow(default: BooleanPreferenceDefault): StateFlow = flow { emitAll(default.observe(standardPreferenceProvider())) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/AndroidSupport.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/AndroidSupport.kt deleted file mode 100644 index a6db295c8..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/AndroidSupport.kt +++ /dev/null @@ -1,97 +0,0 @@ -@file:Suppress("ktlint:standard:filename") - -package co.electriccoin.zcash.ui.screen.support - -import android.content.Intent -import androidx.activity.compose.BackHandler -import androidx.annotation.VisibleForTesting -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import co.electriccoin.zcash.di.koinActivityViewModel -import co.electriccoin.zcash.ui.R -import co.electriccoin.zcash.ui.common.compose.LocalActivity -import co.electriccoin.zcash.ui.common.model.TopAppBarSubTitleState -import co.electriccoin.zcash.ui.common.viewmodel.WalletViewModel -import co.electriccoin.zcash.ui.screen.support.model.SupportInfo -import co.electriccoin.zcash.ui.screen.support.model.SupportInfoType -import co.electriccoin.zcash.ui.screen.support.view.Support -import co.electriccoin.zcash.ui.screen.support.viewmodel.SupportViewModel -import co.electriccoin.zcash.ui.util.EmailUtil -import kotlinx.coroutines.launch - -@Composable -internal fun WrapSupport(goBack: () -> Unit) { - val supportViewModel = koinActivityViewModel() - - val walletViewModel = koinActivityViewModel() - - val supportInfo = supportViewModel.supportInfo.collectAsStateWithLifecycle().value - - val walletState = walletViewModel.walletStateInformation.collectAsStateWithLifecycle().value - - BackHandler { - goBack() - } - - WrapSupport( - goBack = goBack, - supportInfo = supportInfo, - topAppBarSubTitleState = walletState - ) -} - -@VisibleForTesting -@Composable -internal fun WrapSupport( - goBack: () -> Unit, - supportInfo: SupportInfo?, - topAppBarSubTitleState: TopAppBarSubTitleState, -) { - val activity = LocalActivity.current - - val snackbarHostState = remember { SnackbarHostState() } - - val scope = rememberCoroutineScope() - - val (isShowingDialog, setShowDialog) = rememberSaveable { mutableStateOf(false) } - - Support( - snackbarHostState = snackbarHostState, - isShowingDialog = isShowingDialog, - setShowDialog = setShowDialog, - onBack = goBack, - onSend = { userMessage -> - val fullMessage = - EmailUtil.formatMessage( - body = userMessage, - supportInfo = supportInfo?.toSupportString(SupportInfoType.entries.toSet()) - ) - val mailIntent = - EmailUtil.newMailActivityIntent( - activity.getString(R.string.support_email_address), - activity.getString(R.string.app_name), - fullMessage - ).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - runCatching { - activity.startActivity(mailIntent) - }.onSuccess { - setShowDialog(false) - }.onFailure { - setShowDialog(false) - scope.launch { - snackbarHostState.showSnackbar( - message = activity.getString(R.string.unable_to_open_email) - ) - } - } - }, - topAppBarSubTitleState = topAppBarSubTitleState, - ) -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/CrashInfo.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/CrashInfo.kt index 4ed561f8f..062734187 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/CrashInfo.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/model/CrashInfo.kt @@ -9,7 +9,7 @@ import co.electriccoin.zcash.spackle.io.listFilesSuspend import kotlinx.datetime.Instant import java.io.File -// TODO [#1301]: Localize support text content +// TODO [#1301]: Localize feedback text content // TODO [#1301]: https://github.com/Electric-Coin-Company/zashi-android/issues/1301 data class CrashInfo(val exceptionClassName: String, val isUncaught: Boolean, val timestamp: Instant) { diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/view/SupportView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/view/SupportView.kt deleted file mode 100644 index 6680bc2a1..000000000 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/support/view/SupportView.kt +++ /dev/null @@ -1,249 +0,0 @@ -package co.electriccoin.zcash.ui.screen.support.view - -import androidx.compose.foundation.ExperimentalFoundationApi -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.height -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -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.AppAlertDialog -import co.electriccoin.zcash.ui.design.component.BlankBgScaffold -import co.electriccoin.zcash.ui.design.component.BlankSurface -import co.electriccoin.zcash.ui.design.component.Body -import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation -import co.electriccoin.zcash.ui.design.component.ZashiButton -import co.electriccoin.zcash.ui.design.component.ZashiTextField -import co.electriccoin.zcash.ui.design.theme.ZcashTheme -import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors -import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography -import co.electriccoin.zcash.ui.design.util.scaffoldPadding - -@Preview -@Composable -private fun SupportPreview() { - ZcashTheme(forceDarkMode = false) { - BlankSurface { - Support( - isShowingDialog = false, - setShowDialog = {}, - onBack = {}, - onSend = {}, - snackbarHostState = SnackbarHostState(), - topAppBarSubTitleState = TopAppBarSubTitleState.None, - ) - } - } -} - -@Preview -@Composable -private fun SupportDarkPreview() { - ZcashTheme(forceDarkMode = true) { - BlankSurface { - Support( - isShowingDialog = false, - setShowDialog = {}, - onBack = {}, - onSend = {}, - snackbarHostState = SnackbarHostState(), - topAppBarSubTitleState = TopAppBarSubTitleState.None, - ) - } - } -} - -@Preview("Support-Popup") -@Composable -private fun PreviewSupportPopup() { - ZcashTheme(forceDarkMode = false) { - SupportConfirmationDialog( - onConfirm = {}, - onDismiss = {} - ) - } -} - -@Composable -@Suppress("LongParameterList") -fun Support( - isShowingDialog: Boolean, - setShowDialog: (Boolean) -> Unit, - onBack: () -> Unit, - onSend: (String) -> Unit, - snackbarHostState: SnackbarHostState, - topAppBarSubTitleState: TopAppBarSubTitleState, -) { - val (message, setMessage) = rememberSaveable { mutableStateOf("") } - - BlankBgScaffold( - topBar = { - SupportTopAppBar( - onBack = onBack, - subTitleState = topAppBarSubTitleState, - ) - }, - snackbarHost = { SnackbarHost(snackbarHostState) } - ) { paddingValues -> - SupportMainContent( - message = message, - setMessage = setMessage, - setShowDialog = setShowDialog, - modifier = Modifier.scaffoldPadding(paddingValues) - ) - - if (isShowingDialog) { - SupportConfirmationDialog( - onConfirm = { onSend(message) }, - onDismiss = { setShowDialog(false) } - ) - } - } -} - -@Composable -private fun SupportTopAppBar( - onBack: () -> Unit, - subTitleState: TopAppBarSubTitleState -) { - SmallTopAppBar( - subTitle = - when (subTitleState) { - TopAppBarSubTitleState.Disconnected -> stringResource(id = R.string.disconnected_label) - TopAppBarSubTitleState.Restoring -> stringResource(id = R.string.restoring_wallet_label) - TopAppBarSubTitleState.None -> null - }, - titleText = stringResource(id = R.string.support_header), - navigationAction = { - TopAppBarBackNavigation( - backText = stringResource(id = R.string.back_navigation).uppercase(), - backContentDescriptionText = stringResource(R.string.back_navigation_content_description), - onBack = onBack - ) - }, - ) -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun SupportMainContent( - message: String, - setMessage: (String) -> Unit, - setShowDialog: (Boolean) -> Unit, - modifier: Modifier = Modifier -) { - val focusRequester = remember { FocusRequester() } - - Column( - Modifier - .fillMaxHeight() - .verticalScroll( - rememberScrollState() - ) - .then(modifier), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) - - Image( - imageVector = ImageVector.vectorResource(R.drawable.zashi_logo_sign), - colorFilter = ColorFilter.tint(color = ZcashTheme.colors.secondaryColor), - contentDescription = null, - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingBig)) - - Body( - text = stringResource(id = R.string.support_information), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) - - ZashiTextField( - value = message, - onValueChange = setMessage, - modifier = - Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - placeholder = { - Text( - text = stringResource(id = R.string.support_hint), - style = ZashiTypography.textMd, - color = ZashiColors.Inputs.Default.text - ) - }, - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) - - Spacer( - modifier = - Modifier - .fillMaxHeight() - .weight(MINIMAL_WEIGHT) - ) - - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) - - // TODO [#1467]: Support screen - keep button above keyboard - // TODO [#1467]: https://github.com/Electric-Coin-Company/zashi-android/issues/1467 - ZashiButton( - onClick = { setShowDialog(true) }, - text = stringResource(id = R.string.support_send), - modifier = - Modifier - .fillMaxWidth() - ) - } - - LaunchedEffect(Unit) { - // Causes the TextFiled to focus on the first screen visit - focusRequester.requestFocus() - } -} - -@Composable -private fun SupportConfirmationDialog( - onConfirm: () -> Unit, - onDismiss: () -> Unit -) { - AppAlertDialog( - onConfirmButtonClick = onConfirm, - confirmButtonText = stringResource(id = R.string.support_confirmation_dialog_ok), - dismissButtonText = stringResource(id = R.string.support_confirmation_dialog_cancel), - onDismissButtonClick = onDismiss, - onDismissRequest = onDismiss, - title = stringResource(id = R.string.support_confirmation_dialog_title), - text = - stringResource( - id = R.string.support_confirmation_explanation, - stringResource(id = R.string.app_name) - ) - ) -} diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/model/WhatsNewState.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/model/WhatsNewState.kt index 6b59596af..4ef6d4716 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/model/WhatsNewState.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/model/WhatsNewState.kt @@ -7,24 +7,28 @@ import co.electriccoin.zcash.ui.design.util.stringRes import kotlinx.datetime.LocalDate data class WhatsNewState( - val version: StringResource, + val titleVersion: StringResource, + val bottomVersion: StringResource, val date: LocalDate, val sections: List ) { companion object { - fun new(changelog: Changelog) = - WhatsNewState( - version = stringRes(R.string.whats_new_version, changelog.version), - date = changelog.date, - sections = - listOfNotNull(changelog.added, changelog.changed, changelog.fixed, changelog.removed) - .map { - WhatsNewSectionState( - stringRes(value = it.title), - stringRes(it.content) - ) - }, - ) + fun new( + changelog: Changelog, + version: String + ) = WhatsNewState( + titleVersion = stringRes(R.string.whats_new_version, changelog.version), + bottomVersion = stringRes(R.string.settings_version, version), + date = changelog.date, + sections = + listOfNotNull(changelog.added, changelog.changed, changelog.fixed, changelog.removed) + .map { + WhatsNewSectionState( + stringRes(value = it.title), + stringRes(it.content) + ) + }, + ) } } diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/view/WhatsNewView.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/view/WhatsNewView.kt index 4f61c6006..2075aa4d9 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/view/WhatsNewView.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/view/WhatsNewView.kt @@ -4,34 +4,40 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.ParagraphStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview -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.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.BlankSurface import co.electriccoin.zcash.ui.design.component.SmallTopAppBar -import co.electriccoin.zcash.ui.design.component.TopAppBarBackNavigation +import co.electriccoin.zcash.ui.design.component.ZashiTopAppBarBackNavigation +import co.electriccoin.zcash.ui.design.component.ZashiVersion import co.electriccoin.zcash.ui.design.theme.ZcashTheme +import co.electriccoin.zcash.ui.design.theme.colors.ZashiColors +import co.electriccoin.zcash.ui.design.theme.dimensions.ZashiDimensions +import co.electriccoin.zcash.ui.design.theme.typography.ZashiTypography import co.electriccoin.zcash.ui.design.util.getValue import co.electriccoin.zcash.ui.fixture.ChangelogFixture +import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.screen.whatsnew.model.WhatsNewSectionState import co.electriccoin.zcash.ui.screen.whatsnew.model.WhatsNewState import kotlinx.datetime.toJavaLocalDate @@ -62,33 +68,42 @@ fun WhatsNewView( ) { Row { Text( - text = state.version.getValue(), - style = ZcashTheme.typography.primary.titleSmall, - fontSize = 13.sp + text = state.titleVersion.getValue(), + style = ZashiTypography.textXl, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.SemiBold ) Text( - modifier = Modifier.weight(1f), + modifier = + Modifier + .weight(1f) + .align(CenterVertically), text = DateTimeFormatter.ISO_LOCAL_DATE.format(state.date.toJavaLocalDate()), textAlign = TextAlign.End, - style = ZcashTheme.typography.primary.titleSmall, - fontSize = 13.sp + style = ZashiTypography.textSm, + fontWeight = FontWeight.SemiBold, + color = ZashiColors.Text.textPrimary, ) } - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingXl)) state.sections.forEach { section -> WhatsNewSection(section) - Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingLarge)) + Spacer(modifier = Modifier.height(ZashiDimensions.Spacing.spacingXl)) } + + Spacer(Modifier.weight(1f)) + + ZashiVersion(modifier = Modifier.fillMaxWidth(), version = state.bottomVersion) } } } @Composable private fun WhatsNewSection(state: WhatsNewSectionState) { - val bulletString = "\u2022\t\t" - val bulletTextStyle = MaterialTheme.typography.bodySmall + val bulletString = "\u2022 " + val bulletTextStyle = ZashiTypography.textSm val bulletTextMeasurer = rememberTextMeasurer() val bulletStringWidth = remember(bulletTextStyle, bulletTextMeasurer) { @@ -116,12 +131,15 @@ private fun WhatsNewSection(state: WhatsNewSectionState) { Column { Text( text = state.title.getValue(), - style = ZcashTheme.typography.primary.titleSmall, + color = ZashiColors.Text.textPrimary, + fontWeight = FontWeight.SemiBold, + style = ZashiTypography.textMd, ) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingMin)) Text( + modifier = Modifier.padding(start = ZashiDimensions.Spacing.spacingMd), text = bulletStyle, style = bulletTextStyle ) @@ -142,11 +160,7 @@ private fun AppBar( }, titleText = stringResource(id = R.string.whats_new_title).uppercase(), navigationAction = { - TopAppBarBackNavigation( - backText = stringResource(id = R.string.back_navigation).uppercase(), - backContentDescriptionText = stringResource(R.string.back_navigation_content_description), - onBack = onBack - ) + ZashiTopAppBarBackNavigation(onBack = onBack) }, ) } @@ -155,7 +169,11 @@ private fun AppBar( private fun WhatsNewViewPreview() { BlankSurface { WhatsNewView( - state = WhatsNewState.new(ChangelogFixture.new()), + state = + WhatsNewState.new( + changelog = ChangelogFixture.new(), + version = VersionInfoFixture.new().versionName + ), walletState = TopAppBarSubTitleState.None, onBack = {} ) diff --git a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/viewmodel/WhatsNewViewModel.kt b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/viewmodel/WhatsNewViewModel.kt index 23d218548..ee0c6b9a9 100644 --- a/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/viewmodel/WhatsNewViewModel.kt +++ b/ui-lib/src/main/java/co/electriccoin/zcash/ui/screen/whatsnew/viewmodel/WhatsNewViewModel.kt @@ -15,7 +15,12 @@ import kotlinx.coroutines.flow.stateIn class WhatsNewViewModel(application: Application) : AndroidViewModel(application) { val state: StateFlow = flow { - val changelog = VersionInfo.new(application).changelog - emit(WhatsNewState.new(changelog)) + val versionInfo = VersionInfo.new(application) + emit( + WhatsNewState.new( + changelog = versionInfo.changelog, + version = versionInfo.versionName + ) + ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(ANDROID_STATE_FLOW_TIMEOUT), null) } diff --git a/ui-lib/src/main/res/ui/about/values-es/strings.xml b/ui-lib/src/main/res/ui/about/values-es/strings.xml index 837b157d7..723cf5bdb 100644 --- a/ui-lib/src/main/res/ui/about/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/about/values-es/strings.xml @@ -1,14 +1,15 @@ Acerca de + Introducing Zashi Versión de Zashi %s Nombre de la app:%1$s Compilación: %1$s - ¡Envía y recibe ZEC en Zashi!\nZashi es una billetera de diseño minimalista y autogestionada, exclusivamente para ZEC, que mantiene tu historial de transacciones y saldo de la billetera privados. Construida por Zcashers, para Zcashers. Desarrollada y mantenida por Electric Coin Co., el inventor de Zcash, Zashi cuenta con un mecanismo de retroalimentación integrado para habilitar más funciones, más rápidamente. + Send and receive ZEC on Zashi!\nZashi is a minimal-design, self-custody, + ZEC-only shielded wallet that keeps your transaction history and wallet balance private.\n\nBuilt by + Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features + a built-in user-feedback mechanism to enable more features, more quickly. + - Consulta nuestra Política de Privacidad\u0020 - aquí - . No se pudo encontrar una aplicación de navegador web. - Novedades Política de Privacidad diff --git a/ui-lib/src/main/res/ui/about/values/strings.xml b/ui-lib/src/main/res/ui/about/values/strings.xml index 41c88f94d..9c25a1e92 100644 --- a/ui-lib/src/main/res/ui/about/values/strings.xml +++ b/ui-lib/src/main/res/ui/about/values/strings.xml @@ -1,14 +1,15 @@ About + Introducing Zashi Zashi Version %s App name:%1$s Build: %1$s - Send and receive ZEC on Zashi!\nZashi is a minimal-design, self-custody, ZEC-only shielded wallet that keeps your transaction history and wallet balance private. Built by Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features a built-in user-feedback mechanism to enable more features, more quickly. + Send and receive ZEC on Zashi!\nZashi is a minimal-design, self-custody, + ZEC-only shielded wallet that keeps your transaction history and wallet balance private.\n\nBuilt by + Zcashers, for Zcashers. Developed and maintained by Electric Coin Co., the inventor of Zcash, Zashi features + a built-in user-feedback mechanism to enable more features, more quickly. + - See our Privacy Policy\u0020 - here - . Unable to find a web browser app. - What\'s new Privacy Policy \ No newline at end of file diff --git a/ui-lib/src/main/res/ui/advanced_settings/values-es/strings.xml b/ui-lib/src/main/res/ui/advanced_settings/values-es/strings.xml index 93e7d8e5f..d54c84371 100644 --- a/ui-lib/src/main/res/ui/advanced_settings/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/advanced_settings/values-es/strings.xml @@ -5,5 +5,5 @@ Elegir un servidor Conversión de moneda Se te pedirá confirmación en la siguiente pantalla - Eliminar Zashi + Reset Zashi diff --git a/ui-lib/src/main/res/ui/advanced_settings/values/strings.xml b/ui-lib/src/main/res/ui/advanced_settings/values/strings.xml index 0cee94fd5..199810be1 100644 --- a/ui-lib/src/main/res/ui/advanced_settings/values/strings.xml +++ b/ui-lib/src/main/res/ui/advanced_settings/values/strings.xml @@ -5,5 +5,5 @@ Choose a Server Currency Conversion You will be asked to confirm on the next screen - Delete Zashi + Reset Zashi diff --git a/ui-lib/src/main/res/ui/common/drawable/ic_warning.xml b/ui-lib/src/main/res/ui/common/drawable/ic_warning.xml new file mode 100644 index 000000000..61cf47e32 --- /dev/null +++ b/ui-lib/src/main/res/ui/common/drawable/ic_warning.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/ui-lib/src/main/res/ui/common/drawable/ic_zashi_tooltip.xml b/ui-lib/src/main/res/ui/common/drawable/ic_zashi_tooltip.xml new file mode 100644 index 000000000..41ea870be --- /dev/null +++ b/ui-lib/src/main/res/ui/common/drawable/ic_zashi_tooltip.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/ui-lib/src/main/res/ui/delete_wallet/values-es/strings.xml b/ui-lib/src/main/res/ui/delete_wallet/values-es/strings.xml index 22972bb6a..94fdf9f54 100644 --- a/ui-lib/src/main/res/ui/delete_wallet/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/delete_wallet/values-es/strings.xml @@ -1,23 +1,12 @@ - - Eliminar %1$s - - + Reset Zashi - Por favor, no elimine esta aplicación a menos que esté seguro de comprender los efectos. + Please don\'t reset this app unless you\'re sure you understand the effects. - Eliminar la aplicación %1$s eliminará la base de datos y los datos - almacenados en caché. Cualquier fondo que tenga en esta billetera se perderá y solo podrá recuperarse utilizando su frase de - recuperación secreta de %1$s en %1$s u otra billetera Zcash. - - - Entiendo - - - Eliminar %1$s + Resetting the Zashi app will delete the database and cached data. Any funds you have in this wallet will be lost and can only be recovered by using your Zashi secret recovery phrase in another Zcash wallet. - - La eliminación de la billetera falló. Inténtelo de nuevo, por favor. + I understand + Reset Zashi + Wallet deletion failed. Try it again, please. diff --git a/ui-lib/src/main/res/ui/delete_wallet/values/strings.xml b/ui-lib/src/main/res/ui/delete_wallet/values/strings.xml index 4545517df..94fdf9f54 100644 --- a/ui-lib/src/main/res/ui/delete_wallet/values/strings.xml +++ b/ui-lib/src/main/res/ui/delete_wallet/values/strings.xml @@ -1,23 +1,12 @@ - - Delete %1$s - - + Reset Zashi - Please don\'t delete this app unless you\'re sure you understand the effects. + Please don\'t reset this app unless you\'re sure you understand the effects. - Deleting the %1$s app will delete the database and cached - data. Any funds you have in this wallet will be lost and can only be recovered by using your %1$s secret recovery phrase in %1$s or another Zcash wallet. + Resetting the Zashi app will delete the database and cached data. Any funds you have in this wallet will be lost and can only be recovered by using your Zashi secret recovery phrase in another Zcash wallet. - I understand - - - Delete %1$s - - + Reset Zashi Wallet deletion failed. Try it again, please. diff --git a/ui-lib/src/main/res/ui/export_data/values-es/strings.xml b/ui-lib/src/main/res/ui/export_data/values-es/strings.xml index e2fcb0833..5e1ce8541 100644 --- a/ui-lib/src/main/res/ui/export_data/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/export_data/values-es/strings.xml @@ -1,9 +1,16 @@ + Data Export Consentimiento para Exportar Datos Privados - Al hacer clic en \"Estoy de acuerdo\" a continuación, das tu consentimiento para exportar los datos privados de Zashi, lo cual incluye todo el historial de la billetera, toda la información privada, los memos, los montos y las direcciones de los destinatarios, incluso para tu actividad protegida.*\n\nEstos datos privados también permiten ver ciertas acciones futuras que realices con Zashi.\n\nCompartir estos datos privados es irrevocable: una vez que hayas compartido estos datos privados con alguien, no hay forma de revocar su acceso. - *Ten en cuenta que estos datos privados no les dan la capacidad de gastar tus fondos, solo la capacidad de ver lo que haces con tus fondos. - Exportar datos privados - Estoy de acuerdo + + By clicking “I Agree” below, you give your consent to export Zashi\’s private data which includes the entire + history of the wallet, sll private information, memos, amounts, and recipient addresses, even for your + shielded activity.*\n\nThe private data also gives the ability to see certain future actions you take with + Zashi.\n\nSharing this private data is irrevocable - once you have shared this private data with someone, there + is no way to revoke their access.\n\n*Note that this private data does not give them the ability to spend your + funds, only the ability to see what you do with your funds. + + Export Private Data + I agree to Zashi\'s Export Private Data Policies and Privacy Policy Compartir datos internos de Zashi con: No se pudo encontrar una aplicación con la cual compartir. diff --git a/ui-lib/src/main/res/ui/export_data/values/strings.xml b/ui-lib/src/main/res/ui/export_data/values/strings.xml index 763d8c408..01fb733ce 100644 --- a/ui-lib/src/main/res/ui/export_data/values/strings.xml +++ b/ui-lib/src/main/res/ui/export_data/values/strings.xml @@ -1,14 +1,16 @@ + Data Export Consent for Exporting Private Data - By clicking \"I Agree\" below, you give your consent to export Zashi’s private - data which includes the entire history of the wallet, all private information, memos, amounts and recipient - addresses, even for your shielded activity.*\n\nThis private data also gives the ability to see certain future - actions you take with Zashi.\n\nSharing this private data is irrevocable — once you have shared this private - data with someone, there is no way to revoke their access. - *Note that this private data does not give them the ability to spend your - funds, only the ability to see what you do with your funds. - Export private data - I agree + + By clicking “I Agree” below, you give your consent to export Zashi\’s private data which includes the entire + history of the wallet, sll private information, memos, amounts, and recipient addresses, even for your + shielded activity.*\n\nThe private data also gives the ability to see certain future actions you take with + Zashi.\n\nSharing this private data is irrevocable - once you have shared this private data with someone, there + is no way to revoke their access.\n\n*Note that this private data does not give them the ability to spend your + funds, only the ability to see what you do with your funds. + + Export Private Data + I agree to Zashi\'s Export Private Data Policies and Privacy Policy Share internal Zashi data with: Unable to find an application to share with. \ No newline at end of file diff --git a/ui-lib/src/main/res/ui/feedback/drawable-night/ic_feedback.xml b/ui-lib/src/main/res/ui/feedback/drawable-night/ic_feedback.xml new file mode 100644 index 000000000..02904d39c --- /dev/null +++ b/ui-lib/src/main/res/ui/feedback/drawable-night/ic_feedback.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_1.png b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_1.png new file mode 100644 index 000000000..1a8064f42 Binary files /dev/null and b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_1.png differ diff --git a/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_2.png b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_2.png new file mode 100644 index 000000000..f82e3a810 Binary files /dev/null and b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_2.png differ diff --git a/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_3.png b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_3.png new file mode 100644 index 000000000..37c62d030 Binary files /dev/null and b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_3.png differ diff --git a/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_4.png b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_4.png new file mode 100644 index 000000000..6eb57af9d Binary files /dev/null and b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_4.png differ diff --git a/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_5.png b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_5.png new file mode 100644 index 000000000..b843d15a6 Binary files /dev/null and b/ui-lib/src/main/res/ui/feedback/drawable/ic_emoji_5.png differ diff --git a/ui-lib/src/main/res/ui/feedback/drawable/ic_feedback.xml b/ui-lib/src/main/res/ui/feedback/drawable/ic_feedback.xml new file mode 100644 index 000000000..a82cd77a1 --- /dev/null +++ b/ui-lib/src/main/res/ui/feedback/drawable/ic_feedback.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/ui-lib/src/main/res/ui/support/values/strings.xml b/ui-lib/src/main/res/ui/feedback/values-es/strings.xml similarity index 55% rename from ui-lib/src/main/res/ui/support/values/strings.xml rename to ui-lib/src/main/res/ui/feedback/values-es/strings.xml index 6d7a60fa5..6cded4448 100644 --- a/ui-lib/src/main/res/ui/support/values/strings.xml +++ b/ui-lib/src/main/res/ui/feedback/values-es/strings.xml @@ -1,12 +1,16 @@ Support - - How can we help? + Send Us Feedback + How is your Zashi experience? + How can we help you? + I would like to ask about… Send OK Cancel Open e-mail app - %1$s is about to + Zashi is about to open your e-mail app with a pre-filled message.\n\nBe sure to hit send within your e-mail app. Please let us know about any problems you have had, or features you want to see in the future. + How is your Zashi experience?\n%s %s/5 + How can we help you?\n%s diff --git a/ui-lib/src/main/res/ui/feedback/values/strings.xml b/ui-lib/src/main/res/ui/feedback/values/strings.xml new file mode 100644 index 000000000..6cded4448 --- /dev/null +++ b/ui-lib/src/main/res/ui/feedback/values/strings.xml @@ -0,0 +1,16 @@ + + Support + Send Us Feedback + How is your Zashi experience? + How can we help you? + I would like to ask about… + Send + OK + Cancel + Open e-mail app + Zashi is about to + open your e-mail app with a pre-filled message.\n\nBe sure to hit send within your e-mail app. + Please let us know about any problems you have had, or features you want to see in the future. + How is your Zashi experience?\n%s %s/5 + How can we help you?\n%s + diff --git a/ui-lib/src/main/res/ui/new_wallet_recovery/values-es/strings.xml b/ui-lib/src/main/res/ui/new_wallet_recovery/values-es/strings.xml deleted file mode 100644 index 2bb017503..000000000 --- a/ui-lib/src/main/res/ui/new_wallet_recovery/values-es/strings.xml +++ /dev/null @@ -1,9 +0,0 @@ - - Tu frase secreta de recuperación - Las siguientes 24 palabras son la clave para tus fondos y constituyen la única forma de recuperarlos si pierdes el acceso o adquieres un nuevo dispositivo. ¡Protege tu ZEC almacenando esta frase en un lugar de confianza y nunca la compartas con nadie! - Altura de cumpleaños de la billetera: %1$d - Ya la he guardado - Toca para copiar - Frase de Semilla de Zcash - Cumpleaños de la Billetera Zcash - diff --git a/ui-lib/src/main/res/ui/new_wallet_recovery/values/strings.xml b/ui-lib/src/main/res/ui/new_wallet_recovery/values/strings.xml deleted file mode 100644 index f4f9e7243..000000000 --- a/ui-lib/src/main/res/ui/new_wallet_recovery/values/strings.xml +++ /dev/null @@ -1,12 +0,0 @@ - - Your secret recovery phrase - The following 24 words are the keys to your funds and are the only way to - recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a - place you trust and never share it with anyone! - Wallet birthday height: %1$d - I\'ve saved it - Tap to Copy - Zcash Seed Phrase - Zcash Wallet Birthday - diff --git a/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_reveal.xml b/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_reveal.xml new file mode 100644 index 000000000..b7bea3fa5 --- /dev/null +++ b/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_reveal.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_seed_hide.xml b/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_seed_hide.xml new file mode 100644 index 000000000..fe92d0d64 --- /dev/null +++ b/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_seed_hide.xml @@ -0,0 +1,13 @@ + + + diff --git a/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_seed_show.xml b/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_seed_show.xml new file mode 100644 index 000000000..969d78c9b --- /dev/null +++ b/ui-lib/src/main/res/ui/seed_recovery/drawable/ic_seed_show.xml @@ -0,0 +1,20 @@ + + + + diff --git a/ui-lib/src/main/res/ui/seed_recovery/values-es/strings.xml b/ui-lib/src/main/res/ui/seed_recovery/values-es/strings.xml index 14175bf89..7d09357a4 100644 --- a/ui-lib/src/main/res/ui/seed_recovery/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/seed_recovery/values-es/strings.xml @@ -1,9 +1,18 @@ - Tu frase secreta de recuperación - Las siguientes 24 palabras son la clave para tus fondos y constituyen la única forma de recuperarlos si pierdes el acceso o adquieres un nuevo dispositivo. ¡Protege tu ZEC almacenando esta frase en un lugar de confianza y nunca la compartas con nadie! - Altura de cumpleaños de la billetera: %1$d - ¡Entendido! - Toca para copiar - Frase de Semilla de Zcash - Cumpleaños de la Billetera Zcash + Recovery Phrase + Secure Your Wallet + The following 24 words are the keys to your funds and are the only way + to recover your funds if you get locked out or get a new device. + Reveal security details + Hide security details + Next + Recovery Phrase + Wallet Birthday Height + Wallet Birthday Height + Wallet Birthday Height determines the birth (chain) height of + your wallet and facilitates faster wallet restore process. Save this number together with your seed phrase in + a safe place. + Protect your ZEC by storing this phrase in a place you trust and never share + it with anyone! + Reveal recovery phrase diff --git a/ui-lib/src/main/res/ui/seed_recovery/values/strings.xml b/ui-lib/src/main/res/ui/seed_recovery/values/strings.xml index 9951e17d2..7d09357a4 100644 --- a/ui-lib/src/main/res/ui/seed_recovery/values/strings.xml +++ b/ui-lib/src/main/res/ui/seed_recovery/values/strings.xml @@ -1,12 +1,18 @@ - Your secret recovery phrase - The following 24 words are the keys to your funds and are the only way to - recover your funds if you get locked out or get a new device. Protect your ZEC by storing this phrase in a - place you trust and never share it with anyone! - Wallet birthday height: %1$d - I got it! - Tap to Copy - Zcash Seed Phrase - Zcash Wallet Birthday + Recovery Phrase + Secure Your Wallet + The following 24 words are the keys to your funds and are the only way + to recover your funds if you get locked out or get a new device. + Reveal security details + Hide security details + Next + Recovery Phrase + Wallet Birthday Height + Wallet Birthday Height + Wallet Birthday Height determines the birth (chain) height of + your wallet and facilitates faster wallet restore process. Save this number together with your seed phrase in + a safe place. + Protect your ZEC by storing this phrase in a place you trust and never share + it with anyone! + Reveal recovery phrase diff --git a/ui-lib/src/main/res/ui/settings/drawable-night/ic_settings_whats_new.xml b/ui-lib/src/main/res/ui/settings/drawable-night/ic_settings_whats_new.xml new file mode 100644 index 000000000..1e36efc92 --- /dev/null +++ b/ui-lib/src/main/res/ui/settings/drawable-night/ic_settings_whats_new.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ui-lib/src/main/res/ui/settings/drawable/ic_settings_whats_new.xml b/ui-lib/src/main/res/ui/settings/drawable/ic_settings_whats_new.xml new file mode 100644 index 000000000..aedbcd80f --- /dev/null +++ b/ui-lib/src/main/res/ui/settings/drawable/ic_settings_whats_new.xml @@ -0,0 +1,16 @@ + + + + diff --git a/ui-lib/src/main/res/ui/settings/values-es/strings.xml b/ui-lib/src/main/res/ui/settings/values-es/strings.xml index 0f583a536..8d73cab7b 100644 --- a/ui-lib/src/main/res/ui/settings/values-es/strings.xml +++ b/ui-lib/src/main/res/ui/settings/values-es/strings.xml @@ -6,6 +6,7 @@ Envíanos tus Comentarios Versión %s Libreta de Direcciones + What\'s New Configuraciones adicionales Reescanear blockchain diff --git a/ui-lib/src/main/res/ui/settings/values/strings.xml b/ui-lib/src/main/res/ui/settings/values/strings.xml index c7db9c897..0684743fc 100644 --- a/ui-lib/src/main/res/ui/settings/values/strings.xml +++ b/ui-lib/src/main/res/ui/settings/values/strings.xml @@ -6,6 +6,7 @@ Send Us Feedback Version %s Address Book + What\'s New Additional settings Rescan blockchain diff --git a/ui-screenshot-test/src/main/java/co/electroniccoin/zcash/ui/screenshot/ScreenshotTest.kt b/ui-screenshot-test/src/main/java/co/electroniccoin/zcash/ui/screenshot/ScreenshotTest.kt index 7f9429ef7..ee24e53c8 100644 --- a/ui-screenshot-test/src/main/java/co/electroniccoin/zcash/ui/screenshot/ScreenshotTest.kt +++ b/ui-screenshot-test/src/main/java/co/electroniccoin/zcash/ui/screenshot/ScreenshotTest.kt @@ -7,6 +7,7 @@ import android.os.Build import android.os.LocaleList import androidx.activity.viewModels import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.SemanticsNodeInteraction import androidx.compose.ui.test.assertIsEnabled import androidx.compose.ui.test.hasTestTag import androidx.compose.ui.test.hasText @@ -256,6 +257,21 @@ class ScreenshotTest : UiTestPrerequisites() { it.performClick() } + composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { + composeTestRule.onNodeWithText( + text = resContext.getString(R.string.restore_success_button), + ignoreCase = true + ).exists() + } + + composeTestRule.onNodeWithText( + text = resContext.getString(R.string.restore_success_button), + ignoreCase = true + ).also { + it.performScrollTo() + it.performClick() + } + composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { composeTestRule.activity.walletViewModel.walletSnapshot.value != null } @@ -327,7 +343,6 @@ class ScreenshotTest : UiTestPrerequisites() { // These are the home screen bottom navigation sub-screens onboardingScreenshots(resContext, tag, composeTestRule) - recoveryScreenshots(resContext, tag, composeTestRule) // To ensure that the bottom tab is available, or wait until it is composeTestRule.waitUntilAtLeastOneExists(hasTestTag(HomeTag.TAB_ACCOUNT), DEFAULT_TIMEOUT_MILLISECONDS) @@ -374,7 +389,10 @@ private fun onboardingScreenshots( } // Welcome screen - composeTestRule.onNodeWithText(resContext.getString(R.string.onboarding_header)).also { + composeTestRule.onNodeWithText( + resContext.getString(R.string.onboarding_header), + useUnmergedTree = true + ).also { it.assertExists() ScreenshotTest.takeScreenshot(tag, "Onboarding 1") } @@ -383,7 +401,8 @@ private fun onboardingScreenshots( composeTestRule.onNodeWithText( text = resContext.getString(R.string.onboarding_create_new_wallet), - ignoreCase = true + ignoreCase = true, + useUnmergedTree = true ).also { it.performClick() } @@ -391,7 +410,8 @@ private fun onboardingScreenshots( // Security Warning screen composeTestRule.onNodeWithText( text = resContext.getString(R.string.security_warning_acknowledge), - ignoreCase = true + ignoreCase = true, + useUnmergedTree = true ).also { it.assertExists() it.performClick() @@ -399,32 +419,25 @@ private fun onboardingScreenshots( } composeTestRule.onNodeWithText( text = resContext.getString(R.string.security_warning_confirm), - ignoreCase = true - ).also { - it.performClick() - } -} + ignoreCase = true, + useUnmergedTree = true + ).performClick() -@Suppress("LongMethod", "CyclomaticComplexMethod") -private fun recoveryScreenshots( - resContext: Context, - tag: String, - composeTestRule: AndroidComposeTestRule, MainActivity> -) { - composeTestRule.waitUntil(DEFAULT_TIMEOUT_MILLISECONDS) { - composeTestRule.activity.walletViewModel.secretState.value is SecretState.NeedsBackup - } + composeTestRule.waitForIdle() - composeTestRule.onNodeWithText(resContext.getString(R.string.new_wallet_recovery_header)).also { - it.assertExists() + composeTestRule.waitUntil { + composeTestRule.onNodeWithText( + text = resContext.getString(R.string.seed_recovery_next_button), + ignoreCase = true, + useUnmergedTree = true + ).exists() } - ScreenshotTest.takeScreenshot(tag, "Recovery 1") composeTestRule.onNodeWithText( - text = resContext.getString(R.string.new_wallet_recovery_button_finished), - ignoreCase = true + text = resContext.getString(R.string.seed_recovery_next_button), + ignoreCase = true, + useUnmergedTree = true ).also { - it.assertExists() it.performScrollTo() it.performClick() } @@ -593,3 +606,13 @@ private fun seedScreenshots( ScreenshotTest.takeScreenshot(tag, "Seed 1") } + +@Suppress("SwallowedException", "TooGenericExceptionCaught") +private fun SemanticsNodeInteraction.exists(): Boolean { + return try { + this.assertExists() + true + } catch (e: Throwable) { + false + } +}